Evidence before confidence.
A team of agents turns live public sources into a cited city brief, and refuses to publish anything it cannot find in a source and have checked by a second agent.
System context
One container serves the API and the UI. Every model call passes through one boundary that handles rate limits and fallbacks. Knowledge lives in hosted stores because free hosting has an ephemeral disk.
The agents
Each agent has one job and hands its output to the next. LangGraph orchestrates them and also runs three plain steps: intake, saving and brief assembly. Three agents are gates: anything they remove is recorded as a gap the user can see.
Decides what to search for
Writes searches for each of the 5 areas. In a second round, only for the areas still missing.
Uses fast model; seed terms if no model
Finds sources
Searches the live web at request time and ranks results by publisher authority. Tavily reaches government, municipal and programme pages; the scholarly APIs reach city studies.
Uses Tavily web search, OpenAlex, Europe PMC, Wikipedia
Asks permission
Reads robots.txt before any page is fetched. A refused site is never read.
Removes disallowed sources → gap
Reads the pages
Fetches permitted HTML, XML and PDF, best sources first, within the chosen depth.
Removes unreadable pages → gap
Pulls out findings
Proposes specific claims, each with a quote. A quote that cannot be found in the page is dropped.
Uses a pool of 3 models
Double-checks each claim
A model that did not write the claim rules it supported, scope-flagged or withheld. Numbers must appear in the quote.
Removes unsupported claims → gap
Decides if it is enough
Needs verified city evidence in 3 of 5 areas. Otherwise sends the Planner back, up to the round limit.
Records empty areas as open questions
Answers questions
Answers from stored evidence only, citing it, or says the evidence is not enough.
Uses vectors, graph, ledger
Models
| Job | Model | Why |
|---|---|---|
| Claim extraction | qwen3.8-27b, gpt-oss-120b, gpt-oss-20b | The bottleneck; rotating 3 models triples the free rate limit. |
| Fact check | Any model except the extractor | Independence is enforced by routing, not by prompt. |
| Planning, answers | gpt-oss-20b | Short structured tasks; fast, with a fallback. |
| Knowledge graph | gpt-oss-120b | Its own budget, so it never slows the brief. |
| Embeddings | bge-small locally, hosted API on Render | Free locally; moved off the 512 MB host. |
Data and retrieval
| Store | Holds | Answers |
|---|---|---|
| SQLite | Runs, sources, crawl decisions, facts, gaps, metrics | “What did the system decide, and why?” |
| Qdrant | Verified evidence, filtered by city | “What evidence is near my question?” |
| Graphiti + Neo4j | Organisations, programmes, policies, places | “Who is connected to what?” |
A question is tied to one city, searched in vectors and graph at the same time, reranked, and answered with numbered citations that resolve to stored records. If a store is down, the ledger still answers.
Resilience
- Rate limit is not an outage. A throttled call moves to the model that is free soonest; all callers share one cooldown.
- Degrade, and say so. Without a model, claims become verbatim sentences and checks become rules. The UI shows a banner and the run counts it.
- Brief first, graph later. The graph builds in the background; the user never waits for it.
Trade-offs
- Free open-weights models: $0 and no lock-in, paid for with rate limits.
- Search APIs, not a scraped search engine: Tavily plus scholarly APIs are sanctioned and reliable, paid for with a credit budget (one Tavily credit per search: 3 for a Quick run, up to 12 for Thorough; 1,000 free a month). News and commercial pages also come back, so authority ranking and the fact checker have to filter them.
- No human review, no OCR, no authentication: the fact-check gate and visible gaps carry the safety.
- SQLite on free hosting: history resets on restart; vectors and graph persist.