A voice data analyst. Hold a button, ask a question about a Postgres database out loud, and hear the answer back, with the SQL, the result table and an automatic chart on screen. A LangGraph agent orchestrates the pipeline, DSPy generates the SQL with prompts optimized by MIPROv2 against an execution-accuracy metric, and every query is checked twice for safety before it touches the database.
🎙 "Which genres made the most money?"
SQL SELECT g.name AS genre, sum(il.unit_price * il.quantity) AS revenue FROM invoice_line il
JOIN track t ON t.track_id = il.track_id JOIN genre g ON g.genre_id = t.genre_id
GROUP BY g.name ORDER BY revenue DESC LIMIT 5
🔊 "Rock leads with about 827 dollars, followed by Latin, Metal, Alternative & Punk, and TV Shows."
📊 bar chart of revenue by genre
🎙 "And how much of that came from the USA?" ← follow-up keeps context
flowchart LR
MIC([🎙 audio]) --> STT[speech-to-text]
STT --> P[prepare]
TXT([text]) --> P
P --> G[generate SQL<br/>DSPy ChainOfThought<br/>MIPROv2-optimized]
G --> V{validate<br/>sqlglot AST + EXPLAIN}
V -->|error| F[repair<br/>DSPy FixSQL with the DB error]
F --> V
V -->|ok| X[execute<br/>read-only txn, timeout, row cap]
X --> A[spoken answer<br/>DSPy]
A --> C[chart inference]
C --> TTS[text-to-speech]
TTS --> OUT([🔊 answer + SQL + table + chart])
| Concern | Approach |
|---|---|
| SQL generation | A DSPy ChainOfThought program over a typed signature (db_schema, history, question → sql). The schema is introspected live: columns, types, primary and foreign keys, and a few sample values per text column so the model uses real spellings. |
| Prompt optimization | voice-sql optimize runs DSPy MIPROv2 (instruction proposals + bootstrapped few-shot demos) against 24 training questions, scores candidates by execution match on the real database, and saves the compiled program, which the agent loads at startup. |
| Self-correction | Postgres errors go back to a separate DSPy FixSQL predictor with the failed query, up to a configurable number of attempts. |
| Safety, layer 1 | sqlglot parses every query into an AST: exactly one statement, must be a SELECT/WITH, and no INSERT/UPDATE/DELETE/DDL/SELECT INTO anywhere, including inside CTEs. |
| Safety, layer 2 | Every query runs in a READ ONLY transaction with a statement_timeout and a row cap, as a dedicated analyst role that only has SELECT and defaults to read-only transactions. |
| Conversation | LangGraph checkpoints per thread_id; earlier successful questions and their SQL are passed as history, so "and only for 2024?" works. Failed turns are excluded from history. |
| Voice | Speech-to-text and text-to-speech through any OpenAI-compatible audio API: OpenAI, Groq Whisper, or self-hosted (e.g. Speaches / Kokoro). If TTS fails, the answer still returns as text. |
| Charts | Result shape decides the chart: one label column and numeric columns become a bar chart, or a line chart when the label is a date or period. |
| Observability | LangSmith traces the graph, the DSPy calls, and the validate, execute, transcribe and synthesize steps. |
git clone https://github.com/Alby-Rajesh/voice-text2sql.git
cd voice-text2sql
cp .env.example .env
docker compose up --buildOpen http://localhost:8080, hold the mic button and ask. Compose starts Postgres 16 preloaded with the Chinook music-store database (artists, albums, tracks, customers, invoices) and the read-only analyst role. Point VSQL_LLM__* at any OpenAI-compatible model (vLLM, OpenAI, Groq …) and VSQL_SPEECH__* at an audio API.
Without Docker:
pip install -e ".[dev]"
make db
voice-sql chat
voice-sql servecurl -s localhost:8080/v1/ask -H 'content-type: application/json' \
-d '{"question": "Who are the top 5 customers by spending?", "thread_id": "demo", "speak": false}'
curl -s localhost:8080/v1/voice -F audio=@question.webm -F thread_id=demoBoth return the transcript, answer, SQL, columns, rows, attempts, a chart spec and (for voice, or speak: true) base64 MP3 audio.
voice-sql optimize --auto light
voice-sql evaluateThe gold set (data/chinook_eval.jsonl) has 40 hand-written questions with reference SQL, from simple counts to multi-join revenue rollups, self-joins and anti-joins, split into 24 train and 16 dev. Every reference query is executed in CI.
Metric: execution match. Both queries run on the database, and the prediction counts as correct when it returns the same rows: same row count, every reference value present in the matching row, order enforced only when the reference sorts. Extra columns and different aliases are allowed, because "Rock, 826.65" is right whether or not the model also returns genre_id.
optimize writes artifacts/sqlgen.json (the compiled program) and artifacts/optimization.json (baseline vs optimized dev accuracy). evaluate runs the whole agent, including self-correction, and reports execution accuracy, first-try accuracy, valid-query rate, attempts and latency.
| Model | Program | Dev execution accuracy |
|---|---|---|
| Qwen2.5-Coder-7B-Instruct | zero-shot | run voice-sql optimize |
| Qwen2.5-Coder-7B-Instruct | MIPROv2 (light) |
Environment variables with the VSQL_ prefix and __ for nesting (see settings.py):
| Variable | Default | Purpose |
|---|---|---|
VSQL_LLM__MODEL |
openai/Qwen/Qwen2.5-Coder-7B-Instruct |
DSPy/LiteLLM model id |
VSQL_LLM__BASE_URL |
http://localhost:8000/v1 |
OpenAI-compatible endpoint |
VSQL_DB__DSN |
postgresql://analyst:analyst@localhost:5432/chinook |
database; use a read-only role |
VSQL_DB__STATEMENT_TIMEOUT_MS |
5000 |
per-query timeout |
VSQL_DB__ROW_LIMIT |
200 |
rows returned per query |
VSQL_SPEECH__BASE_URL |
– | OpenAI-compatible audio endpoint |
VSQL_SPEECH__STT_MODEL / TTS_MODEL / TTS_VOICE |
whisper-1 / tts-1 / alloy |
speech models |
VSQL_AGENT__MAX_ATTEMPTS |
3 |
generate → validate → repair limit |
VSQL_AGENT__PROGRAM_PATH |
artifacts/sqlgen.json |
compiled DSPy program |
VSQL_LANGSMITH__API_KEY |
– | enables LangSmith tracing |
src/voice_sql/
├── agent.py LangGraph state machine: generate → validate → repair → execute → answer
├── program.py DSPy signatures and modules: GenerateSQL, FixSQL, SpokenAnswer
├── database.py schema introspection (pg_catalog), sqlglot guard, read-only execution
├── evaluation.py execution-match metric, MIPROv2 optimization, agent evaluation
├── speech.py speech-to-text and text-to-speech over OpenAI-compatible audio APIs
├── chart.py chart inference from result shape
├── api.py FastAPI: /v1/ask, /v1/voice and the web app
├── web/index.html push-to-talk UI with result table and Chart.js
└── cli.py chat, serve, optimize, evaluate
db/ Chinook schema + data and the read-only analyst role
data/ 40-question gold set
make db
make testUnit tests cover the agent graph (repair loop, retry limit, conversation history), the SQL guard, the metric, chart inference, the DSPy program (with DummyLM) and the voice API. Integration tests run against a real Chinook Postgres as the read-only role: schema introspection, error reporting, the read-only transaction, and every gold query. CI starts Postgres as a service and runs both.
MIT. The Chinook database is © Luis Rocha, MIT licensed.