Skip to content

Choosing algorithms

AAA's rule of thumb (from its algorithm guide): start with the simplest algorithm that fits, prefer determinism you can have, and verify with code before verifying with a model. This is how that played out for a real pipeline.

Stage AAA algorithm Why this one
Collect signals (GitHub, HN, SearXNG) WorkflowGraph The steps are known and independent. A DAG runs them in parallel with zero LLM calls, and on_error="continue" means one dead source never stops the run.
"Is this an AI agent?" GenerateTestRepair + SelfConsistency Structured JSON checked by a schema validator. Borderline confidence (0.4 to 0.6) gets a 3-way vote, because the label is an exact answer.
Fetch a page single-tool WorkflowGraph Reuses tool timeouts, pools and the execution guard; one run per URL, many in parallel.
Extract facts GenerateTestRepair with a custom validator The validator is our code: every claim's quote must appear verbatim on a fetched page. Failure feedback goes back to the model.
Find a missing pricing/features page ToolLoop (6 turns, 2 tools) The model must decide where to look, so it earns a loop, but it is bounded and its tools are restricted to the product's own site.
Confirm prices SelfConsistency Prices are exact strings, which is where voting works.
Write the overview GeneratorCritic The one place we generate prose: only from validated facts, judged by a second model, then re-checked by code.

What we deliberately did not use

Algorithm Why not
ReAct Native tool calling is available.
PlanAndExecute Our steps are known, so a DAG is cheaper and more predictable. Revisit for open-ended research.
Supervisor / Swarm / Router One pipeline, no specialists to hand off between. (We route models, not tasks; see patterns.)
Reflexion / Debate / TreeOfThoughts AAA marks their quality gains as unverified with real models, and they cost more calls.
FSM Listing lifecycle is a status column. An FSM earns its place when something must wait days for a human or a webhook.
Memory libraries (Mem0, Graphiti, ...) SQLite tables are our memory.

Source

The role-to-instance table is a single small file:

"""One long-lived instance per role, each with a UNIQUE name.

AAA routes and recovers runs by algorithm name, and registering a new instance per call would
leak a bus subscription each time. Per-run variation travels in the run input, not in the object."""

from __future__ import annotations

from dataclasses import dataclass

from aaa.algos import GenerateTestRepair, GeneratorCritic, Node, SelfConsistency, WorkflowGraph

from botshub_agent.config import Settings
from botshub_agent.discovery import classify
from botshub_agent.discovery.workflow import build_graph
from botshub_agent.enrich import extract, gapfill, overview, pricevote


def _named(algo, name: str):
    algo.name = name
    return algo


def _one(tool: str, name: str) -> WorkflowGraph:
    """A single-tool graph whose arguments are the run input."""
    return _named(WorkflowGraph([Node("r", tool, args=lambda r, inp: dict(inp))], output="r"), name)


@dataclass
class Algos:
    discovery: WorkflowGraph
    fetch_page: WorkflowGraph
    github_readme: WorkflowGraph
    count_mentions: WorkflowGraph
    capture: WorkflowGraph
    videos: WorkflowGraph
    classify: GenerateTestRepair
    vote: SelfConsistency
    extract: GenerateTestRepair
    overview: GeneratorCritic
    gapfill: object
    price_vote: SelfConsistency

    def all(self) -> list:
        return [self.discovery, self.fetch_page, self.github_readme, self.count_mentions, self.capture, self.videos,
                self.classify, self.vote, self.extract, self.overview, self.gapfill, self.price_vote]


def make_algos(s: Settings, registry) -> Algos:
    return Algos(
        discovery=_named(build_graph(s), "bh_discovery"),
        fetch_page=_one("fetch_page", "bh_fetch_page"),
        github_readme=_one("github_readme", "bh_github_readme"),
        count_mentions=_one("count_mentions", "bh_count_mentions"),
        capture=_one("capture_screenshot", "bh_capture"),
        videos=_one("find_product_videos", "bh_find_videos"),
        classify=_named(classify.classify_algo(s.llm_model), "bh_classify"),
        vote=_named(classify.vote_algo(s.llm_model), "bh_classify_vote"),
        extract=_named(extract.extraction_algo(s.llm_model), "bh_extract"),
        overview=_named(overview.overview_algo(s.llm_model, s.critic_model), "bh_overview"),
        gapfill=_named(gapfill.gapfill_algo(registry, s.llm_model), "bh_gapfill"),
        price_vote=_named(pricevote.price_vote_algo(s.llm_model, s.price_votes), "bh_price_vote"),
    )