anvil
Grounded answers, forged from cited sources
Support agents fail quietly: they hallucinate answers, cite nothing, call the wrong tool, or post things nobody approved. Anvil is a support agent over the real FastAPI documentation, built evals first, so every one of those failure modes is a number CI can watch.
How can I enable CORS in FastAPI?
Add CORSMiddleware to the application and pass the origins you want to allow. 1
Configure allow_origins, allow_methods and allow_headers explicitly rather than widening all three at once. 1
Middleware is added with app.add_middleware(), and order matters for anything wrapping the response. 2
sources
tutorial-cors#use-corsmiddlewareadvanced-middlewaretutorial-bigger-applicationssource: anvil eval retrieval · 52 real user questions (Stack Overflow + GitHub issues) over 1,199 chunks of the real FastAPI docs · hybrid search + cross-encoder rerank, fallback embedder all-MiniLM-L6-v2 (the keyless CI arm)
How it works
A LangGraph agent with a SQLite checkpointer, budget guards on every run, and a hard write boundary. Retrieval is hybrid: Postgres full-text and pgvector cosine each return 50 candidates, Reciprocal Rank Fusion merges them, and a cross-encoder reranks the top 25 on CPU. The top rerank score doubles as a grounding-confidence signal.
- 01Route
claude-haiku-4-5 classifies the message: question, action, or smalltalk.
- 02Retrieve and rerank
FTS + pgvector, RRF fusion, then ms-marco-MiniLM-L-6-v2 cross-encoder rerank. Heading-aware chunks give citation targets like
tutorial-cors#use-corsmiddleware. - 03Answer or refuse
Above the confidence threshold, claude-sonnet-5 answers with citations required. Below it, a deterministic refusal fires without spending an LLM call.
- 04Act behind a human
get_github_issuefetches live issues;draft_issue_commentis held behind a LangGraph HITL interrupt until a human approves. Nothing is ever posted to GitHub from this codebase. - 05Gate
anvil gatecompares every eval run against a committed baseline and exits nonzero when any metric drops more than 0.02. CI runs it on every push.
# hybrid + cross-encoder rerank, 52-query golden set recall@5 0.5673 recall@10 0.7404 MRR 0.5658 nDCG@10 0.5847 # fusion-only (no reranker), same 52 queries recall@5 0.4615 recall@10 0.6250 MRR 0.4077 nDCG@10 0.4385 # reranker is worth ~10 recall points at k=5 # and ~15 MRR points on this corpus
The eval stack is the product
Two layers plus a gate, and the numbers are deliberately unglamorous. The questions are real (vague titles, error messages, XY problems) and the corpus is real (1,199 chunks with heavy topical overlap), so the scores are what retrieval actually looks like before you tune it, measured, with a gate that catches regressions from here.
- L1Retrieval, keyless, gates CI
52 golden queries: 45 mined from Stack Overflow titles, 7 from FastAPI GitHub issues, each carrying its source URL. Relevant chunks labeled by inspection; every label is validated against the ingested corpus first, so a renamed heading fails loudly.
- L2End-to-end agent, fixture-proven
42 golden conversations: 16 grounded answers with expected citations, 10 refusals on questions the docs genuinely cannot answer, 10 GitHub-issue lookups with expected arguments, 5 HITL comment drafts (approve and reject paths), 1 smalltalk. Deterministic checks first, then a claude-sonnet-5 judge scores faithfulness and relevancy.
- L3The gate
GitHub Actions runs lint, the 50-test suite, ingestion, the keyless retrieval eval, and
anvil gateon every push. A regression past tolerance goes red.
# rank-1 rerank score distributions, real data answerable (52): median 4.4, 5 of 52 below 0 known gaps (10): range -9.7 to 4.6, 4 of 10 below 0 deterministic threshold: 0.0 (the cross-encoder's own relevance boundary) # the populations overlap; 3 of 10 gap questions # score above threshold and rely on the # prompt-layer refusal. two layers by design.
Every LLM call writes node, model, and exact token counts to a JSONL ledger, so anvil report prints cost per conversation from counted tokens, not a guess.
A stdio MCP server (anvil-mcp) exposes search_docs and get_github_issue against the same backend; comment drafts stay agent-only because they belong behind the HITL interrupt.
Generation is provider-agnostic through init_chat_model: Anthropic by default, or set ANVIL_ANSWER_MODEL to an openai:, google_genai:, or other provider-qualified model.
Quickstart
The committed corpus snapshot (118 pages of the FastAPI docs at a pinned commit) means no network and no API keys are needed for any of this.
$ docker compose up -d postgres $ uv sync $ uv run anvil ingest --embedder fallback # keyless; --embedder openai with a key $ uv run anvil eval retrieval --embedder fallback $ uv run anvil gate $ uv run pytest # 50 tests # with ANTHROPIC_API_KEY set: $ uv run anvil ask "How do I enable CORS?" $ uv run anvil chat $ uv run anvil eval agent $ uv run anvil report