Contributing to why-agent
Thank you for your interest in contributing! This guide covers the development setup, testing workflow, and code quality standards.
Prerequisites
- Python 3.12+ (check
.python-version) - uv β modern Python package manager (install)
- Node.js 20+ β for the Next.js frontend (optional, only if modifying frontend)
- Git β for version control
Development Setup
1. Clone and install dependencies
git clone https://github.com/Isa-Mapo-Hackathon/why-agent.git
cd why-agent
uv sync
This installs both runtime and dev dependencies (pytest, ruff, pyright).
2. Set up environment
cp .env.example .env
Then edit .env with your secrets:
MODEL_BACKENDβ useminimaxorreplayfor local developmentMINIMAX_API_KEYβ get from MiniMax dashboardPARQUET_DIRβ defaults todata/parquetSEMANTIC_LAYER_PATHβ defaults todata/semantic_layer.yml
3. Verify setup
uv run pytest -v
Should run ~15+ tests without errors.
Running the Application
Option A: Streamlit (Python-only, simplest)
uv run streamlit run streamlit_app.py
Opens at http://localhost:8501. Uses the Streamlit UI to ask questions directly to the agent.
Option B: FastAPI + Next.js (full stack)
Terminal 1 β FastAPI backend (with hot reload):
uv run uvicorn client.backend.main:app --reload --port 8000
Backend runs at http://localhost:8000. Check health at http://localhost:8000/api/health.
Terminal 2 β Next.js frontend:
cd client/frontend
npm install # first time only
npm run dev
Frontend runs at http://localhost:3000. The Next.js dev server proxies /api/* to the FastAPI backend on port 8000 automatically.
Common Development Commands
| Task | Command |
|---|---|
| Install deps | uv sync |
| Add a dependency | uv add <package> (runtime) or uv add --dev <package> (dev) |
| Run tests | uv run pytest -v |
| Run one test file | uv run pytest tests/test_agent_smoke.py -v |
| Lint code | uv run ruff check --fix |
| Format code | uv run ruff format |
| Type check (optional) | uv run pyright |
| Run Streamlit | uv run streamlit run streamlit_app.py |
| Run FastAPI backend | uv run uvicorn client.backend.main:app --reload --port 8000 |
| Run Next.js frontend | cd client/frontend && npm run dev |
Testing
Philosophy
Tests are smoke tests, not unit tests. We verify:
- Tools run without crashing
- Output has the expected shape (JSON, dict keys, etc.)
- Error handling is recoverable
We do not mock heavily or test implementation details.
Running tests
# All tests
uv run pytest
# Single file
uv run pytest tests/test_tools.py -v
# Single test
uv run pytest tests/test_tools.py::test_inspect_schema -v
# With print output
uv run pytest -s
Adding a test
- Add a
.pyfile intests/orclient/backend/tests/ - Write a function named
test_* - Use
assertstatements - Run
uv run pytestto verify
Example:
def test_my_feature():
from agent.tools import run_sql
result = run_sql(...)
assert "rows" in result
assert isinstance(result["rows"], list)
Code Quality
Before any commit, code must pass:
uv run ruff check --fix # Fix lint errors automatically
uv run ruff format # Format to standard style
These two commands are required β CI will reject commits that don't pass.
Optional (not in CI, but recommended):
uv run pyright # Type checking (editor runs this too)
Repository Structure
why-agent/
βββ agent/ # Core agent logic
β βββ graph.py # LangGraph state machine
β βββ state.py # Pydantic state models
β βββ client.py # Multi-backend LLM client
β βββ constants.py # Named constants (backends, tool names, demo questions)
β βββ tools/ # The four tools
β β βββ inspect_schema.py
β β βββ run_sql.py
β β βββ compare_periods.py
β β βββ decompose_metric.py
β βββ prompts/ # System + critique prompts
β
βββ client/
β βββ backend/ # FastAPI server
β β βββ main.py # GET /health, POST /api/investigate
β β βββ deps.py # Dependency injection (graph instance)
β β βββ sse.py # Server-Sent Events formatting
β β βββ tests/
β βββ frontend/ # Next.js app
β βββ src/app/page.tsx # Main page
β βββ package.json
β
βββ data/
β βββ parquet/ # Dataset files (gitignored)
β βββ semantic_layer.yml # Metadata + business context
β
βββ tests/ # Python smoke tests
β βββ test_tools.py
β βββ test_client_backends.py
β βββ test_agent_smoke.py
β
βββ docs/ # Documentation
β βββ CONTRIBUTING.md # This file
β βββ RUNBOOK.md # Deployment guide
β βββ why-agent-architecture.png
β
βββ streamlit_app.py # Standalone Streamlit UI
βββ pyproject.toml # Python deps + commands
βββ docker/ # Containers
β βββ Dockerfile # Multi-stage build
β βββ entrypoint.sh # HF Spaces boot script
β βββ nginx.conf # Reverse proxy config
β βββ supervisord.conf # Process management
β
βββ README.md # Project overview + business context
Architecture Overview
βββββββββββββββββββββββββββββββββββ
β Streamlit UI β
β (streamlit_app.py) β
ββββββββββββββ¬βββββββββββββββββββββ
β
βββββββ΄βββββββ
β β
βΌ βΌ
ββββββββββββββββ ββββββββββββββββββββ
β Next.js β β FastAPI Backend β
β (client/ β β (client/backend/ β
β frontend/) β β main.py) β
ββββββββββββββββ ββββββββββ¬ββββββββββ
β
βββββββΌββββββ
β LangGraph β
β Agent β
βββββββ¬ββββββ
β
βββββββββββββββββΌββββββββββββββββ
β β β
βΌ βΌ βΌ
ββββββββββββ ββββββββββββββββ ββββββββββββ
βDuckDB β βPydantic β βLLM Clientβ
β(Parquet) β βTools Schemas β β(3 backends)
ββββββββββββ ββββββββββββββββ ββββββββββββ
Common Issues & Solutions
ModuleNotFoundError: No module named 'agent'
Solution: Make sure you're in the repo root and have run uv sync.
cd /home/ysh/dev/why-agent
uv sync
Tests fail with "No MINIMAX_API_KEY"
Solution: Use MODEL_BACKEND=replay for local testing. Replay mode doesn't call any LLM.
export MODEL_BACKEND=replay
uv run pytest
Ruff formatting conflicts with editor
Solution: Use the commands above β they're the source of truth.
uv run ruff format
uv run ruff check --fix
Next.js frontend doesn't build
Solution: Make sure Node 20+ is installed and npm install ran successfully.
node --version # should be v20+
cd client/frontend
npm install
npm run build
Coding Conventions
Per CLAUDE.md, follow these conventions:
- Sync by default β DuckDB has no async API. Use
async defonly at the LLM boundary. - Pydantic v2 β All structured data (tool inputs/outputs, state, semantic layer).
- Type annotations β Required on public functions (args and return type).
- No print() β Use
logger = logging.getLogger(__name__)in agent code. - No magic strings β Backend names, tool names, scenario IDs go in
agent/constants.py. - Tool docstrings for the LLM β Write them as if the model will read them.
Example tool:
from pydantic import BaseModel, Field
import logging
logger = logging.getLogger(__name__)
class MyToolInput(BaseModel):
query: str = Field(description="A human-readable query.")
def my_tool(args: MyToolInput) -> dict:
"""Use this tool to do X. Returns a dict with 'result' and optional 'error'."""
try:
result = ...
return {"result": result}
except Exception as exc:
logger.exception("Failed")
return {"error": str(exc), "hint": "Try Y instead"}
Deployment
Local Docker build
To test the full stack locally (frontend + backend + agent) in a container:
docker build -t why-agent:latest .
docker run -p 7860:7860 -e MODEL_BACKEND=replay why-agent:latest
Then open http://localhost:7860.
Remote push rules
The repo has two git remotes with different push policies:
| Remote | Purpose | When to push |
|---|---|---|
origin (GitHub) |
Source of truth, PRs, CI | Every commit β always push here |
space (HF Spaces) |
Deployment target | Only when opening a PR |
# Normal dev β push to GitHub only
git push origin feat/my-feature
# Deploy to HF Spaces β only when PR is ready
git push space feat/my-feature:main --force
HF Spaces triggers a full Docker rebuild on every push. Do not push to space during iteration β only when the branch is ready for demo/review and a PR is being opened.
HF Spaces environment variables
When deploying to HF Spaces, set these secrets in the Space settings:
| Variable | Value | Purpose |
|---|---|---|
MODEL_BACKEND |
replay or minimax |
LLM backend; use replay to avoid API costs |
MINIMAX_API_KEY |
(API key) | Required only if MODEL_BACKEND=minimax |
HF_DATASET_ID |
(optional) | Dataset repo ID to auto-download Parquet files on boot |
PARQUET_DIR |
/app/data/parquet |
Path inside container (do not change) |
SEMANTIC_LAYER_PATH |
/app/data/semantic_layer.yml |
Path inside container (do not change) |
Note: Paths in the container must use /app/ prefix, not relative paths.
HF Spaces deployment procedure
Quick start
Create a new Space on huggingface.co/spaces:
- Owner: your username
- Space name:
why-agent(or any name) - License: MIT
- Docker template (or blank)
Link the repo:
cd /path/to/why-agent git remote add space https://huggingface.co/spaces/{username}/{space-name}Push to deploy (only when ready):
git push space feat/my-feature:main --forceSet secrets in the Space UI β Settings β Repository secrets:
MINIMAX_API_KEY(if using MiniMax backend)HF_DATASET_ID(optional; see below)
How the build works
- HF Spaces detects the
Dockerfilein the repo root - Builds the image (takes ~5β10 minutes the first time)
- Runs the container on port 7860
- The
entrypoint.shscript starts nginx, backend, and frontend via supervisord
Auto-downloading Parquet data
If you set HF_DATASET_ID=ysh99226/why-agent-data, the entrypoint will:
- Check if
/app/data/parquetis empty - Run
hf downloadto fetch the dataset - Timeout after 120 seconds and fall back to
MODEL_BACKEND=replay
The hf command (from huggingface-hub package) replaces the deprecated huggingface-cli.
Git workflow for deployment
Do NOT push to HF Spaces during development.
Work on a feature branch:
git checkout -b feat/my-feature git push origin feat/my-featureOpen a PR on GitHub when ready.
Deploy to HF Spaces only when the PR is ready to demo:
git push space main:main --forceOr, if the feature branch is the one being demoed (before merge):
git push space feat/my-feature:main --force
Why --force? HF Spaces doesn't have a traditional git history. Using --force ensures the Space always reflects the exact commit you push, even if the branch history differs from the origin.
Docker build errors & fixes
"replays/ directory not found" or "missing JSON files"
Cause: The Dockerfile expects replays/ to exist and contain at least one .json file for MODEL_BACKEND=replay to work.
Fix:
# Create dummy replay if needed
mkdir -p replays
echo '{"scenario": "demo"}' > replays/demo.json
git add replays/demo.json
git commit -m "chore: add demo replay"
Then rebuild the Docker image.
"SEMANTIC_LAYER_PATH not found" or "semantic_layer.yml missing"
Cause: The Dockerfile copies data/semantic_layer_6w.yml but the file doesn't exist.
Fix:
# Check the actual filename
ls -la data/semantic_layer*
# If using a different name, update the Dockerfile COPY line
COPY data/semantic_layer_6w.yml /app/data/semantic_layer.yml
Or, if you're using a different semantic layer file:
COPY data/YOUR_SEMANTIC_LAYER.yml /app/data/semantic_layer.yml
"supervisord can't find environment variables" or "MODEL_BACKEND not set in child processes"
Cause: Environment variables set in ENV commands are not automatically passed to supervisord child processes.
Fix: The docker/supervisord.conf must explicitly read env vars via environment= lines:
[program:backend]
command=/app/.venv/bin/uvicorn ...
environment=PYTHONUNBUFFERED="1",MODEL_BACKEND="replay"
Or pass them in the command itself. Rebuild the image after fixing supervisord.conf.
"huggingface-cli: command not found"
Cause: The old huggingface-cli tool is deprecated. The project uses the newer hf command from huggingface-hub package.
Fix: The Dockerfile includes huggingface-hub in pyproject.toml. The entrypoint.sh script uses hf download, which is the correct command.
If the entrypoint still fails:
# Verify hf is installed
docker run -it why-agent:latest /app/.venv/bin/hf --version
# If missing, add to pyproject.toml
uv add huggingface-hub
"next: command not found" or "Node.js frontend doesn't start"
Cause: The Next.js build failed, or the server.js file is missing.
Fix:
- Check the build log for
npm run builderrors - Ensure
client/frontend/package.jsonexists and has a valid build script - Rebuild the Docker image:
docker build --no-cache -t why-agent:latest .
"nginx bind: address already in use"
Cause: Port 7860 or 80 is already bound on your machine.
Fix (local testing):
docker run -p 8080:7860 -e MODEL_BACKEND=replay why-agent:latest
# Now visit http://localhost:8080
On HF Spaces, port 7860 is reserved and managed by the platform β no action needed.
"ModuleNotFoundError: No module named 'agent'"
Cause: The Python path is not set correctly in the container.
Fix: The Dockerfile sets ENV PYTHONPATH=/app, which should work. If it doesn't:
- Verify
COPY agent/ /app/agent/in the Dockerfile - Check that the
backendprogram in supervisord uses the full venv path:/app/.venv/bin/uvicorn
"API route returns 404" or "Frontend can't reach backend"
Cause: nginx is not configured to reverse-proxy to the backend on 127.0.0.1:8000.
Fix: Check docker/nginx.conf:
location /api/ {
proxy_pass http://127.0.0.1:8000;
proxy_set_header X-Real-IP $remote_addr;
...
}
Rebuild after fixing the config:
docker build --no-cache -t why-agent:latest .
Health check & monitoring
Verify all services are running
# Inside the container or from host
curl http://localhost:7860/api/health
# Expected: {"ok":true}
curl http://localhost:7860/
# Expected: HTML (Next.js frontend)
curl -X POST http://localhost:7860/api/investigate \
-H "Content-Type: application/json" \
-d '{"question":"Why did revenue go up?"}'
# Expected: Server-Sent Event stream
Check logs in HF Spaces
Click "Logs" in the top right of the Space UI. The logs show:
- nginx startup
- backend startup (uvicorn)
- frontend startup (Node.js)
- Any errors from the agent or tools
Common troubleshooting flows
The frontend loads but the backend is down:
- Check Space logs (UI β Logs)
- Verify
PYTHONPATH=/appis set in the Dockerfile - Verify
supervisord.confhas the correct backend command - Rebuild without cache and push:
git push space feat/my-feature:main --force
The API returns 500 errors but logs show nothing:
- The agent code may have an unhandled exception
- Check the agent's error handling in
agent/graph.py - Verify the semantic layer file exists at
/app/data/semantic_layer.yml - Test locally:
docker run -e MODEL_BACKEND=replay why-agent:latest curl http://localhost:7860/api/health
Parquet data auto-download timed out, but I want to retry:
The entrypoint waits 120 seconds for the HF dataset download, then falls back to MODEL_BACKEND=replay. If you want a fresh download:
- Manually clear the parquet directory in the Space (if you have SSH access)
- Or, restart the Space (UI β Settings β Restart)
- The entrypoint will retry on next boot
I pushed to the Space but the changes didn't appear:
Verify you pushed to the correct branch (should push
*:main):git push space feat/my-feature:main --forceHF Spaces can take 5β10 minutes to rebuild. Wait and refresh after 2 minutes.
If the Space still doesn't update:
- Click "Restart" in the Space UI
- Or delete and recreate the Space
Reporting Issues
If you find a bug or have a feature request:
- Check existing issues in GitHub
- Provide a minimal reproduction (code snippet + data)
- Include your environment (Python version, OS, backend)
Getting Help
- CLAUDE.md β Implementation decisions and locked constraints
- README.md β Business context and architecture
- Agent code β Read
agent/graph.pyto understand the loop; readagent/tools/to see tool contracts - LangGraph docs β https://langchain-ai.github.io/langgraph/
- Pydantic docs β https://docs.pydantic.dev/
Last updated: 2026-05-07