A voice-enabled chat agent that answers cited questions about London's 33 boroughs, grounded in real open data, running on NVIDIA Nemotron. Every figure is computed from the data and traced to its source; the agent says so plainly when the data cannot answer.
It assembles a mosaic: it stitches the relevant facts for a borough across several fragmented open datasets - income, housing affordability, deprivation, health - into one answer, and points you to the source for every number.
- Chat (ChatGPT-style) and voice (ElevenLabs, optional) over London open data.
- Two reasoning lanes, the model picks per question:
- Retrieval (RAG + graph) - cited facts, cross-source joins, exact rankings.
- Computation (text-to-SQL via PydanticAI) - exact aggregates (averages, counts, totals).
- Never invents a number - every figure is cited to its source chunk or its SQL query.
- Self-contained and designed to run fully local on an NVIDIA DGX Spark.
git clone https://github.com/Krishna-kai/AskLondon
cd AskLondon
./bootstrap.sh # venv + deps + index, one command
# add your NVIDIA_API_KEY to .env, then:
.venv/bin/python -m streamlit run chat_app.py # http://localhost:8501bootstrap.sh works offline too (builds a deterministic index with no key). Acceptance check:
EMBED_MODE=hash LLM_MODE=test .venv/bin/python tests.py.
Built for NVIDIA. The agent runs on NVIDIA Nemotron, served via NIM, with NVIDIA NeMo Retriever for the embeddings:
| Run target | Set in .env |
What you get |
|---|---|---|
| Cloud NIM (laptop + NVIDIA API key) | NVIDIA_API_KEY=nvapi-... |
the full two-lane agent on Nemotron (validated 2026-06-07) |
| NVIDIA DGX Spark (sovereign, fully local) | nim_stack.sh up, NIM_BASE_URL=http://localhost:8000/v1 |
Nemotron LLM + NeMo Retriever + rerank + safety, co-resident on the box |
On the Spark the whole Nemotron stack stays in the room; only the optional ElevenLabs voice leaves it. Full bring-up: DEPLOY_SPARK.md.
git clone https://github.com/Krishna-kai/AskLondon && cd AskLondon
./bootstrap.sh # venv + deps + index
# put NVIDIA_API_KEY=nvapi-... in .env (free key at build.nvidia.com), then:
.venv/bin/python -m streamlit run chat_app.py # http://localhost:8501Data resilience: the bundled health data always loads, and the economy sources fall back to a bundled
cache (data/economy_cache.json) if the London Datastore is down, so a demo never loses sources.
Portability beyond NVIDIA is a footnote, not the headline.1
The guards make a weaker local model fail safe, not wrong: if it cannot tool-call the SQL lane cleanly, the anti-fabrication guard returns "compute unavailable" instead of an invented number.
From your laptop, with the project cloned:
ssh-copy-id nvidia@my-spark.local # one-time passwordless SSH (use your box's host id)
./push_to_spark.sh my-spark # rsync the project + bootstrap on the boxThen on the box: ./serve_model.sh (Nemotron on :8000) and ./deploy_spark.sh (ingest + app on
:8501), with NIM_BASE_URL=http://localhost:8000/v1. Details: DEPLOY_SPARK.md.
- Income - HMRC taxpayer income by borough (London Datastore)
- Housing affordability - house-price-to-earnings ratio (London Datastore)
- Deprivation - Indices of Deprivation 2019 (London Datastore)
- Health - life expectancy at birth (OHID Fingertips)
The economy datasets are fetched live from the London Datastore at ingest; the Fingertips
extract is bundled in data/. New sources plug in as small adapters (see connector.py /
economy.py).
chat_app.py ─► agent.py ─► retrieval.py (vector + graph + rank) ─┐
└► sql_lane.py (PydanticAI text-to-SQL) ─┴─► Nemotron ─► cited answer
data: connector.py + economy.py ─► store.py + graphstore.py + sql_db.py
Full write-up in ARCHITECTURE.md. Local deployment on a DGX Spark: DEPLOY_SPARK.md. Versions: VERSION.md.
Two layers, both offline (no key, no GPU):
EMBED_MODE=hash STORE_MODE=local python tests.py # 22 checks, all offline- Plumbing - connector to embeddings to store + graph to retrieval to cited answer.
- Guarantees (pinned so a refactor cannot silently break them): read-only SQL safety (rejects
writes/DDL, including stacked
SELECT 1; DROP ...), never-invent (no figure unless a real SELECT ran), exact ranking (the graph sorts real values), exact compute (DuckDB aggregates).
doctor.py healthchecks the index + model + embed endpoints; deploy_spark.sh runs tests.py on
the box before the app goes live. Validated end to end on cloud NIM (2026-06-07): both lanes cited and
correct, 22/22 in NIM mode.
The same code runs on a laptop (cloud NIM) and on a Spark (local Nemotron) by env only:
EMBED_MODE (hash | nim), STORE_MODE (local | milvus), LLM_MODE (test | nim),
NIM_BASE_URL, EMBED_BASE_URL, NIM_MODEL, SQL_NIM_MODEL. See .env.example.
SQL_NIM_MODEL gives the SQL lane its own model knob - keep it on nvidia-nemotron-nano-9b-v2
even if NIM_MODEL is set to a bigger model; larger reasoning models can malform SQL tool-call
JSON. Validated model for both lanes: Nano-9B-v2 (cloud NIM, re-confirmed 2026-06-07).
MIT - see LICENSE.
Footnotes
-
NVIDIA is the target, not a lock-in. Because every inference call is OpenAI-compatible and routed through
config.py, the same code also runs (a) offline with no key, in a deterministic test mode (used by the acceptance tests / CI), and (b) against a non-NVIDIA local endpoint such as Ollama, LM Studio, or MLX (Mac / Windows / Linux) by settingLLM_MODE=nim,NIM_BASE_URL,NIM_MODEL,SQL_NIM_MODEL, andEMBED_MODE=hash. Both paths are validated, but they are a portability fallback - the project is built for NVIDIA Nemotron on the DGX Spark. ↩