Skip to content

About

No description, website, or topics provided.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Repository files navigation

Data Analyst Agentic System

LangGraph implementation of the design in docs/text_to_sql_agent_design_spec.md. Natural-language questions in, plain-English answers out, against the Chinook SQLite database -- no SQL ever shown to the user.

Architecture

flowchart TD
    U([User question]) --> ORCH

    ORCH["orchestrator_node<br/>plans 1-3 sub-queries, or asks<br/>if it can't form a plan"]
    ORCH -->|needs_clarification, awaiting_user| WAIT([Question / option cards<br/>returned to the user])
    ORCH -->|plan ready| SQL

    subgraph SQLAGENT["sql_agent_node -- one sub-query"]
        SQL["Tool-calling loop<br/>(model chooses the tools itself)"] --> VAL{"result_validator"}
        VAL -->|invalid, sql_retry_count < 2| SQL
    end

    SQL -.->|bound tools| TOOLS["explore_schema · execute_sql<br/>get_sample_rows · get_column_stats<br/>check_table_exists"]

    VAL -->|valid| MORE{More sub-queries<br/>in the plan?}
    MORE -->|yes| ADV[advance_sub_query] --> SQL
    MORE -->|no| ANALYST

    ANALYST["analyst_node<br/>classify report type, check sufficiency,<br/>write the explanation"] -->|insufficient, refine_count < 2| ORCH
    ANALYST -->|sufficient, or refine cap reached| DONE([final_report to the user])

    CP[(LangGraph checkpointer<br/>session memory)] -.->|turn_history,<br/>persisted per thread_id| ORCH
    ANALYST -.->|writes turn_history| CP
Loading

Two things worth noting that aren't obvious from the diagram:

  • The tool-calling loop is genuinely agentic. The model decides which of the five bound tools to call, in what order, and when it has enough information to commit to a query -- it isn't a fixed explore-then-generate pipeline. result_validator is a separate, deterministic sanity check that runs after the model is done, on purpose: defense in depth against a confidently-wrong tool-calling agent.
  • Three nested budgets keep it bounded, since sub-query count, tool-calling freedom, and analyst refinement all multiply each other: sql_retry_count (max 2, per sub-query, failed execute_sql calls only), tool_call_count (max 6, per sub-query, every tool call), and total_tool_calls (max 24, whole turn, the real backstop -- see spec §8).

Session memory (active_filters, last_metric, turn_history) lives directly in AgentState and is persisted automatically by LangGraph's own checkpointer, keyed by session_id -- there's no separate memory store to keep in sync.

Local setup

Requires Python 3.11+ (built and tested on 3.14).

git clone <this repo>
cd Data-Analyst-Agent

python -m venv .venv
.venv/Scripts/activate        # source .venv/bin/activate on macOS/Linux

pip install -r requirements.txt

cp .env.example .env
# edit .env and set ANTHROPIC_API_KEY=sk-ant-...

The Chinook database is already checked in at db/chinook.db (downloaded from lerocha/chinook-database) -- nothing else to provision.

Model provider

Defaults to Anthropic. To use Groq instead, set in .env:

LLM_PROVIDER=groq
GROQ_API_KEY=gsk_...

Pick a Groq model that actually supports tool-calling (the SQL agent's whole loop depends on it) -- llama-3.3-70b-versatile is the default; not every model Groq hosts supports tools. agent/llm.py is the only file that knows about provider differences; everything else uses the same LangChain interface regardless of which one is active.

Note from live testing: Groq's smaller/faster models are noticeably more variable than Claude here -- e.g. correctly aggregating SUM(Total) GROUP BY CustomerId but never joining to Customer for a human-readable name, or occasionally exhausting sql_retry_count on a query Claude gets first try. The harness (retries, budgets, graceful failure) handles this correctly either way; it's a model-quality difference, not a bug -- exactly what eval/run_eval.py's execution-accuracy check is there to catch.

Running it

uvicorn api:app                              # agent backend, in one terminal
streamlit run streamlit_app.py               # chat UI, in another terminal
python main.py                               # interactive REPL (doesn't need api.py)
python main.py "total revenue this quarter"  # single-shot mode (doesn't need api.py)

streamlit_app.py is a chat client over api.py's POST /query, not the graph directly -- api.py must be running first (see config.API_BASE_URL). Each browser tab gets its own session via a random session_id, echoed back by the API and reused on every follow-up request, same isolation mechanism as the CLI's thread_id. Missing filters show up as a question; a vague-intent clarification renders as clickable option buttons instead of typed text. Use "New session" in the sidebar to drop memory and start a fresh conversation without restarting either process.

main.py's CLI, by contrast, still invokes the compiled graph directly in-process -- it has no dependency on api.py at all.

Testing

pytest                    # 100 tests, all runnable without an API key
python -m eval.run_eval   # full gold-question eval suite -- needs a real API key

Everything under tests/unit/ and most of tests/integration/ runs against the real db/chinook.db but with the LLM calls stubbed out (see tests/integration/test_sql_agent_budgets.py and test_multi_query_graph.py for how the tool-calling loop and the multi-sub-query graph are tested without hitting a real model). eval/run_eval.py is the one thing that genuinely needs a configured provider, since it drives the actual agent.

About

No description, website, or topics provided.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages