Zero-infrastructure Text-to-SQL agent for small data teams and individual
data engineers. pip install, point it at your database, ask questions in
natural language. Business metric definitions live in one YAML file that goes
into git — reviewing a metric change is just reviewing a PR. And when metric
definitions conflict, the agent asks you instead of guessing.
# illustrative session (numbers depend on the generated demo data)
你问> 上个月新增用户有多少?
[?] “新增用户”有两种口径:注册口径(按 created_at)和运营口径(按首单时间
first_order_at),你要哪一种?
你答> 注册口径
[ANSWER] 按注册口径,上个月新增用户 8,377 人(已剔除内部测试账号)。
SQL: SELECT count(*) FROM users WHERE strftime('%Y-%m', created_at) = ...
On the complexity spectrum Vanna (train a RAG model first) → WrenAI (deploy
a multi-service platform) → DB-GPT (orchestrate multiple agents), QueryAgent
takes the leftmost position: no vector store, no services, no UI, no agent
framework. One process, one config.yaml, one metrics.yaml.
| Vanna | WrenAI | QueryAgent | |
|---|---|---|---|
| Setup before first answer | train a RAG model | deploy multi-service platform + build MDL | write one config.yaml |
| Semantic / metric layer | none formal (example retrieval) | MDL (full-featured, JSON) | one YAML file, git-diffable |
| Infrastructure | vector store | services + vector store + UI | none |
| Conflicting metric definitions | — | governed centrally | agent stops and asks you |
| Form factor | library | BI platform | library |
| License | MIT | AGPL-3.0 engine | MIT |
WrenAI solves enterprise governance ("the CFO and the PM must get the same number"). QueryAgent solves the engineer's problem: make the agent use my definitions today, with zero new infrastructure. Lightness is the feature.
- Hand-written ReAct loop — no LangChain/LlamaIndex; the whole control flow is one readable generator with five explicit termination conditions, parse-failure fallback, and a self-repair loop (database errors are fed back to the model, capped at 3 retries). Multi-turn chat keeps the session in context, so follow-ups can refer back.
- Observability: every run records its event stream to
.queryagent/traces/*.jsonl;queryagent replay <trace>reconstructs it exactly. Token usage, prompt-cache hit rate, latency and an upper-bound cost estimate are reported per run and per eval suite. On by default with a startup notice and an off-switch (ADR-005). - Metrics as YAML (
metrics.yaml): definitions are matched to the question and injected into the prompt; answers cite the metric used. Metrics with acautionfield trigger a clarifying question when the user's phrasing is ambiguous — and are forbidden from asking when it isn't. - Three-layer SQL safety: a token-level whitelist (single SELECT only, CTEs allowed — string literals can't fool it, comments can't smuggle past it), connector-enforced timeouts and row caps, and a documented read-only account setup as the final backstop. See SECURITY.md.
- Three dialects out of the box: MySQL, SQLite (stdlib, Docker-free
demo), ClickHouse (
pip install queryagent[clickhouse]). New sources implement one 3-method protocol. - Two providers, one abstraction: any OpenAI-compatible endpoint
(DeepSeek / Qwen / GLM / vLLM / Ollama) via
base_url, plus Anthropic. Provider tool-call formats never leak into the agent. Verification is not equal between them — see the note below. - Event-stream architecture: the agent yields
AgentEvents; the CLI, the eval runner, and any future UI are just different consumers. This is the load-bearing seam of the codebase. - Reproducible evaluation built in:
queryagent evalruns a 20-case self-built suite (including should-ask / must-not-ask clarify controls) plus a fixed-seed public-benchmark subset (BIRD/Spider), comparing executed result sets — never SQL text. See eval/README.md.
git clone https://github.com/Harry-Sun0529/queryagent && cd queryagent
python3 -m venv .venv && source .venv/bin/activate
pip install -e ".[dev]"
make demo-data # generates demo_shop.db (fictional e-commerce)
export OPENAI_API_KEY=sk-... # DeepSeek key works here (default config)
queryagent chat --config examples/demo_ecommerce/config.sqlite.yamlTry: 上个月每天的新增用户数 — and watch it ask which definition of
"new user" you mean, then keep the thread: follow up with 那按渠道拆分呢?
and it remembers the month and the definition you chose. --verbose shows
the full THINK / ACT / OBSERVE trace. For scripts and pipes there is a
one-shot form:
queryagent ask "上个月的成交额是多少?按支付口径" \
--config examples/demo_ecommerce/config.sqlite.yamlThe default configs talk to DeepSeek via the OpenAI-compatible backend; the
same backend reaches Qwen/GLM/OpenAI/vLLM/Ollama by changing base_url.
With an Anthropic key, switch the commented llm: block:
llm:
backend: anthropic # reads ANTHROPIC_API_KEY
model: claude-sonnet-5How far each backend is verified. Everything published here — the numbers, the live smoke tests across three dialects, the failure analysis — was produced through the OpenAI-compatible backend against DeepSeek. The Anthropic backend has contract-level tests (message conversion, tool_use and tool_result blocks, usage parsing) but has never made a call to the live API, because the maintainer has no key. It was in fact broken against
anthropic1.x until CI caught it. Treat it as untried in production, and note thatllm.temperaturehas no effect there: the Messages API dropped the parameter, and the backend warns rather than letting a run believe it was deterministic.
make demo-up # MySQL 8 on :3307, data pre-loaded, read-only account
queryagent chat --config examples/demo_ecommerce/config.yaml
make demo-up-ch # additionally ClickHouse on :9001
queryagent chat --config examples/demo_ecommerce/config.clickhouse.yamlThe CLI is one consumer of the event stream; your code can be another. The
public surface is what queryagent exports — everything else may move.
from queryagent import AnswerEvent, ContextBuilder, ToolRegistry, run_agent
from queryagent.connectors.sqlite import SQLiteConnector
from queryagent.llm import make_backend
from queryagent.schema import render_schema
from queryagent.tools import make_default_tools
connector = SQLiteConnector(path="examples/demo_ecommerce/demo_shop.db")
builder = ContextBuilder(
schema_text=render_schema(connector.get_schema()), dialect=connector.dialect
)
registry = ToolRegistry(make_default_tools(connector, timeout_s=10, max_rows=200))
for event in run_agent("有多少用户?", backend=backend, registry=registry,
context_builder=builder):
if isinstance(event, AnswerEvent):
print(event.text)This snippet is executed by tests/test_public_api.py — documented code
that was never run is how a library's first impression breaks.
Create a read-only account (this is a load-bearing part of the security model, not optional hardening):
CREATE USER 'queryagent_ro'@'%' IDENTIFIED BY '...';
GRANT SELECT ON your_db.* TO 'queryagent_ro'@'%';Point config.yaml at it, write a metrics.yaml for your business
definitions, and set metrics_path in the config.
metrics:
- name: new_users # required, unique
display_name: 新增用户
aliases: [新用户, new users]
definition: > # required — injected into the prompt
按 users.created_at 的日期计数(注册口径);不含测试账号。
caution: > # optional — makes ambiguity a question, not a guess
运营口径按 first_order_at 计数;未指明口径且涉及报表时需确认。
tables: [users] # optional
sql_hint: "COUNT(*) FROM users WHERE ..." # optionalRequired fields (name, definition) are frozen; optional fields may grow
(semver promise, see CHANGELOG.md).
make eval # self-built 20 cases, demo db
queryagent eval --model deepseek-v4-pro \
--config examples/demo_ecommerce/config.sqlite.yaml # strong-model run
queryagent eval --public eval/public/dev-subset.json \
--db-dir eval/public/databases \
--config examples/demo_ecommerce/config.sqlite.yaml # BIRD dev setMethodology: executed row sets compared as order-insensitive multisets with float tolerance; five metrics including clarify-behaviour accuracy (asked when it should, didn't when it shouldn't). A fixed-seed subset of a public benchmark serves as an external anchor, with a hard rule that prompts are never tuned against it (eval/README.md).
Self-built suite (36 cases, ranges over 3 runs — DeepSeek exposes no sampling seed, so single-run numbers are noise):
| metric | deepseek-v4-flash |
|---|---|
| first-execution pass rate | 20–23/28 (71–82%) |
| pass rate after self-repair | 26–27/28 (93–96%) |
| metric-citation rate | 6–8/8 |
| clarify-behaviour accuracy | 15–16/16 (94–100%) |
Clarify accuracy counts both arms: the eight questions that should draw a question, and the eight that must not — a system that asks about everything would score zero here. The denominator was four until v0.5.0, where "4/4" was a good-looking number with almost no evidence behind it; quadrupling it left the result standing.
An earlier strong/weak comparison (v4-flash vs v4-pro, on the smaller
case set) found the strong model wins at getting it right first while the
self-repair loop converges the two, so the architecture buys a weak model
the same final accuracy at about a third of the cost
(dual-model-analysis.md).
Public benchmark (BIRD mini-dev, dev/test split — ADR-004):
| dev (100 cases, analysed) | test (200 cases, sealed) | |
|---|---|---|
| first-execution pass rate | 39% | 32% |
| pass rate after self-repair | 49% | 48% |
| tokens / latency / cost per case | 9,612 / 12.4s / $0.0022 | 8,699 / 11.8s / $0.0019 |
Honest notes, in the order they matter:
- A previous claim of ours did not survive a bigger sample. At 30 cases per set we measured a dev gain of +14pp against a test gain of +6pp and called the gap an overfitting measurement. At 100/200 cases the two sets agree within 1pp — the earlier gap is best explained as noise, which is exactly what the power analysis predicted (±25pp for a difference at n=30). The samples were expanded because of that analysis, and the result corrected us.
- What the numbers can and cannot support. A before/after comparison on the same questions is paired and needs 57–114 cases to detect a real effect — 200 covers it. A cross-sample comparison (dev gain vs test gain) needs ~1089 per group for 6pp, which is out of reach here; such comparisons are reported as directional only.
- The sealed set stays sealed. The governing rule is never change the system in response to test results — not "run it once". It ran once this release, on 200 questions freshly sampled from those never used before.
- Where the remaining failures are (from dev analysis at the previous size, still the operative picture): half were shape rather than substance — the right value returned with extra columns — and one general prompt rule fixed that class; a quarter are gold-SQL ambiguities that should not be fixed; a quarter are genuine capability gaps (dev-failure-analysis.md).
- Version-over-version numbers are decomposed, not hand-waved. v0.2.0 reported 83% first-execution and v0.3.0 reported 61–72%; a controlled three-cell run showed the code was a +5pp improvement and the entire drop came from enabling the model's thinking mode, which lowers first-attempt accuracy without lowering final accuracy (version-decomposition.md).
- Costs are peak-rate upper bounds (off-peak is half). Raw reports: eval/results/.
question ──▶ ReAct loop (agent.py) ──▶ Iterator[AgentEvent] ──▶ consumers
│ (chat CLI,
├─ LLMBackend llm/ Anthropic | OpenAI-compat
├─ ToolRegistry tools.py get_schema | execute_sql
│ | ask_clarification
│ └─▶ safety.py ─▶ Connector connectors/ mysql | sqlite
│ (whitelist) | clickhouse
├─ ContextBuilder context.py schema + metrics + budget
└─ MetricStore metrics/ YAML store, alias matching
Every module is small enough to read in one sitting; there is deliberately no framework between you and the control flow. Design rationale for the big decisions lives in commit messages and prompt-log.md — this project was built with heavy AI assistance under a documented protocol, and the log is the honest record of who decided what.
make test # ruff + mypy + pytest (153 tests; DB integration tests
# auto-skip when demo containers aren't running)
make demo-down # tear down demo databases- PostgreSQL connector (validates the Connector seam further)
- Cross-session memory for confirmed metric choices
- Embedding-based matching as an optional MetricStore implementation
- Published eval numbers (strong + weak model, self-built + public subset)
QueryAgent 是给小数据团队和个人工程师的零基建 Text-to-SQL Agent 库: 不用训练、不用部署平台、不用向量库。业务口径写在一个 YAML 里进 git,改口 径就是发 PR;当口径之间冲突(比如"新增用户"既可以按注册也可以按首单)而 问题又没说清时,agent 会停下来反问而不是猜一个。
两分钟上手(免 Docker):
pip install -e ".[dev]" && make demo-data
export ANTHROPIC_API_KEY=... # 或改配置用 DeepSeek 等 OpenAI 兼容端点
queryagent chat --config examples/demo_ecommerce/config.sqlite.yaml安全模型:SQL 白名单(仅单条 SELECT,词法级校验)+ 连接层超时/行数上限 +
只读数据库账号,三层互相独立。评估体系随库附带:make eval 一条命令复现
20 条自建用例(含"该问的要问、不该问的不许问"的追问对照组)。