Patterns we used¶
Reusable shapes that came out of building this agent on AAA.
Verification gate: let code veto the model¶
GenerateTestRepair takes a named validator, any function (candidate, context) -> Verdict. We use it as an anti-hallucination gate:
def make_validator(get_pages):
"""get_pages(context) -> {url: text}. Registered on ValidationHandler as 'facts'."""
def validate(candidate: str, context: dict) -> Verdict:
try:
ex = parse_extraction(candidate)
except (ValueError, ValidationError) as e:
return Verdict(ok=False, feedback=f"Reply with ONLY valid JSON matching the schema. Error: {str(e)[:300]}")
_, problems = ground(ex, get_pages(context))
if not problems:
return Verdict(ok=True)
shown = "; ".join(problems[:8]) + (f" (+{len(problems) - 8} more)" if len(problems) > 8 else "")
return Verdict(
ok=False,
feedback="Remove or fix every unsupported claim. Never guess; use null/[] when the pages do not say it. Each quote must be ONE short contiguous snippet copied exactly (never stitch lines together or skip text between them). "
f"Problems: {shown}",
)
return validate
The validator parses the model's JSON, checks every claim against pages stored in SQLite, and returns specific feedback
(quote not found verbatim on <url>). The model sees that feedback and retries, up to max_iterations.
If it never converges, AAA returns no_solution with the best candidate, and we salvage only the claims that pass the same check.
The result: output is never worse than "fewer facts", never "invented facts".
Single-tool graphs for fan-out¶
WorkflowGraph nodes are static, but "fetch these N URLs" is dynamic. Instead of building a graph per call, define a one-node graph whose
arguments are the run input, and run it many times concurrently:
def _one(tool, name):
return _named(WorkflowGraph([Node("r", tool, args=lambda r, inp: dict(inp))], output="r"), name)
results = await asyncio.gather(*(engine.run(fetch_page_graph, {"candidate_id": 7, "url": u}) for u in urls))
You keep tool timeouts, pools and logging, with no per-call algorithm objects.
One long-lived instance per role, unique names¶
AAA routes and recovers runs by algorithm name, and registering a new instance each call leaks a bus subscription. So we create
each algorithm once, give it a unique name, and put per-run variation in the run input, not the object.
Trusted run metadata for tool scoping¶
A bounded ToolLoop may only read pages on a product's own site. The allowed host must not come from the model, so it travels in the
run's metadata (trusted application data) and the tool reads it from ToolContext:
await engine.run(gapfill_loop, {"task": "..."}, meta={"candidate_id": 42})
@tool
async def read_page(url: str, ctx: ToolContext) -> dict:
cand = repo.candidate(int(ctx.meta["candidate_id"]))
if not allowed(url, cand):
raise ToolError("only pages on the product's own website or GitHub repo may be read")
ctx is injected and hidden from the model's schema, so a prompt injection cannot change which candidate it is acting for.
Route models, not agents¶
AAA's LLMHandler talks to one LLMClient. To run a small critic and an embedding model locally while extraction stays on a hosted
gateway, we wrap two clients in a router that dispatches on the model id:
class RoutingClient:
"""Local models for ids matching `prefixes`; the gateway for the rest."""
def __init__(self, remote: LLMClient, local: LLMClient, prefixes: tuple[str, ...]) -> None:
self.remote, self.local, self.prefixes = remote, local, prefixes
def _pick(self, model: str | None) -> LLMClient:
return self.local if is_local(model, self.prefixes) else self.remote
async def complete(self, request: LLMRequest, /) -> LLMResponse:
return await self._pick(request.model).complete(request)
async def embed(self, texts: list[str], model: str | None = None) -> EmbedResponse:
return await self._pick(model).embed(texts, model)
async def aclose(self) -> None:
await self.remote.aclose()
await self.local.aclose()
Anything starting with mlx-community/ runs in-process on Apple silicon (setup); everything else goes to the
OpenAI-compatible gateway. Algorithms never know the difference.
Crash safety on SQLite¶
SQLiteEventStore is WAL-mode and crash-safe, with one engine process per file. We keep AAA's event log (aaa_events.db) separate from
our business tables (botshub.db), run a single worker process with asyncio concurrency, and call engine.recover(all_algorithms) at startup.
"Never downgrade" for re-runs¶
Re-enriching a published listing can fail (LLM outage, thin page). Rather than overwrite good data, the pipeline records kept_previous and
leaves the live listing alone. Idempotent upserts keyed by slug mean replays never duplicate.