The agent answers questions about five Canadian federal Acts (PIPEDA, the Privacy Act, the Canada Labour Code, the Copyright Act and the Canadian Human Rights Act). The corpus is public Justice Laws XML parsed into section records (ingest/parse.py, committed as data/corpus.jsonl) and indexed in SQLite FTS5 with bm25 ranking (app/corpus.py). Not legal advice.
POST /v1/research ──► run stored, 202 + run_id (app/main.py, app/runs.py)
│
▼
Claude tool-use loop ── search_statutes ─┐ (app/agent.py, app/tools.py)
│ ── get_section ─────┼──► Corpus (app/corpus.py, SQLite FTS5)
│ ── list_acts ───────┘
▼
submit_answer: claims with {section_id, quote}
│
▼
verifier, no model call ── fail ──► one repair turn (app/verify.py)
│ │
▼ ▼ still failing
gate: pass claim moved to "blocked", gate: blocked
│
▼
usage: tokens, cache, latency, USD per call and per run (app/settings.py prices)
│
▼
GET /v1/research/{id} and /v1/research/{id}/events (SSE trace)
POST /v1/researchstores a run and returns202with arun_id. Sending the sameIdempotency-Keyagain returns the same run.- The model gets three read-only tools that read the same corpus as the REST API, and must answer through a strict
submit_answertool: claims, each with{section_id, quote}citations. app/verify.pychecks every citation without a model: the section exists, the quote is verbatim (after whitespace normalisation), every claim is cited, nothing cited is repealed.- A failing answer gets one repair turn with the findings. A claim that still fails is moved to
blocked, never silently dropped, and the run's gate readsblocked. - Every model call records input, output and cache tokens, latency and USD. The run sums them.
- The same retrieval and verifier are exposed over MCP (
mcp_server.py) for Claude Code or Claude Desktop.