Browser-first TypeScript toolkit for converting natural-language questions to SQL via an LLM agent loop with verification layers.
Most NL2SQL libraries just translate a question to a single SQL string and call it done. Real users want trustworthy answers: the agent should plan, run queries, verify them, and iterate until the data actually answers the question. nl2sql packages that loop in a small, dependency-light, provider-agnostic library that works in the browser and Node.
- Plan -> Query -> Verify -> Answer loop powered by native LLM function-calling.
- Streaming:
onTokencallback streams answer tokens to the UI as they arrive. - Configurable SQL requirement:
requireSqlBeforeFinish: falselets the model answer greetings and non-data questions without running SQL. - Stuck detection: after 3 consecutive query failures, the model is told to change approach or finish with what it has.
- Duplicate query cache: identical SQL is detected and cached — the model gets the cached result with a warning instead of re-executing.
- Reserved-word sanitization:
AS total->AS [total]automatically, preventing AlaSQL parse errors. - Layered verification: hard checks (column/table existence), advisory warnings (zero rows, high row count, all-null columns).
- Provider-agnostic: ship your own
LlmProvider(for any model) andSqlProvider(for any database). Reference OpenAI adapter included. - Browser-first: works in Vite/Webpack/Next.js out of the box. No Node-only polyfills.
- Cancellable: pass an
AbortSignalto bail out mid-flight.
npm install nl2sql openaiopenai is a peer dependency when you use the bundled OpenAI adapter. If you implement your own LlmProvider, you don't need it.
import { Nl2SqlAgent, createOpenAiProvider, createStaticSqlProvider } from "nl2sql";
import OpenAI from "openai";
const agent = new Nl2SqlAgent({
provider: createOpenAiProvider(
new OpenAI({ apiKey: process.env.OPENAI_API_KEY!, dangerouslyAllowBrowser: true }),
{ model: "gpt-4o-mini", temperature: 0.1 }
),
language: "en",
requireSqlBeforeFinish: true, // default; set false for conversational mode
});
const result = await agent.query("Top 5 customers by revenue last month", {
sqlProvider: createStaticSqlProvider({
schemas: [{ name: "orders", columns: [{ name: "customer", type: "STRING" }, { name: "revenue", type: "FLOAT" }, { name: "created_at", type: "DATETIME" }] }],
executeSQL: async (sql) => myDb.exec(sql),
}),
}, {
onStep: (step) => console.log("ran:", step.sql),
onToken: (token) => process.stdout.write(token), // streaming
onAnswer: (answer) => console.log("done:", answer),
});
console.log(result.answer);
console.log(result.sql);
console.log(result.steps);| Option | Type | Default | Description |
|---|---|---|---|
provider |
LlmProvider |
required | LLM provider (use createOpenAiProvider or implement your own) |
language |
string |
"en" |
Locale code for output formatting ("en", "fa", etc.) |
maxSteps |
number |
20 |
Maximum LLM round-trips before forcing a final answer |
requireSqlBeforeFinish |
boolean |
true |
When false, the model can answer non-data questions (greetings, thanks) without running SQL. The finish tool only requires answer, not sql/explanation. |
historyReducer |
(messages, max) => messages |
defaultHistoryReducer |
Trims conversation history to fit context windows |
sqlHints.maxRowsWarning |
number |
500 |
Advisory warning threshold for large result sets |
sqlHints.allowNonSelect |
boolean |
false |
Allow INSERT/UPDATE/DELETE statements |
| Callback | Description |
|---|---|
onStep(step) |
Fired after each SQL query executes (includes sql, result, verification) |
onToken(token) |
Streams answer tokens as they arrive from the LLM |
onAnswer(answer) |
Fired when the model produces its final answer |
onFinalSQL(sql) |
Fired with the canonical SQL from the finish call |
signal |
AbortSignal to cancel mid-flight |
- The agent assembles a system prompt from composable sections (role, capabilities, schema, workflow, output rules, optional instructions).
- It calls the LLM with two tools:
run_sqlandfinish. - SQL is sanitized (
sanitizeReservedAliases) and verified before execution: column references must match the schema (minimal SQL tokenizer + Levenshtein near-match). - Duplicate queries are detected via normalized SQL cache and return cached results.
- After execution, advisory warnings are computed (zero rows, high row count, all-null columns) and attached to the tool result.
- If 3 consecutive queries fail, a system message tells the model to change approach or finish.
- The LLM may call
run_sqlagain to verify, or callfinishwith the final answer. - Answer tokens stream to the UI via
onTokenas they arrive.
examples/browser— Vite + AlaSQL demo with OpenAIexamples/node— Node ESM script
Shipping OpenAI API keys in a browser exposes them to users. Use the OpenAI adapter's dangerouslyAllowBrowser: true flag only for local development or trusted internal tools. For production browser deployments, proxy requests through your own backend that injects the API key.
MIT