Open-source showcase · github.com/Shivay00001/data-analyst-agent
TL;DR: data-analyst-agent is a full-stack autonomous BI agent: upload a CSV, type a question in plain English, and it has an LLM write real Python (pandas/numpy/matplotlib/seaborn), executes the code in an isolated subprocess with a 60-second timeout, and returns the generated code, the output, and publication-style charts — all tracked in a task database and displayed in a Next.js console. I read backend/server.py end to end: this is genuinely autonomous analysis, not a chat wrapper. Status: working demo — the agentic loop is real, with the sandboxing and auth hardening a production deployment would need.
1. What it is
Two services: a FastAPI backend (backend/server.py, ~8.9 KB) with SQLAlchemy models (models.py: Dataset and ExecutionLog tables) and an async SQLite store, plus a Next.js frontend (frontend/src/app/page.tsx, ~9.7 KB) that handles dataset upload, prompt entry, provider/key selection, and live task polling. A Dockerfile ships the backend. Dependencies are the real data stack: pandas, numpy, matplotlib, seaborn, litellm, sqlalchemy, aiosqlite.
The workflow mirrors how a human analyst works: look at the data, write code, run it, look at the chart, iterate. Here the LLM writes the code, the backend runs it, and the console shows you everything — including the code itself, which is the right call for trust.
2. The problem it solves
Business data analysis has a bottleneck problem: every "can you just check…" question waits in a queue behind an analyst's backlog, and BI dashboards answer yesterday's questions, not today's. Meanwhile, LLMs are fluent in pandas but can't see your data and can't run code — and letting them near production data raises exactly the right fears about correctness and safety.
This agent closes the loop in a controlled way: the LLM never touches your data directly. It writes analysis code against a stated file path, and only the backend executes that code — in a separate process, with a timeout, writing charts to a per-task directory. You get analyst-speed answers with an audit trail: the exact code that produced every number and chart is stored in the database.
3. How it works — the agentic loop
- Upload.
POST /api/datasets/uploadsaves the file toworkspace/<uuid>_<filename>and records it in thedatasetstable. The/workspacedirectory is also mounted as static files, so generated charts are directly servable. - Dispatch.
POST /api/executetakes{prompt, provider, dataset_filepath}, creates anExecutionLogrow with statuspending, and hands the job to a FastAPIBackgroundTaskso the HTTP request returns immediately with atask_id. - Generate. The background worker calls
litellm.acompletionwith a carefully written system prompt. The prompt is worth quoting in spirit: the model is told it is an autonomous BI analyst, must output only valid Python in a fenced block, must handle missing values before any computation, must print the exact mathematical formula before every metric (auditability), must save plots withplt.savefig('name.png', dpi=300, bbox_inches='tight')— neverplt.show()— wrapped in try/except, and must print each saved filename so the backend can capture it. Predictive ML is explicitly forbidden (no scikit-learn): this is a descriptive-analytics agent that knows its lane. The provider string routes keys automatically:gpt-*→ OpenAI,claude-*→ Anthropic,gemini-*→ Google,zhipu-*→ Zhipu GLM,ollama*→ a local Ollama server atlocalhost:11434— so you can run the brain fully offline. - Execute. The fenced
```pythonblock is extracted, prepended with a styling preamble (pandas/numpy/matplotlib/seaborn imports plussns.set_theme(style='whitegrid')), written to a per-taskscript.py, and run viaasyncio.create_subprocess_execwith the task directory as cwd and a hard 60-second timeout (the process is killed on timeout). Stdout and stderr are captured separately and stored. - Collect. Any output line ending in
.pngis treated as a saved chart and converted to a servable URL under/workspace/<task_id>/. The log row flips tocompleted(orfailed, with the error captured) — code, output, and chart URLs all persisted. - Display. The frontend polls
GET /api/tasks/{task_id}every 2 seconds while the status is pending/running, then renders three panes: generated code, execution output, and the charts. The default prompt in the UI — "Group sales by month and plot a bar chart of total revenue." — is exactly the kind of question this loop eats for breakfast.
4. The details that show it was built by someone who's run analysis in production
- Formula auditability: the system prompt requires the model to print the statistical formula before the result — standard deviation isn't just a number, it's a number with its definition attached.
- NaN-first discipline: missing-value handling is a "CRITICAL INSTRUCTION," not an afterthought — the #1 crash source in real pandas work.
- Save, don't show:
plt.show()would hang a headless server; the prompt bans it and mandatessavefigwith print-capture of filenames. - Per-task isolation: each run gets its own directory and its own process; one bad script can't poison another run's state.
- API keys via headers: the frontend can pass keys per-request (
X-OpenAI-Key, etc.) or fall back to server env vars — no keys baked into the repo (the committed.env.exampleholds placeholders).
4b. What the frontend shows — and why showing the code matters
The console's three-pane result view (generated code, execution output, charts) is the most important UX decision in the repo, and it deserves a paragraph on why. Most "AI analyst" demos show you the chart and hide the code — which means you're trusting a black box with your numbers. This console inverts that: the generated Python is a first-class pane, the raw stdout (including the printed formulas the system prompt mandates) is a second pane, and the chart is the third.
That ordering is a trust model. When the agent says "revenue grew 12% month-over-month," you can read the exact groupby it ran, see the formula it printed, and check whether it handled the NaNs it was told to handle. For business decisions made on these numbers, "show your work" isn't a nicety — it's the difference between a demo and a tool. The 2-second polling loop (setInterval against /api/tasks/{task_id}, stopping on completed/failed) keeps the UI live without websockets, which is the right complexity level for this stage.
One more detail worth noting: the frontend passes API keys per-request via headers (X-OpenAI-Key and friends) rather than requiring them baked into the server environment. That makes the console usable against a shared backend where each analyst brings their own key — a small touch that anticipates multi-user operation even though auth itself isn't built yet.
5. How to run it
# Backend
cd backend
python -m venv venv && venv/Scripts/activate
pip install -r requirements.txt
uvicorn server:app --port 8000
# Frontend
cd frontend
npm install
npm run dev
Open the console, add your provider key (or point it at local Ollama), upload a CSV, type a question like the default monthly-revenue example, and watch the three panes fill in: code, output, charts. Database tables are created automatically on startup via the lifespan hook.
6. Honest status: what works, what needs hardening
Working. The full loop — upload → LLM code generation → isolated execution → chart capture → persisted task log → live-polling UI — is implemented and coherent. Multi-provider routing including local Ollama is real. The auditability constraints in the system prompt are genuinely unusual and genuinely good.
Needs hardening before production (from reading the code):
- The sandbox is soft: execution is a subprocess with a timeout — not a container, not a seccomp profile, not a no-network jail. The code being executed is LLM-generated. On a shared or internet-facing host, that's a remote-code-execution-shaped risk; containerize the executor (Docker/gVisor/Firecracker) before exposing it.
- No auth, open CORS:
allow_origins=["*"]and zero authentication — anyone with network access can upload files and burn your LLM budget. Reverse-proxy + auth at minimum. - Port drift: the README/Dockerfile say port 8000, but the frontend fetches
localhost:8002for its API calls while the backend itself constructs chart URLs onlocalhost:8000. Align the ports or charts won't load from the console. - Upload validation: the upload endpoint accepts any file without type/size checks — fine for a demo, worth bounding in production.
7. Roadmap — from demo to product
- Hard sandboxing — run generated code in throwaway containers with no network, CPU/memory caps, and read-only data mounts.
- Auth + workspaces — users, per-user datasets, per-run cost attribution (LLM tokens per task are the natural billing unit).
- SQL generation — extend beyond CSVs: let the agent write SQL against connected databases (the repo already has SQLAlchemy and a
database.py— the seam is there). - Streaming + iteration — stream the generated code as it's written, and let users ask follow-ups against the same task context ("now break it down by region").
- Eval harness — a benchmark set of datasets × questions with expected chart types and metric values, run against every prompt or model change.
Frequently asked questions
No — it receives only the dataset's file path and your prompt. It writes code against that path; the backend executes the code where the data lives. The model never ingests your rows.
Yes. Any provider string starting with ollama routes to http://localhost:11434, so a local Ollama model can be the analyst brain with zero API spend.
The failure is captured, not hidden: stderr is stored in the execution log, the task flips to failed, and you see both the code and the error in the console — which is exactly the material you'd need to fix the prompt or retry.
In the current repo: safe enough for a local demo on your own machine, not for a multi-user deployment. The subprocess-plus-timeout isolation is real but thin — containerize the executor before exposing it to other users.
The bottom line
data-analyst-agent is the most architecturally interesting repo in this batch: a genuine agentic loop (generate → execute → capture → persist → display) with production instincts baked into the prompt engineering — formula auditability, NaN-first discipline, save-don't-show plotting. The sandboxing and auth need real work before it serves anyone but you, and the ports need aligning, but the core idea is sound and the implementation is honest: you see the code it wrote, the output it produced, and the charts it saved. That's what an AI analyst should look like.
Source: github.com/Shivay00001/data-analyst-agent — MIT licensed. Verified by reading backend/server.py, backend/models.py, backend/database.py, and frontend/src/app/page.tsx on the main branch.