Structured-data “RAG” where the answer is computed, not retrieved. The database schema goes in the prompt, an LLM writes SQL, and a generate → execute → fix loop self-corrects it against a strictly read-only database until it runs and actually answers the question.
SELECT c.name, c.country, SUM(ot.order_total) AS total_spend FROM customers c JOIN order_totals ot ON ot.customer_id = c.customer_id GROUP BY c.customer_id ORDER BY total_spend DESC LIMIT 5;
| name | country | total_spend |
|---|---|---|
| Priya Sharma | India | 18,240.50 |
| Jonas Weber | Germany | 16,905.00 |
| Grace Okoro | United States | 15,110.75 |
| Ken Tanaka | Singapore | 14,388.20 |
| Sofia Rossi | Australia | 13,642.00 |
The Ask view — the live self-correction trajectory (a dry-run error caught and repaired), the generated SQL, and the result table.
Point it at any SQLite database and ask a question. It puts the schema in the prompt, has the model write a query, then runs a guard → dry-run → execute → repair loop until the SQL compiles, runs, and answers — all on a connection that physically cannot write. You get the answer, the exact SQL, and the result table.
The retrieval context is the database itself: annotated CREATE TABLE DDL, foreign keys, low-cardinality column values, and a few sample rows — enough for the model to write correct SQL, not guess it.
The database is the grader. A query that won’t compile, errors, or returns the wrong thing is fed back with the exact error — bounded by max_repairs so it always halts.
A static guard, an mode=ro connection, a deny-by-default authorizer, and a time/row budget. A generation has to pass all four before it touches a byte.
An EXPLAIN step compiles the query — resolving tables, columns and the authorizer — without executing it, catching typos and unknown columns for the price of a parse.
After a query runs, a verifier judges whether the rows actually answer the question. A clear mismatch (wrong entity, aggregate vs. breakdown) triggers one semantic rewrite.
Nodes emit NDJSON events; the UI renders each stage — table selection, every generated query, the guard/dry-run/execute outcome, the verdict — then streams the answer token by token.
Upload a .db/.sqlite file or import a server-side path — it’s copied into a managed directory and only ever opened read-only, so the original is never touched. A retail demo DB ships built-in.
Each conversation is a thread whose memory lives in a LangGraph SqliteSaver checkpointer — so follow-ups like “now break that down by region” resolve against the previous turn.
The Database tab renders every table — columns with types, primary/foreign keys, row counts, and the same enum hints the model sees — so you know what you can ask.
Model, Bedrock connection, and the SQL execution policy — row cap, statement timeout, repair budget — live server-side in SQLite behind a password. The browser can tighten limits but never exceed the server ceilings.
The LLM only ever produces a string. Whether that string is allowed near the data is decided by four independent layers; a hostile or merely confused generation has to get past all of them.
Comments stripped, string literals blanked, then the text must be exactly one statement beginning with SELECT/WITH. Stacked statements and a keyword blocklist (INSERT, UPDATE, DROP, ATTACH, PRAGMA…) are rejected before anything compiles.
Every connection is opened file:…?mode=ro with PRAGMA query_only. SQLite refuses writes at the pager level, so even a bug in layer 1 cannot mutate the file.
SQLite calls back for every action it compiles. Only SELECT/READ/FUNCTION/RECURSIVE are allowed; everything else fails during preparation, before a single row is touched.
A progress handler aborts any query past the statement timeout, and rows are pulled with a hard cap — so a runaway result set is truncated, not materialized.
EXPLAIN dry-run runs layers 1–3 without executing — that’s the agent’s cheap self-check before it commits to a real run.Classic RAG retrieves text. But a huge class of questions — “how many,” “top N,” “total by month” — are answered by computing over structured data. This is the shape of that work: safe, self-correcting, and auditable, because every answer ships with the exact SQL that produced it.
Let non-SQL teammates ask “top 5 customers by spend this quarter” without waiting on the data team — and see the query behind the number.
Dropped into a new database? Ask questions in English and watch the agent discover the tables, joins and column values as it goes.
“How many orders are stuck awaiting fulfilment?” answered live against a read-only replica, with zero risk of an accidental write.
Monthly revenue, breakdowns by region or channel — the same natural-language question re-runs against fresh data and returns a fresh table.
A safe read-only natural-language layer over a product database — the four-layer guard means untrusted questions can’t become dangerous SQL.
A small, dependency-light Python backend and a vanilla-JS frontend with no build step. No vector store, no embeddings — the “index” is the live database schema.
Five nodes: select_tables narrows the schema, generate_sql writes the query, execute guards/dry-runs/runs it, verify checks the result, and answer streams the reply.
START │ ▼ select_tables ──▶ generate_sql ◀───────────────┐ ◀──────────────┐ │ │ repair │ rewrite ▼ │ (SQL error) │ (wrong result) execute ── guard → dry-run → run ──┘ │ │ ok │ ▼ │ verify ── does it answer the question? ───┘ │ yes / budget spent ▼ answer ──▶ END
On a large schema, a planning step picks the smallest set of tables (and join-through tables) the question needs — so the SQL prompt carries focused DDL, not the whole database. Small schemas skip straight to generation.
The annotated DDL — types, keys, low-cardinality column values, sample rows — goes in the prompt, and the model returns one JSON object {sql, explanation}. The enum hints are what stop it guessing 'pending' when the data says 'awaiting_fulfilment'.
The query passes the static guard, then an EXPLAIN dry-run compiles it through the read-only authorizer, then it runs under a row-cap and timeout. Any failure records the exact SQLite error.
If the guard rejects it, the dry-run won’t compile, or execution errors, the loop returns to generate_sql with the error appended as the correction instruction — up to max_repairs times, then it gives up gracefully rather than guessing.
A self-check reads the question, the SQL, and a preview of the rows. A clear mismatch — wrong entity, an aggregate where a breakdown was asked for — triggers one semantic rewrite with the reviewer’s hint; otherwise the result is accepted.
The answer node writes plain English from the result table only, quoting the figures. Nodes call get_stream_writer() to emit tables, sql, guard, dry_run, execute, result, verify, text_delta and usage — forwarded to the browser as NDJSON. Every JSON-returning model call fails open, so a flaky judge can never trap the loop.
max_repairs and one semantic rewrite) plus a graph recursion_limit guarantee the agent always halts.Ships with a pre-seeded app_config.db, so the Bedrock region, key and model carry over — it runs out of the box, and auto-creates a retail demo database on first launch.
# install & launch pip install -r requirements.txt python server.py # → http://127.0.0.1:8400 # then, in the browser: 1. Database → use the built-in demo, or upload / import a .db 2. Ask → “Top 5 customers by total spend, and which country each is in” 3. Settings → model, Bedrock connection & SQL policy (password: bala@143)
/overview — open http://127.0.0.1:8400/overview once the server is running.