sql_tool
by ShadowOO7
README.md
# DataPilot — AI Data Analyst Agent
Multi-agent system: ask a question about a dataset in plain language.
Agents plan, generate/execute SQL (or route to a Python tool when SQL
genuinely doesn't fit), validate the result, optionally chart it, and
explain it back honestly — including saying "I can't compute that"
instead of guessing. Built with LangGraph orchestration, real MCP tool
servers, guardrails proven independent of LLM behavior, a self-correction
retry loop, ChromaDB memory, and an eval harness with ground-truth checks.
Builds on the same orchestration/MCP/guardrails skeleton as the
Job/Resume Intel Agent project — domain swapped, agent pattern reused.
**All 10 steps of the build are complete.** See `TESTING.md` for the
full run-by-run debugging journal (12 real test runs, every bug found
and how it was fixed) — that document is arguably more informative than
this one about how the system actually behaves.
## Architecture
```
┌─────────────┐
│ Planner │ keyword routing (correlation → Python,
│ │ chart request → Chart Agent) + memory
└──────┬──────┘ retrieval (ChromaDB business terms)
│
┌─────────────┴─────────────┐
▼ ▼
┌───────────────┐ ┌───────────────┐
│ SQL Agent │ │ Python Agent │
│ (MCP sql_tool) │ │(MCP python_tool)│
└───────┬───────┘ └───────┬───────┘
│ │
└─────────────┬──────────────┘
▼
┌─────────────┐
│ Validator │──── retry (SQL path only,
│ │ capped, on genuine errors
└──────┬──────┘ or column mismatches)
│
validated?│
┌────────────┴────────────┐
▼ ▼
┌───────────────┐ ┌─────────────┐
│ Chart Agent │ │ Interpreter │
│(MCP chart_tool)│─────────▶│ │
└───────────────┘ └─────────────┘
```
- **Planner** — decides SQL vs Python (deterministic keyword check, not
LLM judgment — see Known Limitations for why), whether a chart was
asked for, and retrieves relevant business-term context from memory.
- **SQL Agent** — fetches schema + generates + executes SQL, entirely
through MCP tool calls. Can respond `NOT_COMPUTABLE` when business
context says a question isn't answerable from this data (e.g. profit).
- **Python Agent** — handles correlation questions SQL fights with
(aggregate + raw columns "in one row"). No arbitrary code execution —
fixed, safe pandas operations only.
- **Validator** — catches SQL errors, zero-row/NULL-only results, and
column-mismatches (SQL that runs fine but ignores a column the
question asked about). Drives the retry loop.
- **Chart Agent** — generates a bar chart when asked, picking axes by
data type (not SQL column order — a real bug found and fixed in
testing). Fails gracefully on shapes that don't fit.
- **Interpreter** — compiles the final plain-language answer, or
explains a failure honestly instead of hallucinating from bad data.
**MCP layer**: `sql_tool`, `python_tool`, `chart_tool` — three real MCP
servers over stdio, not hardcoded functions. Agents talk to them the
same way they'd talk to any MCP-compliant tool.
**Guardrails**: read-only SQL enforcement lives server-side in
`sql_tool_server.py` (blocks DROP/DELETE/UPDATE/INSERT/ALTER, rejects
multi-statement SQL) — and is proven to work independent of the LLM's
behavior via `tests/test_sql_guardrail.py` (7/7 malicious queries
blocked, 3/3 safe queries pass), not just by an adversarial test
question that the model might happen to dodge on its own.
**Memory**: `app/memory.py` — ChromaDB-backed retrieval of 5 business-term
definitions (revenue, profit, discount, region, order), filtered by a
distance threshold so irrelevant questions get no injected context.
Concrete payoff: this dataset has no cost data, so the SQL Agent can
correctly decline profit questions instead of guessing.
**Observability**: `app/observability.py` — every run's full state is
logged to `outputs/traces/*.json` (always works, no external account).
Langfuse is wired too, centrally in `get_llm()`, but only activates if
credentials are set in `.env` — never required to run the project.
**Eval harness**: `tests/eval_harness.py` checks numeric results against
ground truth computed independently with pandas (not the agent's own
output), plus a row-count consistency check that scans the Interpreter's
answers for stated counts and flags mismatches against the real data.
## Build order
1. ✅ Domain + sample dataset (`data/sales.csv`) + fixed test queries
2. ✅ State schema + baseline graph
3. ✅ MCP `sql_tool` server (DuckDB, read-only enforced)
4. ✅ Guardrail proven (`tests/test_sql_guardrail.py`, LLM-bypassing)
5. ✅ Self-correction retry loop (bad SQL → re-plan → re-execute)
6. ✅ Python analysis tool (MCP-wrapped) for non-SQL-expressible questions
7. ✅ Chart tool (MCP-wrapped) + visualization logic
8. ✅ Memory (ChromaDB for business-term RAG) + observability (Langfuse)
9. ✅ Eval harness
10. ✅ UI (Streamlit)
## Setup
**Two terminals — Ollama needs to stay running while anything else runs.**
Terminal 1 (leave running):
```bash
ollama serve
```
Terminal 2:
```bash
pip install -r requirements.txt
cp .env.example .env
ollama pull llama3.2:3b # only needed once
```
> Pinned to `mcp<2.0.0` — MCP 2.x renamed `FastMCP` to `MCPServer` and
> broke this project's server code. If you see
> `ModuleNotFoundError: No module named 'mcp.server.fastmcp'`, run
> `pip install "mcp<2.0.0" --force-reinstall`.
## Running it
**CLI** — runs the fixed test queries and prints full trace/debug info:
```bash
python -m app.main
```
**UI** — chat-style interface, example questions in the sidebar, charts
rendered inline, an expandable panel per answer showing the SQL/Python
used, business context, and full agent trace:
```bash
streamlit run streamlit_app.py
```
**Guardrail proof** — bypasses the LLM entirely, sends known-malicious
SQL straight to the MCP tool:
```bash
python -m tests.test_sql_guardrail
```
**Eval harness** — ground-truth + row-count consistency checks:
```bash
python -m tests.eval_harness
```
Set `DEBUG_MEMORY=1` before any of the above to print ChromaDB's actual
retrieval distances per question (useful for tuning `_MAX_DISTANCE` in
`app/memory.py`).
## Fixed test queries
1. Simple aggregation — total revenue
2. Group + filter — sales by region, Electronics only
3. Correlation — discount vs quantity (routes to Python Agent)
4. Adversarial — SQL-injection-style instruction embedded in the question
5. Nonsense / no-match — question the dataset can't answer
6. SQL-error-prone — tests the retry loop directly
7. Chart request — grouped data, should produce a real bar chart
8. Chart on a shape that may or may not reduce to a single aggregate
9. Business-term memory — profit margin (not computable from this data)
## Project structure
```
data/
sales.csv # sample dataset used by all test queries
app/
state.py # shared GraphState
llm.py # centralized LLM client (Ollama default) + Langfuse wiring point
mcp_client.py # generalized MCP client (works with any server script)
memory.py # ChromaDB business-term retrieval
observability.py # local trace logging + optional Langfuse handler
utils.py # shared helpers (e.g. mentioned_columns)
graph.py # LangGraph wiring
main.py # CLI entrypoint, runs fixed test queries
agents/
planner.py # routing + memory retrieval
sql_agent.py # calls MCP sql_tool
python_agent.py # calls MCP python_tool (correlation questions)
validator.py
chart_agent.py # calls MCP chart_tool
interpreter.py
mcp_servers/
sql_tool_server.py # FastMCP server, DuckDB-backed, read-only enforced
python_tool_server.py # FastMCP server, pandas-backed, fixed safe stats only
chart_tool_server.py # FastMCP server, matplotlib-backed, fixed bar chart only
tests/
test_sql_guardrail.py # bypasses the LLM, proves the read-only guardrail directly
eval_harness.py # benchmark: ground-truth checks + row-count consistency check
streamlit_app.py # UI entrypoint
```
Generated charts are written to `outputs/` and run traces to
`outputs/traces/` (both gitignored — regenerate by running the app).
ChromaDB's local data lives in `chroma_data/` (also gitignored,
regenerated automatically on first run).
## Known limitations (honest)
- **Small-model self-correction is limited.** With `llama3.2:3b`, a
genuinely malformed query sometimes gets retried with the *identical*
broken SQL rather than a fix — the retry cap still kicks in and the
system fails honestly rather than hallucinating, but it doesn't always
self-correct. A larger model would likely do better; this is a
model-capability limit, not a guardrail or architecture gap.
- **No arbitrary Python code execution.** By design — a real sandboxed
`run_python(code, df)` tool with Docker isolation is future work.
exec()-ing LLM-generated code without a sandbox in place was
deliberately avoided rather than "temporarily" allowed.
- **Routing between SQL and Python is keyword-based, not learned.**
Deliberately conservative (SQL is the default; only a correlation
keyword match routes to Python) after the LLM's own routing judgment
proved unreliable twice in testing — once nearly skipping SQL entirely
with no fallback. A broader set of "SQL can't express this well" cases
(regression, forecasting) isn't covered.
- **Chart Agent and Validator assume this dataset's specific shape.**
Chart Agent only produces one shape (category + numeric value → bar
chart); the Validator's checks are written against this table's
structure. Generalizing to arbitrary uploaded datasets would need both
to reason about shape/schema dynamically.
- **Python Agent's column discovery is dynamic (fixed from an earlier
hardcoded list)** — fetched from the SQL tool's schema at runtime,
filtered to numeric types, so a different dataset's numeric columns
work without editing the file.
- **The Interpreter's narration isn't fully fact-checked against the
data.** Seen once in testing: a correct 20-row SQL result got
summarized as "13 unique orders." Partly addressed by
`tests/eval_harness.py`'s row-count consistency check, which hasn't
yet caught a live violation on this benchmark — only proven to not
false-positive.
- **Business-term memory covers 5 fixed, hand-written terms.** Adding
new ones means editing `_SEED_TERMS` in `app/memory.py` directly.
Retrieval distance threshold (`_MAX_DISTANCE`) is an empirical
starting point, confirmed reasonable via `DEBUG_MEMORY=1` runs but not
rigorously tuned.
- **Langfuse integration is wired but not verified** against real
credentials — only local trace logging has been confirmed working.
- **This is a single-dataset prototype, not a general data-analyst
tool.** It demonstrates the architecture pattern — multi-agent
orchestration, MCP tool boundaries, guardrails proven independent of
LLM behavior, retry/self-correction, memory, eval — on one fixed CSV.
Supporting arbitrary uploaded datasets (the original project plan's
Phase 5) would need the points above addressed, not just more test
questions against this one.
## Setup notes for the UI specifically
The Streamlit app is a thin layer over the same `build_graph()` used by
the CLI — no separate agent logic. Each question still spawns fresh MCP
subprocess calls per tool use (same as the CLI), so expect the same
per-query latency you'd see running `python -m app.main`, just rendered
in a browser instead of a terminal.
This server cannot be deployed
Maintenance
ActivityMaintained
ResponsivenessNo issues