PyPI (Python API)
Use this path when you want to embed NL2SQL directly in a Python application. See the Core API documentation at docs/api/index.md.
Install
# Core only
pip install nl2sql-engine
# install all available adapters. (Never prefer this, until you really need all adapters.)
pip install "nl2sql-engine[all]"
# Core with selected adapters
pip install "nl2sql-engine[postgres]"
pip install "nl2sql-engine[mysql,mssql]" # [mssql] also needs Microsoft's ODBC Driver for SQL Server
pip install "nl2sql-engine[duckdb]"
# The browser playground behind `nl2sql demo` (adds fastapi + uvicorn).
# `[all]` does NOT include it -- `all` covers database drivers only.
pip install "nl2sql-engine[demo]"
Running a query needs an LLM key; only indexing is key-free. See Known limitations.
Configure
The engine reads one config, nl2sql.yaml, and the files it includes.
nl2sql init writes it (in Python, nl2sql.config.init_project("my-project")
writes the same files); see
Start a config with nl2sql init.
Each section has its own page:
Building the engine in Python
from nl2sql import NL2SQL
with NL2SQL.from_config("nl2sql.yaml") as engine: # or $NL2SQL_CONFIG, then ./nl2sql.yaml
report = engine.check() # connects, and reports what is ready
print(report)
Building the engine never connects to a datasource or calls a model.
engine.check() does: it raises EngineNotReady when a datasource with
required: true fails, and otherwise leaves a failed datasource out of
routing. The library never reads a .env file; call python-dotenv yourself
before from_config if you keep keys in one. See
Public Facade for every way
to build it, including a Config built in code and NL2SQL.demo(). Use the
config for fixed, app-wide settings. Use runtime registration when you need to
add datasources on a long-running process without restart.
Datasource configuration
datasources/ holds one file per datasource; the file name is its id:
# datasources/my_postgres.yaml
type: postgres
description: Primary analytics DB.
connection:
host: localhost
port: 5432
user: "${env:POSTGRES_USER}"
password: "${env:POSTGRES_PASSWORD}"
database: analytics
limits: { row_limit: 100, timeout_ms: 8000, max_bytes: 10485760 }
Install the adapter extras that match type:
See Adapters for the full list of available adapters. See Datasource config for field definitions.
LLM configuration
llm.yaml selects the model and any per-agent overrides:
# llm.yaml
provider: openai
model: gpt-5.4
temperature: 0.0
# api_key is optional: without it OPENAI_API_KEY is read.
agents:
indexing_enrichment: { model: gpt-5.4-mini }
To route chat through OpenRouter instead — one key for Anthropic, Google, Meta and hundreds of other models, with no extra dependency:
To use Claude on Anthropic's own API, with the system prompts prompt-cached,
install the anthropic extra and point the config at it:
See LLM configuration → Anthropic.
A local Ollama daemon is configured the same way, with
provider: ollama; how well a given local model handles the pipeline's
structured output varies by model, so see
LLM configuration → Ollama first.
Note that OpenRouter serves chat completions only. Embeddings are their own
section: embeddings: { provider: local } (what nl2sql init writes) embeds
with a key-free ONNX model, and provider: openai needs OPENAI_API_KEY. See
Embeddings.
See LLM config for full schema details.
Add a datasource at runtime
You can register a datasource programmatically without editing config files:
from nl2sql import NL2SQL, Datasource, Limits
engine = NL2SQL.from_config("nl2sql.yaml")
engine.datasources.add(Datasource(
id="runtime_pg",
type="postgres",
description="Runtime Postgres datasource",
connection={
"host": "localhost",
"port": 5432,
"user": "postgres",
"password": "${env:RUNTIME_PG_PASSWORD}",
"database": "analytics",
},
limits=Limits(timeout_ms=8000, row_limit=100, max_bytes=10485760),
))
engine.index.build(["runtime_pg"])
The description is indexed as given, even though no file lists
runtime_pg, and the datasource joins engine.config. Indexing makes no LLM call unless you ask for one:
engine.index.build(["runtime_pg"], enrich=True) has the
indexing_enrichment agent write descriptions for tables and columns the
database left without a comment.
You can also add the datasources of another config:
from nl2sql import NL2SQL, load_config
engine = NL2SQL.from_config("nl2sql.yaml")
for datasource in load_config("other/nl2sql.yaml").datasources:
engine.datasources.add(datasource)
Replace the LLM at runtime
engine.llm.replace swaps the whole llm section in the running engine, as
one step: it is validated as llm.yaml is, every agent's key must resolve,
and questions already running finish on the old LLM.
from nl2sql import NL2SQL, LLM, AgentLLM
engine = NL2SQL.from_config("nl2sql.yaml")
engine.llm.replace(LLM(provider="openai", model="gpt-5.4",
agents={"ast_planner": AgentLLM(model="gpt-5.5")}))
engine.llm.list()["ast_planner"]["model"] # 'gpt-5.5'
The llm section of another config:
Policies configuration
policies.yaml defines which roles can access which datasources and tables:
# policies.yaml
default_role: viewer
admin_roles: [admin]
roles:
admin:
description: System Administrator
allow: ["*"]
viewer:
description: Read-only access to the catalogue
allow: ["my_postgres.public.products"]
{allow_all: true} (what nl2sql init writes) lets every caller read
everything, for local use only. See
Policies config for the allow and deny patterns and
wildcards.
Secrets configuration (optional)
A secrets section is only needed when you want to pull secrets from a
provider other than the environment:
# secrets.yaml
providers:
- id: azure-prod
type: azure
vault_url: "https://my-vault.vault.azure.net/"
A datasource then writes password: "${azure-prod:db-password}". See
Secrets config for supported providers and fields.
Run a query
from nl2sql import NL2SQL
from nl2sql import UserContext
engine = NL2SQL.from_config("nl2sql.yaml")
user_ctx = UserContext(user_id="user123", roles=["admin"])
result = engine.run_query(
"Top 5 customers by revenue last quarter?",
user_context=user_ctx
)
print(result.final_answer)
for sub_query in result.sub_queries:
print(sub_query.datasource_id, sub_query.sql)
The QueryResult includes sub_queries (each with its generated sql),
final_answer, errors, warnings, artifact_refs, and a trace_id for
observability. Each sub-query carries a capped sample of its rows (rows); the
full result set lives in artifact storage and is addressed through artifact_refs.
A question repeated on the same day reuses its validated plan from the plan cache (no planner
call, same SQL and rows; still validated every time), and
sub_query.plan_source says "cache". Turn it off with
settings.pipeline.plan_cache: false; empty it with nl2sql cache clear. See
Determinism → The plan cache.
Lifecycle (configure → check → index → query)
- Configure datasources, LLMs, and policies (
nl2sql init). - Build the engine and call
engine.check(). - Index schemas at least once (and after schema changes).
- Run queries.
Index schema (required)
Configure providers
Secrets providers are installed via extras and selected in the secrets section:
pip install "nl2sql-engine[aws]"
pip install "nl2sql-engine[azure]"
pip install "nl2sql-engine[hashicorp]"
See Configuration for the whole config, the environment variables it reads, and where each 0.4 file and variable went.