Skip to main content
Glama
yash-desai-naik

Nutrition Research Assistant

README.md
# Nutrition Research Assistant

Production-grade **multi-agent Indian nutrition research assistant**: answers questions about Indian food and nutrition by combining a curated internal knowledge base, hybrid RAG, specialized agents, MCP tools, conversational memory, optional live web search, graceful failure handling, and end-to-end OpenTelemetry-compatible tracing.

```mermaid
graph TD
    Client[CLI / Swagger / Demo script] --> API[FastAPI :8000]
    API --> Sup[Agno Supervisor]
    Sup --> KA[Knowledge Agent]
    Sup --> CA[Calculator Agent]
    Sup --> DA[Document Agent]
    Sup --> WA[Health/Web Agent]
    KA --> MCP[FastMCP Server :8001/mcp]
    CA --> MCP
    DA --> MCP
    WA --> MCP
    MCP --> Chroma[ChromaDB + BM25 + RRF + Reranker]
    MCP --> Calc[Safe calculator]
    MCP --> Web[Agno DDGSTools]
    MCP --> Docs[documents.json]
    Sup --> Mem[(SQLite memory)]
    API --> OTel[OpenTelemetry + structlog]
```

**Core principle (PRD §89):** the LLM decides _what_ should happen; MCP tools perform _what must happen deterministically_. Agents never touch Chroma directly — everything flows through MCP.

## Tech Stack

| Layer             | Choice                                                       |
| ----------------- | ------------------------------------------------------------ |
| Python / packages | 3.12+ · uv (`pyproject.toml` + lock)                         |
| LLM               | DeepSeek `deepseek-v4-flash` (OpenAI-compatible)             |
| Agents            | Agno (Supervisor + 4 specialized agents)                     |
| MCP               | FastMCP server, streamable-http on `:8001/mcp`               |
| Vector DB         | ChromaDB (local, cosine)                                     |
| Embeddings        | `BAAI/bge-small-en-v1.5` (local)                             |
| Reranker          | `BAAI/bge-reranker-base` (local cross-encoder)               |
| BM25              | `rank_bm25`                                                  |
| Memory            | Agno sessions + SQLite (`data/app.db`)                       |
| API               | FastAPI + uvicorn                                            |
| Observability     | OpenTelemetry (console exporter; OTLP-swappable) + structlog |
| Web search        | Agno built-in `DDGSTools` (optional)                         |

## Repository Layout

```
corpus/         8 curated nutrition documents (frontmatter + per-food tables)
scripts/        ingest.py · rebuild_index.py · test_retrieval.py · eval_questions.json
                test_mcp_wiring.py · test_agents.py · test_memory.py · demo.py
src/
  api/          FastAPI app, routes, schemas          (chat / health / ready)
  agents/       models.py (DeepSeek) · mcp_client.py · agents.py (specialists + supervisor)
  mcp_server/   server.py + tools/{knowledge,calculator,documents,web}.py
  rag/          embeddings · chroma · bm25 · fusion (RRF) · reranker · pipeline
  memory/       SQLite storage
  services/     circuit breaker · retry
  observability tracing (OTel) · logging (structlog)
  config/       settings.py          core/ errors.py · models.py
  cli.py        REPL client
tests/          unit + API tests
```

## Quick Start

```bash
# 1. Environment
uv venv --python 3.12
uv sync

# 2. Secrets
cp .env.example .env        # set DEEPSEEK_API_KEY

# 3. Ingest the corpus (downloads bge-small-en-v1.5 on first run)
uv run python scripts/ingest.py

# 4. Start the MCP tool server  (terminal 1)
uv run python -m src.mcp_server.server

# 5. Start the API                 (terminal 2)
uv run uvicorn src.api.app:app --reload --port 8000
```

Swagger UI: <http://127.0.0.1:8000/docs> (API port is configurable via `API_PORT` in `.env` — this workspace uses 8002 because 8000 is taken)

Try it:

```bash
uv run python -m src.cli                # interactive REPL
uv run python scripts/demo.py           # scripted 5-question demo
curl -X POST http://127.0.0.1:8000/api/v1/chat \
  -H "Content-Type: application/json" \
  -d '{"session_id":"demo","message":"How much protein is in 100g cooked chickpeas?"}'
```

## API

| Endpoint            | Purpose                                                                         |
| ------------------- | ------------------------------------------------------------------------------- |
| `POST /api/v1/chat` | `{session_id, message}` → `{status, session_id, response, sources[], trace_id}` |
| `GET /health`       | `{status, mcp, chroma, llm}` — liveness, no secrets                             |
| `GET /ready`        | `{status, checks}` — alive vs ready distinction                                 |

Degraded responses carry `status: "degraded"`, a human-readable message, and the `trace_id` — never a 500 traceback.

## MCP Tools (port 8001)

| Tool                                      | Backend                                  | Notes                                            |
| ----------------------------------------- | ---------------------------------------- | ------------------------------------------------ |
| `search_knowledge(query, top_k, filters)` | hybrid RAG (dense + BM25 + RRF + rerank) | returns chunks with document_id / score / method |
| `calculate(expression)`                   | AST-whitelisted safe evaluator           | no `eval`, no code execution                     |
| `get_document(document_id)`               | ingested `documents.json`                | never fabricates ids                             |
| `search_web(query)`                       | Agno `DDGSTools`                         | optional; `WEB_SEARCH_ENABLED`                   |
| `search_health_information(query)`        | DDGSTools + domain ranking               | prioritizes WHO/ICMR/NIH/CDC                     |

## Agents

| Agent      | Tools               | Handles                                         |
| ---------- | ------------------- | ----------------------------------------------- |
| Supervisor | team of 4           | intent, routing, composition, memory            |
| Knowledge  | `search_knowledge`  | nutrition facts, comparisons, raw vs cooked     |
| Calculator | `calculate`         | serving scaling, totals                         |
| Document   | `get_document`      | document retrieval                              |
| Health/Web | web + health search | current info, general health (educational only) |

## Failure Handling

- **MCP down** → controlled `degraded` response with trace_id (no 500); circuit breaker fails fast, probes after cooldown (PRD §70/§85)
- **MCP timeout** — 10s transport read timeout; agent run bounded by `AGENT_TIMEOUT_SECONDS`
- **Chroma down** → "Knowledge retrieval temporarily unavailable" (optionally falls back to web)
- **LLM down / bad key** → `LLM_UNAVAILABLE`-style degraded message
- **Web disabled** → "Live search is currently unavailable."
- Retry policy: 2 attempts with backoff, then degrade — never endless

## Observability

Every request produces one trace: `api.request → supervisor.run → mcp.<tool> → rag.dense_search / rag.bm25 / rag.rrf / rag.reranker`, with trace_id echoed in the API response, logs, and spans. All logs are JSON (structlog) with timestamp, level, logger, trace_id, span_id. Set `OTEL_EXPORTER_OTLP_ENDPOINT` to export to Jaeger/Tempo/Collector (`uv sync --extra otel`).

## Tests & Retrieval Eval

```bash
uv run pytest -q
uv run python scripts/test_retrieval.py --rerank   # Recall@5/10, MRR, Hit@1/3 over 24 questions
```

The eval dataset (`scripts/eval_questions.json`, 24 questions across 7 categories) measures retrieval quality; the reranker lifts Hit@1/3 over raw fusion. Results land in `data/eval/results.json`.

## Definition of Done (PRD §87)

- **Core** — FastAPI runs; DeepSeek via OpenAI-compatible client; Agno agents operational; FastMCP operational; Chroma populated; 8 documents; memory works
- **MCP** — all 5 tools work and agents use MCP as the real execution path
- **RAG** — dense + BM25 + RRF + cross-encoder rerank; metadata preserved; eval exists
- **Agents** — Supervisor + 4 specialists; multi-agent composition works
- **Memory** — session ids; follow-ups resolve; persisted in SQLite
- **Observability** — trace_id per request; nested spans; MCP/RAG/LLM/errors traced
- **Failure handling** — MCP unavailable/timeout, Chroma, LLM, web all controlled; no unhandled exception reaches the user

## Troubleshooting

- **Hugging Face symlink warning** — Windows-only; set `HF_HUB_DISABLE_SYMLINKS_WARNING=1` (cosmetic)
- **Retrieval returns nothing** — Chroma empty: run `python scripts/ingest.py`
- **"tool service unavailable"** — MCP server not running (start it) or breaker cooling down (wait ~30s, or restart API)
- **Health says llm not_configured** — `DEEPSEEK_API_KEY` missing in `.env`