Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 6 additions & 4 deletions .env.example
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
# Choose your model (PydanticAI format: provider:model)
MODEL=anthropic:claude-haiku-4-5-20251001
# Compose-level configuration. Copy to `.env` (gitignored) and adjust.
# These feed both the `db` service and the backend's DATABASE_URL.

# Set the API key for your chosen provider
ANTHROPIC_API_KEY=sk-ant-...
# --- Postgres credentials ---
POSTGRES_USER=postgres
POSTGRES_PASSWORD=postgres
POSTGRES_DB=app
10 changes: 9 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -13,4 +13,12 @@ __pycache__/
.env
.venv/

output/
output/

# Node / frontend
frontend/node_modules/
frontend/dist/
.vite/

# Ruff
.ruff_cache/
10 changes: 10 additions & 0 deletions .vscode/extensions.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"recommendations": [
"ms-python.python",
"ms-python.vscode-pylance",
"charliermarsh.ruff",
"bradlc.vscode-tailwindcss",
"dbaeumer.vscode-eslint",
"esbenp.prettier-vscode"
]
}
33 changes: 33 additions & 0 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
{
// ---------------------------------------------------------------------------
// Python (backend) — point Pylance at the uv-managed virtualenv so imports
// like `fastapi`, `sqlalchemy`, `app.*` resolve correctly.
// ---------------------------------------------------------------------------
"python.defaultInterpreterPath": "${workspaceFolder}/backend/.venv/bin/python",
"python.analysis.extraPaths": ["${workspaceFolder}/backend"],
"python.analysis.autoSearchPaths": true,
"python.analysis.typeCheckingMode": "basic",
"python.terminal.activateEnvironment": true,

// ---------------------------------------------------------------------------
// CSS / Tailwind — stop the built-in CSS linter from flagging @tailwind,
// @apply, @layer as "Unknown at rule". Tailwind extension handles these.
// ---------------------------------------------------------------------------
"css.lint.unknownAtRules": "ignore",
"scss.lint.unknownAtRules": "ignore",
"less.lint.unknownAtRules": "ignore",
"tailwindCSS.includeLanguages": {
"typescriptreact": "javascript"
},
"tailwindCSS.experimental.configFile": "frontend/tailwind.config.js",
"editor.quickSuggestions": {
"strings": "on"
},

// ---------------------------------------------------------------------------
// Per-language files association
// ---------------------------------------------------------------------------
"files.associations": {
"*.css": "tailwindcss"
}
}
10 changes: 0 additions & 10 deletions Dockerfile

This file was deleted.

83 changes: 83 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
.DEFAULT_GOAL := help

# ---------------------------------------------------------------------------
# Config
# ---------------------------------------------------------------------------
COMPOSE := docker compose
COMPOSE_TEST := docker compose -f docker-compose.test.yml
TEST_DB_URL := postgresql+asyncpg://postgres:postgres@localhost:5433/app_test

.PHONY: help install up down logs backend frontend migrate seed db-reset \
test test-backend test-frontend test-up test-down

help: ## Show this help
@grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | \
awk 'BEGIN {FS = ":.*?## "}; {printf " \033[36m%-16s\033[0m %s\n", $$1, $$2}'

# ---------------------------------------------------------------------------
# Setup
# ---------------------------------------------------------------------------
install: ## Install backend (uv) and frontend (npm) dependencies
cd backend && uv sync
cd frontend && npm install

# ---------------------------------------------------------------------------
# Run — full stack
# ---------------------------------------------------------------------------
up: ## Run the whole stack (db, minio, backend, frontend) via docker-compose
$(COMPOSE) up --build

down: ## Stop and remove the stack
$(COMPOSE) down

logs: ## Tail logs of the running stack
$(COMPOSE) logs -f

# ---------------------------------------------------------------------------
# Run — individual components (local, no full compose)
# ---------------------------------------------------------------------------
backend: ## Run the FastAPI backend locally (auto-starts db + minio, migrates, then uvicorn)
$(COMPOSE) up -d --wait db
@# Start a local MinIO unless a real/external S3 is configured. A custom
@# S3_ENDPOINT_URL that isn't localhost/minio means a remote S3 — skip it.
@endpoint="$${S3_ENDPOINT_URL:-$$(grep -E '^S3_ENDPOINT_URL=' backend/.env 2>/dev/null | cut -d= -f2-)}"; \
case "$$endpoint" in \
""|*localhost*|*127.0.0.1*|*minio*) \
echo "→ starting local MinIO"; $(COMPOSE) up -d --wait minio ;; \
*) echo "→ external S3 configured ($$endpoint) — skipping MinIO" ;; \
esac
cd backend && uv run alembic upgrade head
cd backend && uv run uvicorn app.main:app --reload --port 8000

frontend: ## Run the Vite dev server locally
cd frontend && npm run dev

migrate: ## Apply Alembic migrations to the dev database
cd backend && uv run alembic upgrade head

seed: ## Seed the dev DB with demo conversations + artifacts (figures/CSVs on S3) + plain threads. Needs MinIO up. Use ARGS=--reset to wipe first.
cd backend && uv run python -m scripts.seed $(ARGS)

db-reset: ## Wipe the dev DB clean (drop all tables, re-run migrations)
cd backend && uv run alembic downgrade base
cd backend && uv run alembic upgrade head

# ---------------------------------------------------------------------------
# Tests
# ---------------------------------------------------------------------------
test: test-up ## Run all tests (spins up test db/minio, then tears down)
@$(MAKE) test-backend || ($(MAKE) test-down && exit 1)
@$(MAKE) test-frontend || ($(MAKE) test-down && exit 1)
@$(MAKE) test-down

test-up: ## Start the ephemeral test db + minio and wait until healthy
$(COMPOSE_TEST) up -d --wait

test-down: ## Stop the ephemeral test services
$(COMPOSE_TEST) down -v

test-backend: ## Run backend tests (pytest) against the test db
cd backend && DATABASE_URL=$(TEST_DB_URL) uv run pytest -q

test-frontend: ## Run frontend tests (vitest)
cd frontend && npm run test
175 changes: 93 additions & 82 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,113 +1,124 @@
# Case Technique — Développeur Full Stack
# Data Analysis Agent — Web App

## Contexte
A web app around a data-analysis agent: ask questions about your CSV datasets and
watch the agent work in real time — its reasoning, the **SQL** it runs (DuckDB),
the **Plotly** charts and CSV tables it generates, and the final answer — all
streamed live. Built with [PydanticAI](https://ai.pydantic.dev/), FastAPI, and React.

Tu reçois un **agent d'analyse de données** qui fonctionne en mode CLI (terminal).
## Stack

L'agent peut :
- **Backend** — FastAPI, async SQLAlchemy + Alembic (Postgres), S3/MinIO for files,
the agent (PydanticAI) under `backend/app/services/agent/`.
- **Frontend** — React + Vite + Tailwind, streaming chat over SSE, interactive
Plotly figures, Markdown answers.
- **Streaming** — Server-Sent Events; the agent's reasoning, tool calls, figures
and answer are emitted as structured events.

- Répondre à des questions sur des données en générant du **SQL** (via DuckDB)
- Créer des **visualisations** avec Plotly
- Expliquer son **raisonnement** (balises `<thinking>`)
- Enchaîner les étapes automatiquement via des **tool calls**
## Run it locally (Docker)

L'agent est construit avec [PydanticAI](https://ai.pydantic.dev/).
You only need Docker and an Anthropic API key.

---
1. **Set the configuration.** Two `.env` files, copied from the examples:

## Objectif
```bash
cp .env.example .env # Postgres credentials (compose-level)
cp backend/.env.example backend/.env # backend config + your API key
```

**Transformer cet agent CLI en une application web complète.**
Then edit **`backend/.env`** and set your key:

L'utilisateur doit pouvoir poser des questions dans une interface web et voir en temps réel :
```dotenv
ANTHROPIC_API_KEY=sk-ant-...
# MODEL is optional; defaults to a fast Claude model:
MODEL=anthropic:claude-haiku-4-5-20251001
```

1. Le **raisonnement** de l'agent (thinking) — affiché progressivement
2. Les **appels d'outils** (tool calls) — nom, arguments, résultat
3. Les **visualisations** Plotly / tableaux de données
4. La **réponse finale** de l'agent
That's the only value you must provide. The database and MinIO settings in the
examples already match what docker-compose wires up.

---
2. **Start everything:**

## Ce qui est fourni
```bash
docker compose up --build
```

```
case_fullstack/
├── agent/
│ ├── agent.py # Création de l'agent PydanticAI
│ ├── context.py # Contexte injecté dans les tools
│ ├── prompt.py # System prompt
│ └── tools/
│ ├── query_data.py # Exécution SQL via DuckDB
│ └── visualize.py # Création de visualisations Plotly
├── data/ # Fichiers CSV (tes données de test)
├── output/ # Visualisations générées
├── main.py # Script CLI de démonstration
├── Dockerfile
├── docker-compose.yml
├── requirements.txt
├── .env.example
└── README.md
```

---
This brings up Postgres, MinIO, the backend (which auto-runs migrations), and
the frontend dev server. Wait until the backend logs `Application startup complete`.

## Setup
3. **Open the app:** <http://localhost:5173>

```bash
# 1. Configurer la clé API
cp .env.example .env
# Éditer .env avec ta clé API
- Frontend: <http://localhost:5173>
- Backend API + docs: <http://localhost:8000/api/health>, <http://localhost:8000/docs>
- MinIO console: <http://localhost:9001> (`minioadmin` / `minioadmin`)

# 2. Ajouter des fichiers CSV dans data/
### Get some data in

# 3. Lancer le CLI via Docker
docker compose run --rm agent
```
Upload a CSV from the **Datasets** panel in the UI, then ask a question.

> Le volume `data/` est monté dans le container — tu peux ajouter/modifier des CSV sans rebuild.
> Les visualisations générées sont dans `output/`.
Or seed a few ready-made demo conversations (with reasoning, SQL, charts and
generated CSVs) plus sample datasets:

---
```bash
docker compose exec backend uv run python -m scripts.seed --reset
```

## Ce qui est attendu
## Configuration reference

### Minimum requis
`backend/.env` — backend + agent settings:

- [ ] **Backend API** avec endpoint de streaming (SSE ou WebSocket)
- [ ] **Frontend web** avec :
- [ ] Champ texte pour poser des questions
- [ ] Affichage **streaming** du thinking (collapsible/dépliable)
- [ ] Affichage des **tool calls** (nom de l'outil, arguments, résultat)
- [ ] Rendu des **visualisations Plotly** (graphiques interactifs)
- [ ] Rendu des **tableaux** de données
- [ ] **Code propre** et structuré
| Variable | Purpose | Default |
| ------------------- | ---------------------------------------------------- | ---------------------------------------- |
| `ANTHROPIC_API_KEY` | **Required.** Your Anthropic API key. | — |
| `MODEL` | PydanticAI model id. | `anthropic:claude-haiku-4-5-20251001` |
| `POSTGRES_*` | DB connection parts (compose overrides with a DSN). | local Postgres |
| `S3_*` | Object storage (compose overrides to the MinIO svc). | local MinIO |

---
`.env` (repo root) — compose-level Postgres credentials shared by the `db` service
and the backend's `DATABASE_URL`.

## Stack technique
## Local development (without Docker)

- **Backend** : FastAPI
- **Frontend** : Libre React
- **Streaming** : SSE ou WebSocket (à ton choix)
Requires [uv](https://docs.astral.sh/uv/) and Node 20+. The `Makefile` wraps the
common tasks; Postgres and MinIO still run via Docker.

---
```bash
make install # backend deps (uv) + frontend deps (npm)
make backend # starts db + minio, migrates, runs uvicorn on :8000
make frontend # Vite dev server on :5173
make seed # demo data (needs MinIO up; ARGS=--reset to wipe)
make test # backend (pytest) + frontend (vitest)
```

## Critères d'évaluation
Run `make help` for the full list.

| Critère | Description |
|---------|-------------|
| **Fonctionnalité** | Le streaming fonctionne, le thinking s'affiche en temps réel, les tool calls sont visibles, les visualisations s'affichent |
| **Code** | Propre, structuré, lisible, bien découpé |
| **UX** | L'expérience utilisateur est fluide et intuitive |
| **Architecture** | Bonne séparation frontend / backend, gestion des états cohérente |
## Project layout

---
```
backend/
app/
api/ # routers: conversations (+ chat streaming), csv_files, artifacts
models/ # SQLAlchemy: conversation, message, message_artifact, csv_file
services/
agent/ # the PydanticAI agent (prompt, tools, runner, model, datasets)
chat.py # orchestration: run a turn, stream events, persist artifacts
storage.py # S3 / MinIO client
alembic/ # migrations
scripts/seed.py # demo data seeder
tests/
frontend/
src/
components/ # ChatPanel, AssistantMessage (timeline), PlotlyFigure, Markdown, …
services/ # api client (SSE), useChatStream, useMessages, …
docker-compose.yml
```

## Ressources utiles
## How a chat turn works

- [PydanticAI — Documentation](https://ai.pydantic.dev/)
- [PydanticAI — Streaming](https://ai.pydantic.dev/streaming/)
- [PydanticAI — Tools](https://ai.pydantic.dev/tools/)
- [Plotly.js — React integration](https://plotly.com/javascript/react/)
- [FastAPI — Streaming Response](https://fastapi.tiangolo.com/advanced/custom-response/#streamingresponse)
- [Server-Sent Events (SSE)](https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events)
1. `POST /api/conversations/{id}/messages` persists the user message and starts a run.
2. The agent streams **structured SSE events**: `reasoning`, `tool_call` (the SQL),
`tool_result`, `artifact` (figures inline, tables downloadable), `text`, plus a
`title` for brand-new conversations.
3. The frontend renders these live as a collapsible step timeline + interactive
chart + Markdown answer.
4. On completion the assistant message and its full artifact timeline are persisted,
so reloading a conversation reconstructs the same view.
13 changes: 0 additions & 13 deletions agent/context.py

This file was deleted.

6 changes: 6 additions & 0 deletions backend/.dockerignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
.venv
__pycache__
*.pyc
.env
.ruff_cache
.pytest_cache
Loading