Demo Data (CLI-first)
The demo is three SQLite databases, registered as three datasources.
Chinook, a small digital music
store, leads; it is vendored in the wheel (MIT, v1.4.5, 1.02 MB). Beside it are
support, a help-desk ticketing system (about 232 KiB), and webanalytics,
website traffic (about 284 KiB). Both hold synthetic data generated by this
repository from a fixed seed
(scripts/generate_demo_datasets.py),
so a regeneration reproduces exactly what is committed and no third-party data
is involved in either. All three are shipped in the wheel and copied into your
project by the CLI, so no data is downloaded or built when you set the demo up.
The tables, columns and row counts are in
Demo datasets.
To see it before installing anything, the same playground runs at https://nl2sql.codewithnk.com, where you bring your own model key.
What to expect
After pip install "nl2sql-engine[demo]" and nl2sql demo:
- No questions at the terminal. The command does not prompt. It looks for
a model key --
--api-key, thenOPENAI_API_KEY,OPENROUTER_API_KEYorANTHROPIC_API_KEY, then a key saved in the project's.env, then an Ollama daemon onlocalhost:11434-- and says which mode it chose: Live mode with the provider, or Replay mode when it finds none. - A project folder and an index. The first time, it writes
./nl2sql-demo(--dirchanges it) withnl2sql init --demo, which copies in the three databases above; every start then builds the engine, callsengine.check(), and indexes the schemas on your machine when the index is missing, empty or out of date. The first run downloads a ~79 MB embedding model; indexing needs no key. - A local page. It prints
Playground ready at http://127.0.0.1:8765/and opens that address in your browser (--no-browserskips it,--portchanges it). Ctrl+C stops it. - No key yet? Paste an OpenAI or Anthropic key (or an OpenRouter one) into the Settings page; the running demo switches to live without a restart.
- Ask. Pick one of twenty guided questions or type your own, as
admin,analystorviewer. A run shows the answer first, then the plan the model wrote, the checks, the generated SQL, the rows, and cost and time. Debug opens each step's model calls, prompts and responses, and the Pipeline page lists every step and which a model decides.
The demo command and the [demo] extra arrive on PyPI with 0.2.0. If pip
says nl2sql-engine has no demo extra, you have an earlier release: install
from a clone with pip install -e packages/sdk -e "packages/nl2sql[demo]".
1. Install the CLI
# Install from PyPI. Add the `demo` extra if you also want the browser
# playground that `nl2sql demo` serves; `[all]` covers drivers only.
pip install nl2sql-engine
pip install "nl2sql-engine[demo]"
# Or install from source (dev)
pip install -e "packages/nl2sql[all,demo]"
2. Write the demo project
init --demo writes a complete config, relative to the folder you name:
nl2sql.yaml # the table of contents
datasources/
chinook.yaml # each with its description, read-only connection,
support.yaml # 10,000-row limit and sample questions
webanalytics.yaml
llm.yaml # the provider and model; no key
embeddings.yaml # provider: local
policies.yaml # the admin, analyst and viewer roles below
storage.yaml # path: .nl2sql (index, state.db, traces, results)
curated.yaml # six verified queries on Chinook
settings.yaml # a 300 s timeout and a trace for every run
data/ # chinook.sqlite, support.sqlite, webanalytics.sqlite
.gitignore # .nl2sql/ and .env
It is the same config nl2sql.config.demo_config() (and NL2SQL.demo())
builds in code. nl2sql index then builds the index, and exits 1 if any
datasource fails. See
Start a config with nl2sql init.
For the browser playground instead of the CLI, nl2sql demo does both steps in
./nl2sql-demo the first time (and adds a nl2sql-demo.json stamp recording
which engine version wrote the folder; see Old demo folders),
then serves a page over it. See the
README for its flags, and
the Settings page for entering a key and choosing
models from the page.
The schemas
Chinook is eleven tables with real declared foreign keys: Artist, Album,
Track, Genre, MediaType, Playlist, PlaylistTrack, Customer,
Employee, Invoice and InvoiceLine. support adds four - agents,
customers, tickets and ticket_messages - and webanalytics five -
devices, referrers, sessions, page_views and conversions. All three
declare their foreign keys, which matters: LogicalValidatorNode checks every
join in a plan against a declared relationship, so a dataset without them cannot
answer a join question at all.
The three share their people. Both generated databases carry a customer_email
column and a customer_id matching Chinook's Customer.CustomerId, so the same
59 customers appear in all three, and every timestamp in them falls inside
Chinook's invoice window, 2009-01-01 to 2013-12-22. That does not make a
question spanning two databases answerable: the engine plans each sub-query
against a single datasource, so it cannot join across them. The shared columns
are groundwork for when it can.
Three registered datasources also change how a question is routed. With one, the
resolver took the only datasource and skipped the vector search; with three, the
datasource search and the LLM answerability judge run on every question, so the
demo exercises the part of the pipeline a single-dataset demo never reached.
Indexing all three writes 186 entries - 111 for chinook (24 of them the verified queries' phrasings and descriptions), 36 for support and
39 for webanalytics - and nl2sql index takes about
seven or eight seconds once the embedding model is cached; the first run on a
machine still downloads the model.
Verified answers
curated.yaml holds six verified queries on Chinook: SQL written
and reviewed in advance, with typed params, and the questions each answers
(top customers by revenue in a year, revenue by year, monthly revenue in a
year, sales by country, best-selling genres, top artists by tracks sold).
nl2sql.yaml includes it as the curated section, so curated answers are
on, and indexing embeds their questions
beside the schema. A question one of them answers runs its SQL with the values
bound instead of a generated plan, and the playground labels the answer
Verified · and the query's id. "What is the total revenue per year?" and
"Which genre sells the most tracks?" among the guided questions are two of
them. See Curated answers.
The keyless replay includes the matcher's recorded choices, so the badge appears without a key too, on the guided questions a verified query answers.
The roles
policies.yaml is written with three roles, spanning all three
datasources, so the policy check is visible without editing anything:
| Role | Patterns | May read |
|---|---|---|
admin |
allow: ["*"] |
every table in every demo database |
analyst |
allow: [chinook, support, webanalytics], deny: [chinook.main.Employee, support.main.agents] |
everything except employee and support-agent records |
viewer |
allow: the seven Chinook catalog tables and webanalytics.main.devices, webanalytics.main.page_views, webanalytics.main.referrers |
no customer, invoice or ticket-message data, and no access to support at all |
Ask the viewer role about customers and no model ever sees the Customer
or Invoice tables: they are hidden from the planner, so the answer is that
the connected data cannot answer the question. A plan that names a hidden
table anyway is refused by the logical validator with a SECURITY_VIOLATION
before any SQL is generated. policies.yaml sets refusal_names_tables:
true, so the demo's refusal names the role and table; outside the demo the
message is the generic "You do not have permission to see the data this
question requires." See
Security Model.
The playground's schema panel follows the Ask as role too: it lists only
the tables that role may read (engine.datasources.schema(..., user_context=)).
API keys in the demo
embeddings.yaml says provider: local, so schema chunks are
embedded with the key-free ONNX all-MiniLM-L6-v2 model bundled with chromadb
instead of the OpenAI embeddings API. The first indexing run downloads roughly
79 MB of model files into a local cache, which can take a few minutes.
This covers the embedding step only. The demo is not key-free end to end:
nl2sql run "..."calls a chat model, so it needs a working LLM key (OPENAI_API_KEY, or an OpenRouter key withllm.yamlpointed atprovider: openrouter). Keep it in the environment, or in a.envfile next tonl2sql.yaml, which the CLI loads without overriding a variable already set.- The demo's model is
gpt-5.4withtemperature: 0.0. It replacedgpt-4o, whose 30,000 tokens-per-minute limit on the owner's account was below one question with a single retry;gpt-5.4had 500,000 and acceptstemperature: 0. To use a model that rejects it (gpt-5.5,gpt-5-mini), settemperature: nullinllm.yaml- see LLM configuration → Temperature. With an OpenRouter key or Ollama, changemodelto one that provider serves: live mode switches the provider, not the model name. nl2sql index --enrichalso runs an optional LLM enrichment pass over the schema; without--enrichno model is called. Enrichment only fills in descriptions the database does not have, and is best-effort: without a usable chat key it is skipped with anINFOline naming the agent, the provider and the variable to set, and the chunks are still indexed - just with no LLM-generated descriptions. An enrichment failure never fails indexing. The demo's own indexing (the first run, the startup repair and the playground's Rebuild) leaves enrichment off, because it spends tokens on your key; the playground offers it as an explicit checkbox. When it is on, the demo's indexing uses a key exported in your shell, or one saved in the project's.env.nl2sql demo --api-key <key>takes the key on the command line and writes it into the project's.env, never into a YAML file. The provider follows the key's shape: a key beginningsk-or-is OpenRouter and is stored asOPENROUTER_API_KEYwithprovider: openrouter; a key beginningsk-ant-is Anthropic and is stored asANTHROPIC_API_KEYwithprovider: anthropic, modelclaude-opus-5andtemperature: null(installnl2sql-engine[anthropic]); anything else is OpenAI.llm.yamlnames no key: the running engine is given it in memory, ahead of any key exported for the same provider, and later runs read it from.env. The process environment is never written..envis covered by.gitignore, but a key passed on the command line is visible in shell history and tops, so the environment variable remains the more private route.- So
nl2sql init --demoandnl2sql indexcomplete with no chat key at all, andnl2sql indexexits0. It exits1if any datasource actually fails to index, which is safe to rely on in a script. A missing key surfaces when a chat model is first called (nl2sql doctornames it before that). - Pointing
llm.yamlatprovider: ollamaremoves the chat key requirement, but a local model's ability to satisfy the pipeline's structured output is model-dependent - see LLM configuration → Ollama.
Sharing the demo with other people is the one case where none of this applies:
nl2sql demo --hosted serves the playground with no key on the server at
all, and each visitor pastes their own into the page; a visitor without one
can replay the guided questions from recorded runs. It refuses to start when
OPENAI_API_KEY, OPENROUTER_API_KEY or ANTHROPIC_API_KEY is set. See
Hosted demo.
Each project keeps its index under its own storage.path (the demo's is
nl2sql-demo/.nl2sql/index), with one namespace per embedding model. If you
change the embeddings section of an existing project, run nl2sql index:
the new model's namespace starts empty, and the old one stays until
nl2sql index prune.
Column statistics are read when a datasource is indexed, so a change to what
indexing records reaches an existing demo folder only when it is indexed again.
After 0.2.2, a text column with at most 50 distinct values keeps all of them
rather than five. nl2sql demo does not re-index a folder whose index is
healthy, so run nl2sql index in the folder, or start a fresh one with
nl2sql demo --dir <new folder>. The hosted image builds its index with
nl2sql init --demo and nl2sql index, so a new image picks the change up.
Index health and repair
The playground's schema panel reads the schema snapshot, for whichever database
its Showing switcher names; the resolver, which
picks the database for every question when more than one is registered, reads
the separate vector index, and with three registered it searches on every
question. After a run the answer header names the database it picked. The two
can disagree: a folder once had its snapshot intact and 0 entries in its index,
so the page looked fine while every question failed with
SCHEMA_RETRIEVAL_FAILED. Three things now guard against that:
- Startup repair. Every
nl2sql demostart callsengine.check(), which reports what the index contains: the demo rebuilds it when it is missing or empty, or a datasource has no live build. A failed rebuild is reported (the playground still starts, and offers Rebuild) rather than ignored. - Rebuilds never leave an empty index. A datasource's new entries are written beside its current ones and switched in only when all are written; a failure keeps the previous entries answering questions. See Indexing.
- Actionable errors. With an empty index the resolver says
"The vector index is empty ... Re-index with
nl2sql index", andnl2sql doctorprints the engine's check, the index status included.
The search index panel
The playground's left rail opens with the Database panel, and with the demo's three databases it carries a switcher for which of them to read: a segmented control while there are four or fewer, a Showing select beyond that. At the foot of the rail, Search index is one line when all is well -- "Search index fresh. 186 entries, built 3 minutes ago." -- that opens to entries by type (datasource, tables, columns, relationships), the schema version they were built from, and when. One index holds every database, so with the demo's three the line inside says which they are -- "Covers chinook, support and webanalytics". With a single database both panels name it instead, and there is no switcher. When the index is empty, missing or out of date, or a rebuild is running or failed, the panel opens itself, a warning under the top bar says what that means for a question, and the panel's Rebuild the index button becomes the rail's one filled button. Rebuild is also always available on demand.
- Rebuild re-reads one database's schema into a new snapshot and rebuilds its
entries beside the current ones; questions keep using the current entries
until the new ones are complete, and a question in flight finishes on the
entries it started with. That database is the demo's own, named in the button's help;
nl2sql indexrebuilds all three. - It shows each step as it runs. The first run downloads the 79 MB embedding model, so it can take a few minutes.
- Write descriptions with the LLM is off by default because it spends tokens on your key; it is unavailable in replay mode.
- It has the Settings page's guardrails (so does the
Retrieval inspector): local only unless
--allow-settings, and only from the playground page itself. - On the hosted demo there is no button at all. The panel says instead that the index was built before the demo started and which databases it covers, and prints the server's own reason where the button would be: rebuilding writes to disk, and the sample data never changes.
Old demo folders
Demo folders are not upgraded in place: a folder written by an older engine
keeps that engine's defaults (for example an older default model). A folder
written by a 0.4 engine (configs/, .env.demo) has no nl2sql.yaml and
cannot be read by 0.5: start a new --dir. New folders get a nl2sql-demo.json stamp; when nl2sql demo
starts in a folder stamped by an older engine, or in one with no stamp at all,
it warns and suggests a fresh --dir. The playground's index panel repeats the
warning.
Pages
The playground is five pages, named across the top of every one of them:
Home, Ask, Pipeline, Settings and Retrieval. Each is a
route in the address bar -- #/, #/ask, #/pipeline, #/settings and
#/retrieval -- so a reload stays where you were, a
link can be pasted to someone else, and Back and Forward walk the pages you
visited. A route nobody serves lands on Home.
Home says what the engine does and shows it: the five steps a question travels (Question, Plan, Check, SQL, Rows, marked by whether a model or code decides each), then a short clip of Ask, Pipeline and Retrieval with a link into each. Try a sample question opens Ask and asks a guided question; on the hosted demo it picks one with a recorded answer, so it works before you add a key. The nav marks the page you are on, and marks Settings or Retrieval when the server has it off; the page itself then says why. Moving between pages keeps the question, the answer and the Debug choice: they belong to the visit, not to the page.
At the right of the top bar a status pill says which mode the page is in -- Live, Replay mode with how many guided questions it has recordings for, or on the hosted demo whether this tab has a key yet. Hover it, or let a screen reader read it, for the full sentence: how replay answers, or whose key answers and the hosted limits.
The Ask page
Ask is the question box, then the run, answer first:
- Suggestions. Under the question box, a row of chips offers three or four guided questions for the database the rail is showing, and N more opens every pile, one per database. On a phone the row is a strip you swipe sideways. The run sits right below the box.
- The answer leads. Once a run is back, its first block is the answer: the
question and the role it was asked as, the answer sentence in the largest
type on the page, and a strip such as
10 rows / 3 of 3 checks / 2 plans / 5.04 s / $0.0187. Each item in the strip moves the keyboard to its station below. A refusal gets the same block: Refused at the checks. No SQL was written. - The stations. Plan, Checks, SQL, Rows and Cost & time follow, each with a small heading, a tag for who decides it (Model · gpt-5.4, from the run's usage, or Code) and the step's time. On the spine a round mark is a model step and a square one is code; filled with a tick is done, an outline is waiting and a dashed outline was not reached.
- Every sub-query. When a question is split into several sub-queries, the Plan and SQL stations show each one under its own head (Sub-query 1 of 2, its intent and its database).
- Rows, at any size. The Rows station reads the run's saved results, not the sample the answer model saw. Its head gives the whole result's row count, and a result cut at its row ceiling says so: Cut at 1,000 rows, so some are missing. Narrow the question to get them all. The table shows 100 rows at a time, fetched from the server, with Previous, Next and Rows 101–200 of 3,340. A run with several results gets one tab per result. Download CSV streams the whole result from the server, every row.
- The gate. The Checks station shows the plans the checks saw (Plan 1 refused, then Plan 2 passed when the first was sent back), why, in one sentence, and each check as a tile with a tick or a cross.
- While it runs. The run streams (
POST /api/ask/stream, Server-Sent Events): each station lights as its step finishes, with its time, and the next one goes live; the rest wait, faded. The Rows station loads its first page as soon as the result is saved, before the answer is written. The answer sentence then appears as the model writes it, a shade lighter until the run is back, when the plan, the checks and the SQL fill in. A recorded run on the hosted demo streams its recorded answer the same way, a word at a time. Where the stream cannot be used (an older server, a proxy that cuts it, a browser that cannot read a streamed reply), the page asksPOST /api/askinstead and the first station stays lit until the whole run is back. A counter beside the button reads Running · 3.4 s, and after a moment Ask becomes Stop, which cancels the request: the run then says Stopped and every station is marked not reached. The server notices the dropped request within a quarter of a second and cancels the run: no further step starts and no further model call is sent, though the one already in flight finishes (and is billed by the provider). On the hosted demo a stopped question still counts toward both limits, because it is charged when it arrives, before it runs. - When it fails. A provider's refusal is said in words with the next step, for example OpenAI rejected this key. with Replace the key, which opens Settings. The provider's own reply is folded under Provider response.
- On a phone, once a run is on the page, a bar at the bottom, Ask another question, takes you back to the question box. It hides while the box is in view.
The Pipeline page
Pipeline lists every step a question passes through, in the order a run takes them, and marks which of them a model decides. Five do: the answerability check, the question splitter, the query planner, the plan repair and the answer writer. The other eight are ordinary code -- the layer router, the SQL agent, the schema search, the plan checks, the retry bookkeeping, the SQL writer, the executor and the result combiner -- which is the point: the model plans, and deterministic code writes the SQL, checks it against the real schema and this role's policy, and runs it.
The steps are grouped into three phases: Understand the question, Answer each sub-query (the SQL agent and its own steps, in a bracket, because they repeat per sub-query and again when the checks refuse a plan) and Combine and explain. A step a model decides is a raised row with a round mark and its model in a chip; a code step is a compact row with a square mark. A legend reads: asks a model, code, did not run, skipped.
Each step gives its name in plain words, the graph node name the Debug ledger uses, and one sentence on what it decides. Before any question, each model step names the model it is set to use, so the page is worth reading on arrival. Without a key, where the guided questions replay recordings, it names instead the model the recordings were made with, tagged recorded. After a question, each row gets a time bar and its time, so the page doubles as the run's waterfall, and a model step names the model that answered it and its input, cached and output tokens. The timings record how long each step took, not when it started, so the bars are laid end to end in pipeline order, and a step that ran more than once is drawn for its longest run. A step passed over while a later one ran (the plan repair, when no retry was needed) is marked skipped; a step the run never reached is marked did not run.
The list is not written by hand on the page: it comes from GET /api/pipeline,
which reads it from the graph's own node names, and a test fails when a node is
added, renamed or removed and the description does not follow. The per-node
drill-down under Debug on the Ask page is unchanged; this page is the
overview. The long version is in
Pipeline architecture.
The Settings page
The playground that nl2sql demo serves has a Settings page in the header
nav. It edits the same two files the CLI reads, and nothing else: there is no
second settings store, and the browser keeps nothing but UI conveniences.
- API keys, one per provider. Paste a key and press Save key. The
provider follows the key's shape by the same rule as
--api-key(sk-ant-is Anthropic,sk-or-is OpenRouter, anything else OpenAI). The key is written to the demo project's.env, replacing that provider's previous key and keeping the others, and the default moves to that provider. The running demo switches from replay to live without a restart: questions already running finish on the model client they started with, then the engine's LLM clients are rebuilt from the project'snl2sql.yaml(itsllm.yaml), with the saved key given to them in memory rather than through the environment. Keys are write-only: no response, log line, trace or error carries one, only a masked form such assk-...4f2aper provider. On a later start the precedence above still holds, so--api-keyor a key exported in your shell wins over a saved one, and every saved key is loaded so each step finds its own. -
A provider and a model for each LLM step. One selector each for the answerability check (
datasource_resolver), question splitter (decomposer), query planner (ast_planner), plan repair (refiner) and answer writer (answer_synthesizer). Each has a "Default" option that uses the top-level provider and model, then one group of models per provider. A provider with no saved key is listed but cannot be chosen, and a step already on a provider whose key is gone says so under its selector. A choice is written tollm.yamlunderagents:with itsprovider,modeland temperature. It takes the top level's endpoint and key when it is on the same provider, and names no key otherwise, so its own provider's variable is read. It takes effect on the next question, and a step put on another provider stays there when the default moves. The list is short on purpose. On 2026-09-20 each OpenAI model was sent the engine's parameters of the time (temperature=0,seed=42, strictjson_schema) and worked with them, two of them only once the temperature was left out. The engine now asks every model for a tool call instead, as it already did for every node but the planner. That says the model accepts the engine's calls, not how well it plans, which is the evaluation's question:Model Written with gpt-5.4(the default),gpt-5.4-mini,gpt-4.1,gpt-4.1-mini,gpt-4otemperature: 0.0gpt-5.5,gpt-5-minitemperature: null: they reject temperature 0, so a step on one runs at the model's default temperature and its answers vary more from run to runClaude's list was not probed on a real account; its temperatures follow Anthropic's documented rules:
Model Written with claude-opus-5(the default),claude-sonnet-5temperature: null: both reject any temperatureclaude-haiku-4-5temperature: 0.0The list lives in one place,
VERIFIED_MODELSinnl2sql/llm/providers.py. It covers OpenAI and Anthropic for now: with an OpenRouter key or Ollama as the default the panel says the default has no model list, and offers only the other providers whose keys are saved. - Local only by default. Settings work only when the playground is bound to a loopback address (127.0.0.1,localhost,::1). On0.0.0.0or any other address the panel says why it is off and the settings routes answer403, because the playground has no login: a stranger who can reach it could swap in their own key, or spend on yours.nl2sql demo --allow-settingsturns it on anyway, for a network you trust. Changes are also refused from another site's page (a foreignOrigin), through a hostname that is not a loopback name, or in anything but JSON. - Hosted mode is a third state. Withnl2sql demo --hostedthe page shows a key form and a Model for each step section that write only to this browser tab'ssessionStorage; the settings routes stay refused because there is nothing on the server to save. Choosing a model needs nothing saved, so hosted mode keeps it: one key per provider and the model each step runs on travel with each question, in a header each. The Ask page there opens with the key form itself: pick a provider, paste the key and Use this key, under three facts about it -- Stored in this browser tab only, Sent with each question in a request header, Never written to disk, logs or traces. Nothing is held closed without a key: the guided questions the server has recordings for are marked and answer from a recorded run, labelled Recorded run, and any other question answers with a button to Settings to add a key. With recordings the key form is folded under Use your own key. Locally there is nothing to announce, and nothing is shown. See Hosted demo.
The Retrieval inspector
Retrieval is the third page in the header nav. It embeds any text with the local model and runs the engine's own vector search against the live index: the pool of nearest entries with their similarity, the entries MMR picks in order, the score each won with, and what was dropped. There is no re-ranking model; the "re-ranking" is MMR, explained in Indexing.
- Knobs. Picks (
k, the pool is4 * k), lambda (the engine uses 0.7; 1 is plain nearest-neighbour order, 0 favours entries unlike earlier picks), which entry types to include, and a datasource filter. After the first search, each change searches again. Each entry shows the text that was embedded for it. - Reading it. The picks in order, and the entries MMR passed over, sit above the pool as rows of chips. Each similarity has a bar placed within the pool's own range, nearest full and farthest empty: raw cosine scores bunch together, so this is what shows which entries are actually nearer.
- Free. The embedder is local; nothing here calls the LLM.
- For chunking experiments. Change the chunking, press Rebuild, search the same text, and diff the Copy as text output of the two runs: one fixed-width line per entry (rank, pick or drop, similarity, MMR score, type, name).
- Local only, like Settings and Rebuild, because it shows every index entry,
column statistics and sample values included, with no role filter: on a
non-loopback
--hostit is off (POST /api/retrievalanswers403) unless--allow-settingsis given, and it refuses another site's page, a non-loopback hostname and anything but JSON.
A run's own retrieval is under Debug: open the datasource_resolver or
schema_retriever row in the per-node table.
Rating answers
Below Cost & time, each answer asks Was this answer right?: 👠Right
or 👎 Wrong, with an optional note (Wrong number, Wrong table,
Wrong filter, Missing rows, or your own words). The rating is saved in
the demo's state store (.nl2sql/state.db) with the question, role, SQL, the model per
step and the engine version; never the rows. It is local only, like Settings:
on a non-loopback --host it is off unless --allow-settings.
settings.feedback.enabled: false in the project's config turns it off. Read the ratings back, with
the refusal, retry, error and plan-cache rates, from the CLI (below). See
Feedback and Signals.
3. Use the demo from the CLI
Every command reads one config: --config PATH, else NL2SQL_CONFIG, else
./nl2sql.yaml. Paths inside it are relative to nl2sql.yaml, so from the demo
folder (cd nl2sql-demo) no flag is needed; from anywhere else, add
--config nl2sql-demo/nl2sql.yaml before the command.
# Run a query against the demo
nl2sql run "How many customers do we have, by country?"
# Ask as a role that is not allowed the answer
nl2sql run --role viewer "Who are the top 5 customers by total spend?"
# Plan and validate without touching the database
nl2sql run --no-exec "Which artist has the most albums?"
# Pin one datasource instead of letting the resolver pick
nl2sql run --ds-id support "How many support tickets are still open, by priority?"
# Re-index after editing the demo's config
nl2sql index
# Re-index one datasource only; the others' entries are left untouched
nl2sql index --datasource chinook
# After changing the embedding model: drop the old model's index
nl2sql index prune
# Check the install, the keys, and the engine: each datasource, each key, the index
nl2sql doctor
The two flags that name a datasource are not the same flag: run takes
--ds-id, which pins the datasource and makes the resolver skip its search;
index takes --datasource (-d), which chooses what to re-index.
The demo's settings.yaml sets traces: { mode: always }, so every run also
writes a trace to .nl2sql/traces/ and prints its path. Inspect one with
nl2sql trace show <file or trace id>, or re-run it on the recorded model
answers with nl2sql trace replay <file>. See
Debugging a Run.
Asking a question a second time on the same day reuses the first run's validated plan from the
plan cache (in .nl2sql/state.db), so the SQL and rows are the same and the
planner is not called; the playground's Debug view says "Plan from the plan
cache". The plan is still validated for the role you ask as. To start fresh:
The playground's ratings, and the guardrail rates over every rated run and kept trace:
nl2sql feedback list # the ratings, newest first
nl2sql feedback stats # thumbs up/down, refusals, retries, errors, plan cache
nl2sql feedback export --good # thumbs-up runs as draft eval cases, for review
nl2sql feedback clear # delete every rating
export --good writes feedback_gold_drafts.yaml, a suite of draft cases. A
person must review each draft, approve it and run engine.evals.regenerate
before it runs; see
Feedback and Signals.
nl2sql demo --record turns the cache off while recording
(settings.pipeline.plan_cache is off in the engine it builds; the files are
not changed), so every planner
answer is captured. It also stops with exit 1, recording nothing, when the
demo's indexing fails: a plain nl2sql demo keeps going and offers Rebuild,
but answers recorded against no index would be the wrong ones. Only a
question whose run succeeded (rows, an answer, no error) keeps its recordings;
a failed run's are dropped, the question is listed as not recorded, and the
command exits 1 after writing recordings.json with the rest. The file names
the provider and the model that answered, and a replay reports that model --
in the usage ledger, the Debug pane and on the Pipeline page, tagged recorded --
rather than the one the replaying config names. See
Determinism → The plan cache.
The .env next to nl2sql.yaml is loaded by the CLI, nl2sql-api and
nl2sql demo at startup, so doctor and run find a key kept only there; a
variable already exported in your shell wins over the file. There is no
--env flag and no .env.<name> lookup: one config folder, one .env.
run and index call engine.check() first. A datasource marked
required: true that cannot connect stops them with the report (exit 1); any
other one that fails is printed and left out of routing.
Sample questions
The demo's datasource files carry twenty questions (sample_questions), the same
ones the playground offers as guided questions -- a few as chips for the
database the rail is showing, and all twenty behind N more, in one pile per
database, each headed by the datasource id. Chinook's twelve come first, so the demo opens on familiar
ground:
- How many customers do we have, by country?
- Who are the top 5 customers by total spend?
- Which artist has the most albums?
- What is the total revenue per year?
- Which genre sells the most tracks?
- What is the average invoice total by billing country?
- Which employees support the most customers?
- What is the longest track in each genre?
- Which 5 customers bought the most jazz tracks?
- What was the monthly revenue in 2013 for customers in the USA?
- Which playlists contain tracks from more than three genres?
- Who are the top customers by total spend?
Then four on support:
- How many support tickets are still open, by priority?
- Which support agents resolved the most tickets?
- What is the average time to close a ticket, by category?
- Which customers raised the most support tickets?
And four on webanalytics:
- Which pages were viewed the most last year?
- How many sessions came from each marketing channel?
- What is the bounce rate by device type?
- How many purchases were there per month in 2013?
None of them spans two databases, deliberately: the engine cannot answer such a question yet. The rail says so too, under the database switcher: each question is answered from one database, and joining across them is planned. Clicking a question from another pile moves the switcher to that database, so the schema on the left is the one the question is about.
They are also indexed, per datasource, each question as an entry of its own
(schema.datasource_example, from sample_questions in its datasource file),
beside the schema.datasource entry with the configured description. Routing
votes with both, and with each datasource's tables and columns, which is how
the resolver tells the three apart (see
Routing). Re-index
(nl2sql index) after editing them.
Whether a given question is answered correctly depends on the model; see Known limitations.
Regenerating the demo
nl2sql init --demo DIR --force overwrites the config files and all three
databases under data/ with fresh copies; without --force it names the files
that exist and writes nothing. It never touches .env, so a saved key
survives, and it leaves the index under .nl2sql/ alone: run nl2sql index
afterwards. nl2sql demo never rewrites an existing project's config (only
llm.yaml, to point it at the mode it starts in); start a new --dir for a
fresh one.
The datasets that are not here
--lite and --docker chose between a generated manufacturing dataset in
SQLite and the same data in a Postgres/MySQL/MSSQL Compose stack. Both are gone:
the manufacturing DDL declared no foreign keys, so it could not answer a join
question, and it was the only dataset the test suite ran on. The Postgres, MySQL
and MSSQL adapters are unaffected - they are product features, configured
like any other datasource. The demo's three SQLite databases took their place.
A question that has to read two of them at once is still not answerable: the
engine plans each sub-query against a single datasource.