NVI

analytics agent — BIRD financial

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 financial as 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 lng project; reached over WireGuard. Same env var works in dev and any future deploy.
  • Postgres dedicated to nvi. Persisted via .data/postgres hostPath 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.