summaryrefslogtreecommitdiff
path: root/CLAUDE.md
blob: b2f68b7251db71b292e951b9626a17f64cfee207 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
# CLAUDE.md

This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.

## Commands

```bash
# Install dependencies
pip install -r requirements.txt

# Run dev server (auto-reload)
uvicorn main:app --reload

# Run on a specific port
uvicorn main:app --reload --port 8080

# Explore the API interactively
open http://localhost:8000/docs
```

There are no tests. The SQLite database (`chef.db`) is auto-created on first startup via `Base.metadata.create_all()` in the lifespan handler — no migrations needed for new installs. The lifespan handler also runs a one-time `ALTER TABLE menu_plans ADD COLUMN notes TEXT` migration for existing databases that predate that column.

## Architecture

Single-process FastAPI app. All state lives in SQLite. Ollama runs as a separate local process on port 11434.

**Request flow for AI endpoints:**
1. Router calls `pantry_service.build_pantry_context(db)` to snapshot the current pantry + recent meal history into a plain dict
2. That dict is passed to an `ai_service` function which builds a prompt and calls Ollama synchronously via `run_in_executor` (keeps the async event loop unblocked during the 15–120s generation)
3. Structured AI endpoints (menu, grocery, swap) use `format="json"` and return parsed dicts. The chat endpoint uses plain text — no JSON parsing.
4. The router saves AI output into the DB and returns both the DB record and the raw AI response to the frontend

**Route registration order matters:** `app.include_router(...)` calls happen before `app.mount("/", StaticFiles(...))`. Reversing this breaks all API routes — the static catch-all intercepts them first.

**JSON columns:** `recipes.ingredients`, `menu_plans.plan`, and `grocery_lists.items` are stored as JSON strings in `Text` columns. Always `json.dumps()` before saving and `json.loads()` before using.

**`menu_plans.plan` structure:** A flat JSON array of recipe IDs — `[1, 4, 7, ...]`. The `POST /api/menus/generate` response also includes `recipes` with full recipe details (name, ingredients, instructions) — the frontend uses this for immediate display. On page reload only the plan IDs are available, so `GET /api/recipes` is needed to hydrate names and details.

**`meal_ingredients.ingredient_name` is denormalized** — it stores a copy of the name string rather than a FK to `ingredients`. This preserves meal history when pantry items are deleted.

**`grocery.py` exports two routers:** `router` (prefix `/api/grocery`) and `ai_router` (prefix `/api/ai`). Both are included in `main.py`.

**`_current_monday()` helper** is defined in both `routers/menus.py` and `routers/grocery.py`. It returns the ISO date of the current week's Monday and is used by every endpoint that scopes data to the current week.

**`ai_service._chat_sync(messages, json_format=True)`** is the single sync Ollama call. Pass `json_format=False` for the chat endpoint which returns plain text. All async public functions call `run_in_executor` wrapping this to avoid blocking the event loop.

**Config** is loaded from `.env` via `pydantic-settings`. Key vars:

| Var | Default | Purpose |
|-----|---------|---------|
| `OLLAMA_HOST` | `http://localhost:11434` | Ollama server URL |
| `MODEL_NAME` | — | Which Ollama model to use (e.g. `llama3.1`, `mistral`) |
| `DATABASE_URL` | `sqlite:///./chef.db` | SQLAlchemy DB URL |
| `OLLAMA_TIMEOUT` | `120` | Seconds before Ollama call times out |
| `SYSTEM_PROMPT` | *(see config.py)* | System prompt prepended to all structured AI calls |

Change `MODEL_NAME` to switch models. Edit `SYSTEM_PROMPT` to change the AI's persona and priorities for menu/grocery/swap generation (does not affect the chat tab's system prompt, which is built dynamically with kitchen context).