Overview
A text-to-SQL analytics agent over a real warehouse — structured so the agent layer is legible, not buried in a framework.
nvi answers business questions over the BIRD financial
warehouse (PKDD'99 Czech bank: accounts, transactions, loans, cards,
clients, districts, orders, dispositions). It does this by composing
a small set of Analyses — each its own mini-agent — orchestrated
through a langgraph wiring layer with full Langfuse observability.
The shape mirrors the JD's three building blocks (text-to-SQL, reasoning agents, planners) as three distinct layers rather than one omnibus prompt. That gives every part a name and a tight contract, and lets each Analysis pick the simplest reasoning pattern (CoT, ReAct, plan-and-execute) for what it does instead of forcing one over everything.
Architecture
Three layers — Plan composes Analyses; Analyses are mini-agents; Tools are deterministic.
- Plan (L3)
- One LLM call that turns a question into an ordered set of Analyses with intended interpretation. Not agentic by itself — produces a structured artifact the runtime executes.
- Analyses (L2)
- Each Analysis is a self-contained mini-agent. It picks the simplest reasoning pattern it needs — CoT for one-shot interpretive moves, ReAct for ones that iterate over Tool results, plan-and-execute for compound ones. The Analysis owns its loop.
- Tools (L1)
- Deterministic primitives —
text_to_sql,execute_sql,retrieve_schema,retrieve_metric_definition,python_sandbox. No LLM reasoning inside. Called from inside Analyses.
Plan layer
L3 — one LLM call that decides which Analyses to run.
The planner gets the user question, the metric catalog, a schema overview, and the catalog of Analyses. It returns a structured plan: an ordered list of Analysis invocations with arguments and an intended interpretation. It does not call tools or query data itself — it's a composition step.
To be expanded as the planner is built out.
Analyses layer
L2 — each Analysis is its own mini-agent.
Analyses are the agent layer. Each one wraps a small piece of analytical
intent (compare two periods, drill down by dimension,
find outliers) and decides how to satisfy it — sometimes one
LLM call is enough, sometimes a ReAct loop over Tool results is the
right fit. The Analysis base class only enforces a uniform external
contract (run(state) → Finding); the internal pattern is
a per-Analysis decision.
Per-Analysis pattern table coming as each Analysis is implemented.
Tools layer
L1 — deterministic primitives.
Tools have no LLM reasoning inside them. text_to_sql is
the one exception that uses an LLM, but tightly: schema-RAG context,
sqlglot validation, retry on parse/runtime errors. execute_sql
runs against Postgres read-only with a row cap and timeout.
retrieve_schema and retrieve_metric_definition
power the semantic layer. python_sandbox handles stats
work that's awkward in SQL.
Per-Tool documentation coming as each Tool is implemented.
Runtime
langgraph wires the graph; nvi's domain code stays legible on its own.
The orchestration is built on langgraph — it's the right tool for the graph wiring, parallel execution, and streaming state we need, and reaching for a custom DAG runner here would be reinventing the wheel.
The intent is the opposite, though: nvi's domain code (Plan, Analyses, Tools) presents a clean surface that's readable on its own terms. Understanding what nvi does should not require first learning langgraph's API. The framework wires nodes; nvi's code defines what the nodes are, and that's where the interesting reading is.
Concretely: api/runtime/ contains the langgraph builder
and event tap. api/plan/, api/analyses/, and
api/tools/ contain the domain code — and those files
don't expose langgraph types in their public interfaces.
Run it locally
Prerequisites: lng Langfuse cluster reachable, soleprint-ui framework propagated by spr, .env with Anthropic + Langfuse keys.
make install # uv sync
make kind # create kind-nvi cluster
make tilt-up # bring up postgres, api, ui, docs, gateway
make seed # load BIRD financial into postgres
open http://nvi.local.ar
*.local.ar resolves via dnsmasq; the system Caddy at
~/wdir/ppl/local/Caddyfile proxies nvi.local.ar
to the kind NodePort. Reload after first install: sudo systemctl reload caddy.
Architecture decisions
- Three layers, not one omnibus prompt. Plan composes Analyses; each Analysis picks its own reasoning pattern (CoT / ReAct / plan-and-execute); Tools are deterministic. Borrowing elements from agent frameworks without committing the whole system to one.
- langgraph for orchestration, nvi for domain. Framework owns graph wiring and streaming; nvi's
plan/,analyses/,tools/own the interesting code and are readable without reading langgraph. - BIRD
financialas the warehouse. Fits Nivii's stated Finance vertical; ships with golden SQL pairs so the eval set is free and verifiable. - Langfuse hosted independently by the
lngproject; reached over WireGuard. Same env var works in dev and any future deploy. - Postgres dedicated to nvi. Persisted via
.data/postgreshostPath mount so the BIRD load survives cluster restarts.
Evals
BIRD financial dev set, run by evals/run_evals.py, results pushed to Langfuse tagged eval.
To be populated as the agent stack lands.