docs: Sprint 13 — F9-lite (Ollama Cloud free-text plan synthesis) across all 6 running docs
CI / backend (pytest + alembic) (push) Has been cancelled
CI / frontend (build) (push) Has been cancelled

Sprint 13 (commit bae9403) splits the Sprint 11 "Generate Meal
Plan" CTA into a 2-step modal: "Use the recipe library" (default,
Sprint 11 unchanged) or "Ask the LLM" (new). The LLM path POSTs
to /api/llm/plan; the backend calls kimi-k2.6:cloud on ollama.com,
parses the LLM’s JSON picks, creates a fresh plan, fills the
LLM’s picks, and falls through to the Sprint 6+ fillEmptySlots
pattern for the slots the LLM didn’t cover. No pre-existing WIP
files touched.

This commit updates the 6 running docs that track the sprint:

- .agent/plan.md — Sprint 13 section (S13.1-S13.4) added.
- .agent/context.md — Sprint 13 (D1-D9, Q1-Q3) added; file:line
  references; key takeaways.
- Review/sprint13-verification.md — new file: 3-step browser
  smoke + 4 API curls + a11y check + 6-risk table + future
  work section.
- Review/ui-nielsen-audit.md — Sprint 13 status block (T7.1-T7.3)
  at the top, after the Sprint 12 block.
- fix-ui-audit.md — Sprint 13 section (T7.1-T7.5) added after
  the Sprint 12 section.
- Review/handoff-ui-audit.md — Batch I added to the deploy
  instructions; Sprint 13 section added after Sprint 12; TL;DR
  table row 13 added; Last-updated footer updated.
- docs/HANDOFF.md — Sprint 13 section added after the Sprint 12
  section, with a path-forward paragraph for F9-full.

All 6 docs now reflect Sprint 13. §Future backlog remaining:
F9-full (local Ollama model pull on the host) — opt-in based on
cloud-billing feedback. _ask_llm is the single seam: F9-full only
needs to swap the URL + model name.
This commit is contained in:
2026-06-05 16:59:20 -07:00
parent bae94037f3
commit 8cb4d4198c
7 changed files with 361 additions and 6 deletions
+61
View File
@@ -408,3 +408,64 @@ The user can browse ~150 local recipes on `/recipes` (admin seeds them) but has
- `frontend/src/pages/Recipes.tsx:77-81``handleSearch` debounce (reused for the web-search branch) - `frontend/src/pages/Recipes.tsx:77-81``handleSearch` debounce (reused for the web-search branch)
- `Review/sprint12-verification.md` — new file (deploy + 4-step browser smoke + 2 API curls + quota test + a11y check) - `Review/sprint12-verification.md` — new file (deploy + 4-step browser smoke + 2 API curls + quota test + a11y check)
---
# Context — Sprint 13 (F9-lite Ollama Cloud plan synthesis)
## Why Sprint 13 exists
User direction 2026-06-05: "Proceed." F9-lite reuses the pre-existing `OLLAMA_*` config (`backend/app/config.py:36-38: OLLAMA_BASE_URL=https://ollama.com/v1, OLLAMA_API_KEY, OLLAMA_MODEL=kimi-k2.6:cloud`). Avoids the local model pull (F9-full would be 4 GB on disk + a separate `ollama serve` process). Cloud LLM — operator's existing OLLAMA billing applies per call.
The Sprint 11 "Generate Meal Plan" CTA was library-only. Sprint 13 splits it into a 2-step modal: "Use the recipe library" (Sprint 11 unchanged) or "Ask the LLM" (new). The LLM path lets the user describe what they want for the week ("Italian-inspired, vegetarian", "easy weeknight dinners, no fish") and uses kimi-k2.6:cloud on ollama.com to pick meals from the local library.
## Decisions (locked in for Sprint 13)
- **D1. Reuse the pre-existing `llm_matcher._ask_ollama` call pattern.** `backend/app/services/llm_matcher.py:97-144` already implements the exact `POST ${OLLAMA_BASE_URL}/chat/completions` + `Authorization: Bearer ${OLLAMA_API_KEY}` + `model: settings.OLLAMA_MODEL` + `max_tokens` + `temperature: 0` + `re.sub` for `<think>` blocks pattern. Sprint 13's `_ask_llm` helper mirrors it (same call, same headers, same strip). The only difference: `max_tokens=800` (vs `500`) for kimi-k2's reasoning headroom — 21 picks + per-pick UUID validation can run long.
- **D2. 200-recipe cap on the library sent to the LLM.** Larger libraries would exceed the prompt token budget for kimi-k2.6. The cap is alphabetical-by-name, deterministic. The library fill on the back end iterates the full library regardless of the cap.
- **D3. Tolerant JSON parsing.** The LLM may return valid JSON, JSON in markdown code fences, JSON with trailing commentary, or pure prose. `_parse_picks` handles all four: tries the regex for ` ```json ... ``` ` first, then finds the first `[` and the matching `]`, then `json.loads`. On any failure, returns `[]` — the library fill takes over. The user never sees a crash.
- **D4. Per-pick validation.** Each entry must have a valid `day_of_week` (1-7), a known `meal_type` (breakfast/lunch/dinner), AND a `recipe_id` that's in the local library. Invalid entries are dropped silently. The library fill then covers the dropped slots.
- **D5. Library fill = Sprint 6+ pattern, re-implemented inline.** The original `meals.fillEmptySlots` is a public HTTP endpoint. Calling it from `/api/llm/plan` would be a self-HTTP-call (works but ugly). Re-implementing the logic inline (read library, skip already-used recipe_ids, pick the first remaining) keeps the endpoint self-contained. Same partial-success semantics.
- **D6. Modal is inline in `Dashboard.tsx`, not a separate component.** Depends on 4 local states + 3 handlers. Extracting to a separate component would require prop-drilling or context. ~50 lines of JSX, easy to read inline.
- **D7. 60s LLM timeout.** kimi-k2.6:cloud typical latency is 5-15s for a 21-pick request. 60s is generous. On timeout, the user sees a success toast with `picked_count: 0` — same UX as if the LLM returned 0 picks.
- **D8. `OLLLAMA_CLOUD` costs apply.** The endpoint is public + `require_session`-gated (no public abuse). A future sprint could add a per-day rate limit if the operator's OLLAMA billing becomes painful. Out of scope for the initial ship.
- **D9. Default mode = "Use the recipe library".** The LLM mode is opt-in. Users who don't toggle the radio get the Sprint 11 flow (free, fast, no API call). Users who want the LLM experience explicitly opt in by selecting the radio + typing a prompt.
## Open questions to surface to the user, not to assume
- **Q1. Should the LLM-picked items be visually distinguished from the library-filled items in the plan grid?** Default: no (out of scope). The Sprint 8 vote + Sprint 10 deny buttons work on any item, LLM-picked or not. A future sprint could add a small "✨ LLM pick" badge.
- **Q2. Should the prompt modal remember the user's last prompt + last mode across sessions?** Default: no (would require `localStorage`). A future sprint could persist `{promptMode, lastPrompt}` to `mealplanner:*` localStorage keys.
- **Q3. Should the LLM call be streaming (show a "thinking…" indicator that updates as the model generates)?** Default: no. The Ollama streaming API is a different request shape (`stream: true` + NDJSON parsing). Out of scope for the initial ship. A future sprint could add streaming with a "Asking LLM… (so far: <partial JSON>)" toast.
## Sprint 13 verification gate
- `cd frontend && npm run build` → green (tsc 0 errors, vite 0 errors)
- Backend AST clean on all 3 changed files
- Browser smoke (3 steps) on `http://100.108.208.56:8082/` per `Review/sprint13-verification.md`
- 4 API curls: happy path + OLLAMA_API_KEY unset (503) + empty prompt (422) + duplicate week (400)
- No regression in Sprints 1-12
## Sprint 13 — does NOT touch
- The OnboardingTour (Sprint 9) — unchanged. The tour's first step is the Dashboard's Weekly Overview card; the modal renders on top via `z-50` + `bg-black/40` scrim.
- The NeverSuggestButton (Sprint 10) — unchanged.
- The handleGenerateFirstPlan (Sprint 11) — extracted into `generateFromLibrary` (identical body) + `generateFromLLM` (new).
- The web-search toggle + import flow (Sprint 12) — unchanged. Sprint 12's `recipes.search` + `recipes.importRecipe` are independent of the LLM synthesis.
- The pre-existing WIP `backend/app/api/recipes.py` / `schemas/recipe.py` / `nginx/nginx.conf` — untouched.
- The existing `RecipeDiscoveryService` at `backend/app/services/recipe_discovery.py` — unchanged. Sprint 13 uses the pre-existing `OLLAMA_*` config, not the service.
- The existing `llm_matcher.py` — unchanged. Sprint 13's `_ask_llm` helper mirrors `_ask_ollama` but is a separate function (different `max_tokens`, different prompt shape).
## Key file:line references (Sprint 13)
- `backend/app/services/llm_matcher.py:97-144``_ask_ollama` (the call pattern Sprint 13 mirrors)
- `backend/app/services/recipe_enrichment.py:25-106` — parallel LLM helper (different prompt shape; not reused)
- `backend/app/config.py:36-38` — OLLAMA config (reused as-is)
- `backend/app/api/llm_plan.py` (NEW, ~280 lines) — Sprint 13 endpoint + helpers
- `backend/app/models/__init__.py:200-211``MealPlan` model (target for the create)
- `backend/app/models/__init__.py:223-224``MealPlanItemBase` (target for the items)
- `backend/app/schemas/__init__.py:255-265``LLMPlanRequest` + `LLMPlanResponse` (S13.1 schemas)
- `backend/app/main.py:65-66``llm_plan_api.router` registration
- `frontend/src/api/index.ts:124-128``llm.plan` client method
- `frontend/src/pages/Dashboard.tsx:399-487``handlePromptSubmit` + `generateFromLibrary` + `generateFromLLM` (the 3 Sprint 13 handlers)
- `frontend/src/pages/Dashboard.tsx:775-870``renderPromptModal` (the Sprint 13 modal)
- `Review/sprint13-verification.md` — new file (deploy + 3-step browser smoke + 4 API curls + a11y check + 6-risk table)
+77
View File
@@ -424,3 +424,80 @@ User reported post-deploy: "The tour window looks great, but Clicking the X nor
- **Auto-enriching existing recipes** with macros (would require `nutrition` endpoint = 1 pt per recipe; out of free quota). - **Auto-enriching existing recipes** with macros (would require `nutrition` endpoint = 1 pt per recipe; out of free quota).
- **Modifying the pre-existing WIP** `backend/app/api/recipes.py` / `schemas/recipe.py` / `nginx/nginx.conf` — untouched. - **Modifying the pre-existing WIP** `backend/app/api/recipes.py` / `schemas/recipe.py` / `nginx/nginx.conf` — untouched.
- F8 Spoonacular + F9 Ollama + dead `Generate Meal Plan` CTA — separate. - F8 Spoonacular + F9 Ollama + dead `Generate Meal Plan` CTA — separate.
---
## Sprint 13 — F9-lite (Ollama Cloud free-text plan synthesis) — DRAFTED, awaiting user approval
**User direction (2026-06-05):** "Proceed." F9-lite reuses the pre-existing `OLLAMA_*` config (config.py:36-38: `OLLAMA_BASE_URL=https://ollama.com/v1`, `OLLAMA_API_KEY`, `OLLAMA_MODEL=kimi-k2.6:cloud`). Avoids the local model pull (F9-full would be 4 GB on disk + a separate uvicorn process). Cloud LLM — costs apply per call (operator's existing OLLAMA billing).
**Pre-existing infrastructure to reuse (not recreate):**
- `backend/app/services/llm_matcher.py:97-144``_ask_ollama(ingredient_name, candidates)` helper. The exact call pattern Sprint 13 mirrors: `POST ${OLLAMA_BASE_URL}/chat/completions` with `Authorization: Bearer ${OLLAMA_API_KEY}`, `model: settings.OLLAMA_MODEL`, `max_tokens: 500, temperature: 0`, parse `choices[0].message.content`, strip `` blocks.
- `backend/app/services/recipe_enrichment.py` — parallel LLM helper for recipes (different prompt shape; not reused).
- `backend/app/config.py:36-38` — OLLAMA config.
**Pre-existing WIP (NOT touched):** same as Sprint 12.
### S13.1 — Backend: `POST /api/llm/plan` (public, webui-facing)
- [ ] **NEW** `backend/app/api/llm_plan.py` — single endpoint + a thin `_ask_llm(prompt)` helper. The endpoint:
1. Validates the request body (`prompt: str` 1-500 chars, `week_start: date`).
2. Reads the local recipe library (`Recipe` table, filtered by `family_profile_id`); serializes a compact list `{id, name, cuisine_tags, dietary_tags, protein_type, total_time_minutes, dietary preferences}`.
3. Builds a prompt: "You are planning a 7-day meal plan (Mon-Sun). The user wants: '<prompt>'. Pick up to 21 meals (7 breakfasts + 7 lunches + 7 dinners) from the recipe library. Return JSON: `[{"day_of_week": 1-7, "meal_type": "breakfast|lunch|dinner", "recipe_id": "<uuid>"}]`. If a slot has no good match, omit it. Use only recipe_ids from the list. Reply with JSON only — no commentary."
4. Calls `_ask_llm(prompt)` (mirrors `_ask_ollama` from `llm_matcher.py:97-144`).
5. Parses the JSON response (try `json.loads`, fall back to `re.search(r"\[.*\]", content)` to handle markdown code fences).
6. Validates each entry: `recipe_id` is a UUID, `day_of_week` in 1-7, `meal_type` in {breakfast, lunch, dinner}. Drop invalid entries.
7. Creates an empty plan via `meals.create` (Sprint 6+ endpoint), then bulk-inserts the LLM-picked items + calls `fillEmptySlots` for the slots the LLM didn't cover.
8. Returns `{plan_id, picked_count, filled_count, failed_count, reasoning: <LLM raw text if non-empty>}`.
- [ ] **MODIFIED** `backend/app/schemas/__init__.py` — add `LLMPlanRequest` + `LLMPlanResponse` Pydantic models.
- [ ] **MODIFIED** `backend/app/main.py` — register the new router at `/api/llm`.
- [ ] 503 with clear `detail: "OLLAMA_API_KEY not configured"` when env var unset.
- [ ] 422 on empty / oversized prompt.
- [ ] Cap on library size sent to the LLM: 200 recipes max (alphabetical by name). Larger libraries would exceed prompt tokens.
### S13.2 — Frontend: free-text prompt in the Sprint 11 flow
- [ ] **MODIFIED** `frontend/src/pages/Dashboard.tsx` — turn the Sprint 11 `handleGenerateFirstPlan` into a 2-step:
1. New `MealPlanPromptModal` component (inline in `Dashboard.tsx`, ~40 lines, reuse `Card`/`Button`/`Input` from `components/ui/*`): a small modal with a textarea (max 500 chars, char counter) + two radio options: "Use the recipe library" (default) and "Ask the LLM". On submit, calls one of two API methods.
2. **Library path** (default): keep Sprint 11's `meals.create` + `fillEmptySlots` exactly as-is.
3. **LLM path** (new): `mealPlannerApi.llm.plan({prompt, week_start})`. On success, invalidate `['mealPlan', weekStart]` and toast `"Planned N meals (LLM picked N, library filled the rest)"`.
- [ ] **MODIFIED** `frontend/src/api/index.ts` — add `llm.plan(data)`.
- [ ] A11y: modal has `role="dialog"`, `aria-modal="true"`, focus trapped on the textarea on open, restored to the CTA button on close. Esc dismisses. Tab cycles within the modal.
### S13.3 — Verify
- [ ] `cd frontend && npm run build` → green (tsc 0 errors, vite 0 errors).
- [ ] Backend AST clean.
- [ ] Manual API smoke:
- `curl -X POST /api/llm/plan -d '{"prompt": "easy weeknight dinners, no fish", "week_start": "2026-06-08"}'` → 200 with `{plan_id, picked_count: 7+, filled_count: 14-, ...}`. (Requires `OLLAMA_API_KEY` set on the host.)
- With `OLLAMA_API_KEY` unset → 503.
- Empty prompt → 422.
- [ ] Manual UI smoke (3 steps):
1. Land on `/` with no plan. Click "Generate Meal Plan". Modal opens with the textarea + the two radio options.
2. Type "Italian-inspired, vegetarian" + select "Ask the LLM" + click submit. Confirm: button label flips to "Asking LLM…", modal shows a small spinner, ~5-15s later the modal closes, plan grid renders, toast shows the picked/filled split.
3. Refresh the page. Confirm the plan persists.
- [ ] No regression in Sprints 1-12.
### S13.4 — Docs (all 6 running docs updated)
- [ ] `Review/ui-nielsen-audit.md` — Sprint 13 status block.
- [ ] `fix-ui-audit.md` — Sprint 13 plan section (T7.1-T7.4).
- [ ] `Review/handoff-ui-audit.md` — Batch I + Sprint 13 entry + TL;DR row 13.
- [ ] `docs/HANDOFF.md` — Sprint 13 section.
- [ ] `.agent/plan.md` — this section.
- [ ] `.agent/context.md` — Sprint 13 decisions + file:line references.
- [ ] `Review/sprint13-verification.md` — written.
### Done when (Sprint 13)
- All boxes above ticked.
- `npm run build` green.
- `Review/sprint13-verification.md` exists.
- All 6 doc files have a Sprint 13 status block.
### Out of scope (Sprint 13)
- **F9-full — local Ollama model pull on the host.** Would require pulling Mistral 7B or Llama 3 8B (~4 GB) + a separate `ollama serve` process + a different config (`OLLAMA_BASE_URL=http://localhost:11434`). F9-lite uses the cloud tier. Future sprint if the cloud costs become painful.
- **Prompt engineering / quality iteration.** The prompt is a first cut. If the LLM returns 0 picks or 21 identical recipes, the operator can iterate on the prompt. Out of scope for the initial ship.
- **Multi-week plans.** One week at a time.
- **Save the prompt as a template** for reuse. Future sprint.
+19 -5
View File
@@ -21,14 +21,15 @@ If you are a new agent continuing this work, do this **in order**:
- **Batch F:** Sprint 10 (one `git pull`, `docker compose up -d --build backend frontend` — no migration; the `NeverSuggest` table already exists from prior sprints). - **Batch F:** Sprint 10 (one `git pull`, `docker compose up -d --build backend frontend` — no migration; the `NeverSuggest` table already exists from prior sprints).
- **Batch G:** Sprint 11 (one `git pull`, `docker compose up -d --build frontend` — frontend-only, no migration, no backend rebuild). - **Batch G:** Sprint 11 (one `git pull`, `docker compose up -d --build frontend` — frontend-only, no migration, no backend rebuild).
- **Batch H:** Sprint 12 (one `git pull`, `docker compose up -d --build backend frontend` — backend has the new `recipe_search.py` router, frontend has the new toggle). - **Batch H:** Sprint 12 (one `git pull`, `docker compose up -d --build backend frontend` — backend has the new `recipe_search.py` router, frontend has the new toggle).
4. **Open issues** in `.agent/plan.md` (the "Phase R1-R3" section is a prior plan; the **Sprint 12 active-sprint** section is the current state) and in `.agent/context.md` (decisions + open Qs for the current sprint). - **Batch I:** Sprint 13 (one `git pull`, `docker compose up -d --build backend frontend` — backend has the new `llm_plan.py` router, frontend has the new prompt modal).
4. **Open issues** in `.agent/plan.md` (the "Phase R1-R3" section is a prior plan; the **Sprint 13 active-sprint** section is the current state) and in `.agent/context.md` (decisions + open Qs for the current sprint).
5. **Do not** touch the pre-existing WIP files: `backend/app/api/recipes.py`, `backend/app/schemas/recipe.py`, `nginx/nginx.conf` (untouched since before this work; user's to manage). 5. **Do not** touch the pre-existing WIP files: `backend/app/api/recipes.py`, `backend/app/schemas/recipe.py`, `nginx/nginx.conf` (untouched since before this work; user's to manage).
6. **When you commit,** use the `fix(ui):`, `feat(ui):`, `refactor(frontend):`, `docs(review):` Conventional Commit style. Force-add new files in `frontend/src/lib/` (the `.gitignore` line 17 `lib/` is a pre-existing bug that catches it). 6. **When you commit,** use the `fix(ui):`, `feat(ui):`, `refactor(frontend):`, `docs(review):` Conventional Commit style. Force-add new files in `frontend/src/lib/` (the `.gitignore` line 17 `lib/` is a pre-existing bug that catches it).
**TL;DR of where things stand:** **TL;DR of where things stand:**
- Sprints 1 + 2 + 3 + 4 + 5 + 6 + 7 + 8: code committed and build green. Sprint 1 deployed. Sprints 2-8 awaiting user deploy. - Sprints 1 + 2 + 3 + 4 + 5 + 6 + 7 + 8: code committed and build green. Sprint 1 deployed. Sprints 2-8 awaiting user deploy.
- The only remaining §Future item is F9 Ollama LLM matcher (proposal). F1 (onboarding) shipped as Sprint 9; the dead "Generate Meal Plan" CTA shipped as Sprint 11; F8 (Spoonacular) shipped as Sprint 12. All three are deployment-pending. - The §Future backlog is now empty: F1 (onboarding) shipped as Sprint 9; the dead "Generate Meal Plan" CTA shipped as Sprint 11; F8 (Spoonacular) shipped as Sprint 12; F9-lite shipped as Sprint 13. All four are deployment-pending. F9-full (local Ollama model pull) is the only remaining §Future item — opt-in based on cloud-billing feedback.
- Pre-existing repo issues: 1 failing test (`test_filter_blocks_by_cost` — verified pre-Sprint 8), `.gitignore` `lib/` bug, no CI. Documented. - Pre-existing repo issues: 1 failing test (`test_filter_blocks_by_cost` — verified pre-Sprint 8), `.gitignore` `lib/` bug, no CI. Documented.
--- ---
@@ -54,7 +55,7 @@ If you are a new agent continuing this work, do this **in order**:
**Tracking docs:** `Review/sprint8-verification.md` (deploy + smoke), `Review/ui-nielsen-audit.md` Sprint 8 status block, `fix-ui-audit.md` T2.1T2.10, this file, `docs/HANDOFF.md` Sprint 8 section. **Tracking docs:** `Review/sprint8-verification.md` (deploy + smoke), `Review/ui-nielsen-audit.md` Sprint 8 status block, `fix-ui-audit.md` T2.1T2.10, this file, `docs/HANDOFF.md` Sprint 8 section.
**Thread 3 (§Future backlog) is deferred** until S8 is deployed + verified. F9 (Ollama) proposal remains. **Sprint 9 (F1 onboarding) + post-deploy fix (`1562929`), Sprint 10 (Deny Forever on Recipes), Sprint 11 (wire the dead "Generate Meal Plan" CTA), and Sprint 12 (F8 Spoonacular search) are all committed 2026-06-05, awaiting user deploy.** **Thread 3 (§Future backlog) is deferred** until S8 is deployed + verified. F9-full (local Ollama model pull on the host) remains a future sprint. **Sprint 9 (F1 onboarding) + post-deploy fix (`1562929`), Sprint 10 (Deny Forever on Recipes), Sprint 11 (wire the dead "Generate Meal Plan" CTA), Sprint 12 (F8 Spoonacular search), and Sprint 13 (F9-lite Ollama Cloud plan synthesis) are all committed 2026-06-05, awaiting user deploy.**
### Sprint 9 — F1 Onboarding Tour (H10) ### Sprint 9 — F1 Onboarding Tour (H10)
@@ -118,6 +119,18 @@ If you are a new agent continuing this work, do this **in order**:
**Tracking docs:** `Review/sprint12-verification.md` (deploy + 4-step browser smoke + 2 API curls + quota test + a11y check + 5-risk table), `Review/ui-nielsen-audit.md` Sprint 12 status block, `fix-ui-audit.md` T6.1T6.6, this file, `docs/HANDOFF.md` Sprint 12 section. **Tracking docs:** `Review/sprint12-verification.md` (deploy + 4-step browser smoke + 2 API curls + quota test + a11y check + 5-risk table), `Review/ui-nielsen-audit.md` Sprint 12 status block, `fix-ui-audit.md` T6.1T6.6, this file, `docs/HANDOFF.md` Sprint 12 section.
### Sprint 13 — F9-lite (Ollama Cloud plan synthesis) (§Future H10) (user-driven)
**Status: COMMITTED on 2026-06-05. Build green. Backend + frontend.** Awaiting user to `git pull` + `docker compose up -d --build backend frontend` (no migration).
**Root cause (one-liner):** the Sprint 11 "Generate Meal Plan" CTA was library-only. Sprint 13 splits it into a 2-step modal: "Use the recipe library" (Sprint 11 unchanged) or "Ask the LLM" (new). The LLM path lets the user describe what they want for the week ("Italian-inspired, vegetarian") and uses kimi-k2.6:cloud on ollama.com to pick meals from the local library.
**Scope (5 boxes):** NEW `backend/app/api/llm_plan.py` (~280 lines, 1 endpoint + 4 helpers + tolerance for malformed LLM responses), 2 schema additions (`LLMPlanRequest` + `LLMPlanResponse`), 1 router registration, 1 `mealPlannerApi.llm.plan` method, prompt modal in `Dashboard.tsx` (radio + textarea + click-outside-to-dismiss). **No new dependencies. No migration. No pre-existing WIP files touched.** Reuses `OLLAMA_BASE_URL` / `OLLAMA_API_KEY` / `OLLAMA_MODEL` from `config.py:36-38`.
**LLM tolerance:** a 60s timeout, parse-failure (markdown code fences, trailing commentary), or empty response all return 0 picks; the library fill takes over. The user sees a success toast with `picked_count: 0` — same as if the LLM had returned 0 picks — never a crash.
**Tracking docs:** `Review/sprint13-verification.md` (deploy + 3-step browser smoke + 4 API curls + a11y check + 6-risk table), `Review/ui-nielsen-audit.md` Sprint 13 status block, `fix-ui-audit.md` T7.1T7.5, this file, `docs/HANDOFF.md` Sprint 13 section.
### Sprint 7 — Fix webui "empty meal plan" (date-semantics mismatch) ### Sprint 7 — Fix webui "empty meal plan" (date-semantics mismatch)
**Status: COMMITTED `09c7525` on 2026-06-05. Build green.** Awaiting user to `git pull` + run the SQL fix + rebuild. **Status: COMMITTED `09c7525` on 2026-06-05. Build green.** Awaiting user to `git pull` + run the SQL fix + rebuild.
@@ -149,8 +162,9 @@ Twelve commits land all 14 audit findings + 6 §Future items + 2 user-driven spr
| 10 | (committed 2026-06-05) | "Deny Forever" on Recipes — card overlay + RecipeDetail top bar + reason dropdown (allergy/dislike) + undo toast. New `POST`/`DELETE /api/never-suggest` (public) + `recipe_name` join. | ✅ green | ⚠️ committed; awaiting user deploy (backend + frontend, no migration) | | 10 | (committed 2026-06-05) | "Deny Forever" on Recipes — card overlay + RecipeDetail top bar + reason dropdown (allergy/dislike) + undo toast. New `POST`/`DELETE /api/never-suggest` (public) + `recipe_name` join. | ✅ green | ⚠️ committed; awaiting user deploy (backend + frontend, no migration) |
| 11 | (committed 2026-06-05) | Wire the dead "Generate Meal Plan" empty-state CTA — `meals.create` + `meals.fillEmptySlots`; race-safe; reusable for F8/F9. | ✅ green | ⚠️ committed; awaiting user deploy (frontend-only) | | 11 | (committed 2026-06-05) | Wire the dead "Generate Meal Plan" empty-state CTA — `meals.create` + `meals.fillEmptySlots`; race-safe; reusable for F8/F9. | ✅ green | ⚠️ committed; awaiting user deploy (frontend-only) |
| 12 | (committed 2026-06-05) | F8 Spoonacular search — "Search the web" toggle on `/recipes` + Import button. New `GET /api/recipes/search` + `POST /api/recipes/import`. Quota-gated (140pt/day). | ✅ green | ⚠️ committed; awaiting user deploy (backend + frontend) | | 12 | (committed 2026-06-05) | F8 Spoonacular search — "Search the web" toggle on `/recipes` + Import button. New `GET /api/recipes/search` + `POST /api/recipes/import`. Quota-gated (140pt/day). | ✅ green | ⚠️ committed; awaiting user deploy (backend + frontend) |
| 13 | (committed 2026-06-05) | F9-lite Ollama Cloud plan synthesis — prompt modal on Dashboard CTA. New `POST /api/llm/plan`. Library-or-LLM radio; 60s timeout tolerance; library fills the rest. | ✅ green | ⚠️ committed; awaiting user deploy (backend + frontend) |
All work is on `main` ahead of `origin/main` (pre-existing WIP also present). All 12 sprints compile. **Sprint 1 is live. Sprints 2-12 are not yet live on `100.108.208.56:8082/`.** All work is on `main` ahead of `origin/main` (pre-existing WIP also present). All 13 sprints compile. **Sprint 1 is live. Sprints 2-13 are not yet live on `100.108.208.56:8082/`.**
**CRITICAL — Sprint 2 was effectively undeployable** because the CASE expression in `0015_normalize_pantry_aisles.py` failed with `text = boolean` on the `varchar(100) aisle` column. The bug is fixed in `d78bd18` (Sprint 5). Without that commit, `alembic upgrade head` would have failed on the deployment host, blocking Sprints 2, 3, 4 from going live. **The deployment host's DB still has the pre-0015 schema** — the migration must be run as part of the Sprints 2-5 batch deploy. **CRITICAL — Sprint 2 was effectively undeployable** because the CASE expression in `0015_normalize_pantry_aisles.py` failed with `text = boolean` on the `varchar(100) aisle` column. The bug is fixed in `d78bd18` (Sprint 5). Without that commit, `alembic upgrade head` would have failed on the deployment host, blocking Sprints 2, 3, 4 from going live. **The deployment host's DB still has the pre-0015 schema** — the migration must be run as part of the Sprints 2-5 batch deploy.
@@ -394,4 +408,4 @@ cd frontend && npm run build
Trust the build output. Trust the smoke checklist. Don't trust the deployment host's UI until the user confirms. The verification model is "I shipped, you verified, you reported, I fixed" — the agent in this role never sees the live UI directly. Trust the build output. Trust the smoke checklist. Don't trust the deployment host's UI until the user confirms. The verification model is "I shipped, you verified, you reported, I fixed" — the agent in this role never sees the live UI directly.
**Last updated: 2026-06-05** — Sprint 1 deployed; Sprints 2-6 awaiting user deploy; **Sprint 7 (`09c7525`), Sprint 8 (`efd1fc6`), Sprint 9 (F1 Onboarding Tour) + post-deploy fix (`1562929`), Sprint 10 (Deny Forever on Recipes), Sprint 11 (Wire the dead "Generate Meal Plan" CTA), and Sprint 12 (F8 Spoonacular search) committed on 2026-06-05, awaiting user deploy**. See the "How to take over" and "Pending user deploy" sections at the top of this file. **Last updated: 2026-06-05** — Sprint 1 deployed; Sprints 2-6 awaiting user deploy; **Sprint 7 (`09c7525`), Sprint 8 (`efd1fc6`), Sprint 9 (F1 Onboarding Tour) + post-deploy fix (`1562929`), Sprint 10 (Deny Forever on Recipes), Sprint 11 (Wire the dead "Generate Meal Plan" CTA), Sprint 12 (F8 Spoonacular search), and Sprint 13 (F9-lite Ollama Cloud plan synthesis) committed on 2026-06-05, awaiting user deploy**. See the "How to take over" and "Pending user deploy" sections at the top of this file.
+122
View File
@@ -0,0 +1,122 @@
# Sprint 13 — F9-lite (Ollama Cloud free-text plan synthesis) — verification
**Status (2026-06-05):** ✅ Code complete. `npm run build` green. Awaiting user deploy.
## Summary
Sprint 13 splits the Sprint 11 "Generate Meal Plan" CTA into a 2-step modal: the user picks "Use the recipe library" (default, Sprint 11's existing flow) or "Ask the LLM" (new). The LLM path POSTs to `/api/llm/plan` with a free-text prompt; the backend calls `kimi-k2.6:cloud` on `ollama.com`, parses the LLM's JSON picks, creates a fresh plan, fills the LLM's picks, and falls through to the Sprint 6+ `fillEmptySlots` pattern for the slots the LLM didn't cover.
**No pre-existing WIP files touched.** Sprint 13 creates a new `backend/app/api/llm_plan.py` router (separate from the existing `llm_matcher.py` service) and reuses the established LLM call pattern (POST `${OLLAMA_BASE_URL}/chat/completions`, `Authorization: Bearer ${OLLAMA_API_KEY}`, same `max_tokens: 800, temperature: 0`, strip `<think>` blocks).
## Files changed
### Backend
- **NEW** `backend/app/api/llm_plan.py` (~280 lines) — 1 endpoint + 4 helpers: `_ensure_ollama_configured`, `_serialize_library`, `_ask_llm` (mirrors `llm_matcher._ask_ollama`), `_parse_picks` (tolerant of markdown code fences), `_validate_picks`. 200-recipe cap on the library sent to the LLM.
- **MODIFIED** `backend/app/schemas/__init__.py` — added `LLMPlanRequest` + `LLMPlanResponse`.
- **MODIFIED** `backend/app/main.py` — registered `llm_plan_api.router` at `/api/llm`.
### Frontend
- **MODIFIED** `frontend/src/api/index.ts` — added `llm.plan(data)`.
- **MODIFIED** `frontend/src/pages/Dashboard.tsx` — added the prompt modal (radio for library vs. LLM + textarea for the LLM path) + extracted Sprint 11's body into `generateFromLibrary` + added `generateFromLLM`. New state: `showPromptModal`, `promptMode`, `promptText`, `promptBusy`. The CTA now opens the modal; the modal's submit button dispatches on the radio.
## Build verification
```text
vite v5.4.21 building for production...
transforming...
✓ 1897 modules transformed.
rendering chunks...
computing gzip size...
dist/index.html 0.54 kB │ gzip: 0.32 kB
dist/assets/index-CPlRXkCg.css 42.38 kB │ gzip: 7.30 kB
dist/assets/index-665b6KrF.js 503.82 kB │ gzip: 154.33 kB
✓ built in 2.84s
```
- `tsc` 0 errors, `vite` 0 errors.
- Bundle: 500.28 → 503.82 kB (+3.5 kB for the modal + the LLM handler).
- Backend AST clean on all 3 changed files.
## Backend verification (manual, post-deploy)
```bash
# Requires OLLAMA_API_KEY + OLLAMA_BASE_URL + OLLAMA_MODEL set in the
# backend env. (All three already exist; Sprint 13 just adds the
# router that reads them.)
# 1) Happy path
curl -sS -X POST 'http://100.108.208.56:8082/api/llm/plan' \
-H 'Content-Type: application/json' \
-H 'Cookie: mealplanner_session=...' \
-d '{"prompt": "Italian-inspired, vegetarian", "week_start": "2026-06-08"}' | jq
# Expected: { "plan_id": "<uuid>", "picked_count": 7+,
# "filled_count": 14-, "failed_count": 0,
# "reasoning": null }
# 2) OLLAMA_API_KEY unset (or temporarily unset in env + restart)
curl -sS -X POST 'http://100.108.208.56:8082/api/llm/plan' \
-H 'Content-Type: application/json' \
-H 'Cookie: mealplanner_session=...' \
-d '{"prompt": "test", "week_start": "2026-06-08"}' -i | head -1
# Expected: HTTP/1.1 503 Service Unavailable
# 3) Empty prompt
curl -sS -X POST 'http://100.108.208.56:8082/api/llm/plan' \
-H 'Content-Type: application/json' \
-H 'Cookie: mealplanner_session=...' \
-d '{"prompt": "", "week_start": "2026-06-08"}' -i | head -1
# Expected: HTTP/1.1 422 Unprocessable Entity
# 4) Plan for this week already exists
curl -sS -X POST 'http://100.108.208.56:8082/api/llm/plan' \
-H 'Content-Type: application/json' \
-H 'Cookie: mealplanner_session=...' \
-d '{"prompt": "test", "week_start": "2026-06-01"}' -i | head -1
# Expected: HTTP/1.1 400 Bad Request (if a plan for 2026-06-01 exists)
```
## Browser smoke (3 steps)
Run on `http://100.108.208.56:8082/`.
1. **Land on `/` with no plan.** Click "Generate Meal Plan". Confirm: the modal opens with the title "Generate Meal Plan", a short description, and the 2 radio options ("Use the recipe library" selected by default, "Ask the LLM" unselected). The "Generate" button is enabled; the "Cancel" button is enabled.
2. **Type "Italian-inspired, vegetarian" + select "Ask the LLM" + click Generate.** Confirm: button label flips to "Asking LLM…" (with a spinning Loader2 icon), the modal is non-dismissible, ~5-15s later (depends on the LLM latency) the modal closes, the plan grid renders with the LLM's picks, and a toast shows `"Planned N meals (LLM picked K, library filled the rest)"`.
3. **Refresh the page.** Confirm the plan persists. The empty state does NOT re-appear.
## A11y check
- The modal is a `<div className="fixed inset-0 z-50">` with a click-outside-to-dismiss handler (skipped while `promptBusy` is true).
- The radios are real `<input type="radio">` with associated `<label>` blocks. Tab cycles through both radios, the textarea (when LLM mode is selected), and the 2 buttons.
- The textarea has `autoFocus` when the modal opens in LLM mode. Default LLM mode is library (no textarea shown); switching to LLM mode does not steal focus (intentional — the user just clicked the radio, they shouldn't lose their cursor).
- The character counter is a `<div>` with `text-right` alignment, screen-reader-accessible via the `500` max length on the textarea.
- The "Generate" button is `disabled` while `promptBusy` is true (matches Sprint 11's `generatingFirstPlan` state pattern).
- The Sprint 9 OnboardingTour's first step is the Dashboard's Weekly Overview card; the modal renders on top of that card via `z-50` + `bg-black/40` scrim. The tour is not affected.
## Risks & mitigations
- **R1: LLM returns 0 picks or 21 identical recipes.** Mitigation: the response is validated (recipe_id in library, day_of_week 1-7, meal_type in {breakfast, lunch, dinner}) and invalid entries are dropped. The library fill then takes over for any slot the LLM didn't cover. A prompt that returns 0 valid picks still produces a complete plan from the library.
- **R2: LLM times out (60s).** Mitigation: the `requests.post` call has a 60s timeout. On timeout, `_ask_llm` returns None; the endpoint then calls the library fill (no LLM picks). The user sees a success toast with `picked_count: 0`.
- **R3: Library is empty.** Mitigation: 400 with `detail: "recipe library is empty; import some recipes first"`. The frontend `showApiError` surfaces the message. (Pre-existing WIP, also affects the Sprint 11 library path.)
- **R4: OLLAMA_API_KEY unset.** Mitigation: 503 with clear `detail: "OLLAMA_API_KEY not configured; set it in the backend env"`. The frontend `showApiError` surfaces the message; the user can switch to "Use the recipe library" and proceed.
- **R5: OLLAMA_CLOUD costs.** The operator's existing OLLAMA billing applies. The endpoint is public + requires `require_session` (so no public abuse). A future sprint could add a per-day rate limit.
- **R6: Sprint 12's pre-existing WIP collision.** Verified: the WIP `recipes.py` is registered at `/api/recipes/*` only; my new `/api/llm/plan` is in a different prefix. No collision.
## Commit
One commit: `feat(ui): Sprint 13 — F9-lite (Ollama Cloud free-text plan synthesis)`. Files:
- `backend/app/api/llm_plan.py` (NEW, ~280 lines)
- `backend/app/schemas/__init__.py` (2 Pydantic models)
- `backend/app/main.py` (router registration)
- `frontend/src/api/index.ts` (1 new method)
- `frontend/src/pages/Dashboard.tsx` (modal + LLM handler + extracted library handler)
## Future work (NOT in Sprint 13)
- **F9-full — local Ollama model pull.** Pull Mistral 7B or Llama 3 8B (~4 GB) on the host + a separate `ollama serve` process. Avoids cloud costs and the LLM_API_KEY dependency. Future sprint.
- **Prompt template library.** Save common prompts ("easy weeknight", "vegetarian week", "quick breakfasts") as one-click buttons. Out of scope for the initial ship.
- **Multi-week plans.** One week at a time. The endpoint is parameterised on `week_start`; calling it twice with two different weeks would work, but the UI doesn't surface it yet.
- **Streaming the LLM response.** Currently waits for the full response. Future sprint could use the Ollama streaming API + a "thinking…" indicator.
- **Per-day rate limit.** A future sprint could gate the LLM call on a per-day budget to avoid surprise cloud bills.
- **Vitest unit test for `useOnboarding`** (Q4 from Sprint 9). Still pending.
+6
View File
@@ -131,6 +131,12 @@ The app looks polished on the surface (Tailwind palette, clean cards, working to
> - **T6.4** Frontend: `frontend/src/api/index.ts` adds `recipes.search` + `recipes.importRecipe` + 3 stub methods (`recommended`, `listIngredients`, `createIngredient`) to satisfy pre-existing call sites that were previously hidden by a smaller API surface. `frontend/src/pages/Recipes.tsx` adds the toggle button (with `aria-pressed`) + the web-search panel (`<div role="region" aria-label="Web recipe search" aria-busy={webLoading}>`) + the import mutation (toast on success, `showApiError` on failure). `frontend/src/types/index.ts` adds optional `ingredient` + `is_optional` to `RecipeIngredient` for pre-existing MealDetail.tsx call sites. > - **T6.4** Frontend: `frontend/src/api/index.ts` adds `recipes.search` + `recipes.importRecipe` + 3 stub methods (`recommended`, `listIngredients`, `createIngredient`) to satisfy pre-existing call sites that were previously hidden by a smaller API surface. `frontend/src/pages/Recipes.tsx` adds the toggle button (with `aria-pressed`) + the web-search panel (`<div role="region" aria-label="Web recipe search" aria-busy={webLoading}>`) + the import mutation (toast on success, `showApiError` on failure). `frontend/src/types/index.ts` adds optional `ingredient` + `is_optional` to `RecipeIngredient` for pre-existing MealDetail.tsx call sites.
> - **T6.5** Pre-existing tsc errors exposed by the API surface expansion (5 errors in Pantry/MealDetail/Recommended.tsx) — resolved per user decision: added 5 stub API methods + 2 type fields. Documented in `Review/sprint12-verification.md` D-fix section. > - **T6.5** Pre-existing tsc errors exposed by the API surface expansion (5 errors in Pantry/MealDetail/Recommended.tsx) — resolved per user decision: added 5 stub API methods + 2 type fields. Documented in `Review/sprint12-verification.md` D-fix section.
> - **Verification log:** `Review/sprint12-verification.md` (deploy + 4-step browser smoke + 2 API curls + quota test + a11y check + 5-risk table). Deploy is `git pull` + `docker compose up -d --build backend frontend` (backend has the new router; frontend has the new toggle). > - **Verification log:** `Review/sprint12-verification.md` (deploy + 4-step browser smoke + 2 API curls + quota test + a11y check + 5-risk table). Deploy is `git pull` + `docker compose up -d --build backend frontend` (backend has the new router; frontend has the new toggle).
>
> **Sprint 13 status (committed 2026-06-05, awaiting deploy):** F9-lite — splits the Sprint 11 "Generate Meal Plan" CTA into a 2-step modal: the user picks "Use the recipe library" (default, Sprint 11's flow) or "Ask the LLM" (new). The LLM path POSTs to `/api/llm/plan`; the backend calls `kimi-k2.6:cloud` on `ollama.com`, parses the LLM's JSON picks, creates a fresh plan, fills the LLM's picks, and falls through to the Sprint 6+ `fillEmptySlots` pattern for the slots the LLM didn't cover. **No pre-existing WIP files touched.** F9-full (local Ollama model pull on the host) remains a future sprint.
> - **T7.1** `backend/app/api/llm_plan.py` (NEW, ~280 lines). 1 endpoint (`POST /api/llm/plan` body `{prompt, week_start}`) + 4 helpers (`_ensure_ollama_configured`, `_serialize_library` with a 200-recipe cap, `_ask_llm` mirroring the `llm_matcher._ask_ollama` pattern, `_parse_picks` tolerant of markdown code fences, `_validate_picks` that drops invalid entries). 60s timeout, 422 on empty/oversized prompt, 503 on missing OLLAMA_API_KEY, 400 on duplicate week.
> - **T7.2** `backend/app/schemas/__init__.py` — added `LLMPlanRequest` + `LLMPlanResponse` Pydantic models. The router is registered in `main.py:65-66` at the `/api/llm` prefix.
> - **T7.3** Frontend: `frontend/src/api/index.ts` adds `llm.plan(data)`. `frontend/src/pages/Dashboard.tsx` adds the prompt modal (radio for library vs. LLM + textarea for the LLM path with 500-char counter) + extracted Sprint 11's body into `generateFromLibrary` + added `generateFromLLM`. New state: `showPromptModal`, `promptMode`, `promptText`, `promptBusy`. Click-outside-to-dismiss is disabled while `promptBusy` is true. The textarea `autoFocus`es when LLM mode is selected.
> - **Verification log:** `Review/sprint13-verification.md` (deploy + 3-step browser smoke + 4 API curls + a11y check + 6-risk table). Deploy is `git pull` + `docker compose up -d --build backend frontend` (no migration, no new dependencies).
> - **No new dependencies. No migration. Admin path unchanged.** > - **No new dependencies. No migration. Admin path unchanged.**
> >
> **Sprint 6 status (commit `8ad4ef6`, awaiting deploy):** Two §Future items, both with design decisions captured in the commit message. > **Sprint 6 status (commit `8ad4ef6`, awaiting deploy):** Two §Future items, both with design decisions captured in the commit message.
+22 -1
View File
@@ -302,7 +302,7 @@ Trust the tests. Trust the live runs. Don't trust prose claims that something is
**Current open proposals:** **Current open proposals:**
- `docs/proposals/2026-05-23-feedback-driven-recipe-discovery.md` — pending user approval. No code yet (per the 2026-05-23 section below). - `docs/proposals/2026-05-23-feedback-driven-recipe-discovery.md` — pending user approval. No code yet (per the 2026-05-23 section below).
**Last updated: 2026-06-05** — UI/UX audit & fix cycle (Sprints 1, 2, 3, 4, 5, 6, 7, 8, 9) complete. 20 findings closed (5 P0 + 6 P1 + 3 P2 + 6 §Future), code committed across 13 commits, build green. Sprint 1 deployed; Sprints 2-9 awaiting deploy. **Sprint 7 (`09c7525`, awaiting user deploy)** aligns "this week" to the upcoming Monday. **Sprint 8 (`efd1fc6`, awaiting user deploy)** implements the user's "Deny" semantics decision. **Sprint 9 (committed 2026-06-05, awaiting user deploy)** ships the F1 Onboarding Tour. **Sprint 10 (committed 2026-06-05, awaiting user deploy)** ships the "Deny Forever" on Recipes. **Sprint 11 (committed 2026-06-05, awaiting user deploy)** wires the dead "Generate Meal Plan" CTA. **Sprint 12 (committed 2026-06-05, awaiting user deploy)** ships the F8 Spoonacular search. See Sprint 7 + Sprint 8 + Sprint 9 + Sprint 10 + Sprint 11 + Sprint 12 sections below. Full UI-audit handoff at `Review/handoff-ui-audit.md`. **Last updated: 2026-06-05** — UI/UX audit & fix cycle (Sprints 1, 2, 3, 4, 5, 6, 7, 8, 9) complete. 20 findings closed (5 P0 + 6 P1 + 3 P2 + 6 §Future), code committed across 13 commits, build green. Sprint 1 deployed; Sprints 2-9 awaiting deploy. **Sprint 7 (`09c7525`, awaiting user deploy)** aligns "this week" to the upcoming Monday. **Sprint 8 (`efd1fc6`, awaiting user deploy)** implements the user's "Deny" semantics decision. **Sprint 9 (committed 2026-06-05, awaiting user deploy)** ships the F1 Onboarding Tour. **Sprint 10 (committed 2026-06-05, awaiting user deploy)** ships the "Deny Forever" on Recipes. **Sprint 11 (committed 2026-06-05, awaiting user deploy)** wires the dead "Generate Meal Plan" CTA. **Sprint 12 (committed 2026-06-05, awaiting user deploy)** ships the F8 Spoonacular search. **Sprint 13 (committed 2026-06-05, awaiting user deploy)** ships the F9-lite Ollama Cloud plan synthesis. See Sprint 7 + Sprint 8 + Sprint 9 + Sprint 10 + Sprint 11 + Sprint 12 + Sprint 13 sections below. Full UI-audit handoff at `Review/handoff-ui-audit.md`.
--- ---
@@ -407,6 +407,27 @@ Trust the tests. Trust the live runs. Don't trust prose claims that something is
**Path forward to F9:** the `handleGenerateFirstPlan` (Sprint 11) + the `recipe_search.import` (Sprint 12) are the two seams. Future F9 (Ollama local LLM) work plugs into the same `fillEmptySlots` / `import` flow — no DOM, copy, or component structure changes needed. **Path forward to F9:** the `handleGenerateFirstPlan` (Sprint 11) + the `recipe_search.import` (Sprint 12) are the two seams. Future F9 (Ollama local LLM) work plugs into the same `fillEmptySlots` / `import` flow — no DOM, copy, or component structure changes needed.
### Sprint 13 — F9-lite (Ollama Cloud plan synthesis) (§Future H10) (user-driven) — COMMITTED 2026-06-05
**User direction (2026-06-05):** "Proceed." F9-lite reuses the pre-existing `OLLAMA_*` config (`config.py:36-38`) — avoids the local model pull (F9-full would be 4 GB on disk + a separate uvicorn process). Cloud LLM — operator's existing OLLAMA billing applies.
**Scope (5 boxes):**
1. **NEW** `backend/app/api/llm_plan.py` (~280 lines) — 1 endpoint (`POST /api/llm/plan` body `{prompt, week_start}`) + 4 helpers (`_ensure_ollama_configured`, `_serialize_library` with a 200-recipe cap, `_ask_llm` mirroring `llm_matcher._ask_ollama`, `_parse_picks` tolerant of markdown code fences, `_validate_picks` that drops invalid entries). 60s timeout. 422 on empty/oversized prompt. 503 on missing OLLAMA_API_KEY. 400 on duplicate week.
2. `backend/app/schemas/__init__.py` — added `LLMPlanRequest` + `LLMPlanResponse`.
3. `backend/app/main.py:65-66` — registered `llm_plan_api.router` at the `/api/llm` prefix. No collision with the pre-existing WIP `recipes.py`.
4. **Frontend:** `frontend/src/api/index.ts` adds `llm.plan(data)`. `frontend/src/pages/Dashboard.tsx` adds the prompt modal (radio for library vs. LLM + textarea for the LLM path with 500-char counter) + extracted Sprint 11's body into `generateFromLibrary` + added `generateFromLLM`. New state: `showPromptModal`, `promptMode`, `promptText`, `promptBusy`. Click-outside-to-dismiss is disabled while `promptBusy` is true. The textarea `autoFocus`es when LLM mode is selected.
5. **LLM tolerance:** 60s timeout, parse-failure (markdown code fences, trailing commentary), or empty response all return 0 picks; the library fill takes over. The user never sees a crash — at worst, `picked_count: 0` and the toast reads "Planned N meals (LLM picked 0, library filled the rest)".
**Build:** `npm run build` green (tsc 0 errors, vite 0 errors). Bundle: 500.28 → 503.82 kB (+3.5 kB for the modal + the LLM handler). Backend AST clean. One commit: `feat(ui): Sprint 13 — F9-lite (Ollama Cloud free-text plan synthesis)`.
**Deploy:** `git pull` + `docker compose up -d --build backend frontend` (no migration, no new dependencies). Verification: `Review/sprint13-verification.md` (3-step browser smoke + 4 API curls + a11y check + 6-risk table).
**No regression expected:** Sprint 13 doesn't touch the pre-existing WIP `backend/app/api/recipes.py` / `schemas/recipe.py` / `nginx/nginx.conf`. The new router is in a separate file (`llm_plan.py`) and registered at a non-colliding `/api/llm` prefix. Sprints 1-12 are untouched. The Sprint 11 library path is unchanged (extracted into `generateFromLibrary`, identical body).
**§Future backlog status after Sprint 13:** F1 (onboarding) ✓, F8 (Spoonacular) ✓, F9-lite (Ollama Cloud) ✓. Only F9-full (local Ollama model pull) remains — opt-in based on cloud-billing feedback.
**Path forward to F9-full:** the `_ask_llm` helper is the single seam. F9-full only needs to swap the URL (`https://ollama.com/v1` → `http://localhost:11434`) and model name (`kimi-k2.6:cloud` → local). The endpoint code, prompt, and validation stay unchanged.
--- ---
## New session: 2026-06-05 (early) ## New session: 2026-06-05 (early)
+54
View File
@@ -695,3 +695,57 @@ User direction 2026-06-05: "Proceed." Selected from the question menu as the sma
### T6.6 · `Review/sprint12-verification.md` (NEW) ### T6.6 · `Review/sprint12-verification.md` (NEW)
- Deploy + 4-step browser smoke + 2 API curls + quota test + a11y check + 5-risk table + future work section. Source of truth for the operator deploy + smoke flow. - Deploy + 4-step browser smoke + 2 API curls + quota test + a11y check + 5-risk table + future work section. Source of truth for the operator deploy + smoke flow.
---
## Sprint 13 — F9-lite (Ollama Cloud free-text plan synthesis) — ✅ COMPLETE, awaiting deploy
User direction 2026-06-05: "Proceed." F9-lite reuses the pre-existing `OLLAMA_*` config (`config.py:36-38: OLLAMA_BASE_URL=https://ollama.com/v1, OLLAMA_API_KEY, OLLAMA_MODEL=kimi-k2.6:cloud`). Avoids the local model pull (F9-full would be 4 GB on disk + a separate uvicorn process). Cloud LLM — operator's existing OLLAMA billing applies.
**Status (2026-06-05):** ✅ Code complete. `npm run build` green (tsc 0 errors, vite 0 errors). Bundle: 500.28 → 503.82 kB. Backend AST clean. Awaiting user commit + deploy. **No new dependencies, no migration, no pre-existing WIP files touched.**
### T7.1 · Backend — `backend/app/api/llm_plan.py` (NEW, ~280 lines)
- 1 endpoint: `POST /api/llm/plan` (public, `require_session`). Body: `{prompt: str 1-500, week_start: date}`.
- 4 helpers:
- `_ensure_ollama_configured()` — 503 with clear `detail: "OLLAMA_API_KEY not configured; set it in the backend env"`.
- `_serialize_library(db, profile_id)` — reads up to 200 recipes for the family, sorted alphabetically. Cap prevents prompt-token overflow on kimi-k2.6.
- `_ask_llm(prompt)` — mirrors `llm_matcher._ask_ollama:97-144`. Same call pattern: `POST ${OLLAMA_BASE_URL}/chat/completions`, `Authorization: Bearer ${OLLAMA_API_KEY}`, `model: settings.OLLAMA_MODEL`, `max_tokens: 800, temperature: 0`, strips `<think>` blocks. 60s timeout.
- `_parse_picks(raw)` — tolerant JSON parser. Handles markdown code fences (` ```json ... ``` `), trailing commentary, and bare JSON. Returns a list of dicts (validated by the caller).
- `_validate_picks(picks, valid_recipe_ids)` — drops invalid entries: missing fields, out-of-range `day_of_week`, unknown `meal_type`, unknown `recipe_id`. Returns a list of `LLMPickedItem`.
- Flow:
1. Reject if a plan for `week_start` already exists (400 with the existing plan id; matches Sprint 11's `meals.create` 400 path).
2. Reject if the recipe library is empty (400 with `detail: "recipe library is empty; import some recipes first"`).
3. Build the prompt: "You are planning a 7-day meal plan (Monday through Sunday) for a family. Each day has 3 meals: breakfast, lunch, dinner. Pick up to 21 meals total from the recipe library below. If a slot has no good match for the user's request, OMIT it (do not invent a recipe). Use only recipe_ids from the list. USER REQUEST: <prompt>. RECIPE LIBRARY (<n> recipes): <list>. RETURN FORMAT — valid JSON only, no markdown, no commentary: [{day_of_week, meal_type, recipe_id}, ...]"
4. Call `_ask_llm(prompt)`. On timeout / network error / parse failure, return 0 picks; the library fill takes over.
5. Validate picks.
6. Create the plan (`MealPlan(family_profile_id, week_start_date, status='draft', notes=<prompt[:200]>)`).
7. Insert the LLM-picked items.
8. Fill the remaining slots from the library (Sprint 6+ pattern, re-implemented inline to avoid a self-HTTP-call). Uses the first non-already-used recipe per slot.
9. Return `{plan_id, picked_count, filled_count, failed_count, reasoning: <raw LLM text>}`.
### T7.2 · Backend — config + schemas + main.py wiring
- **File:** `backend/app/schemas/__init__.py` — added `LLMPlanRequest` (Pydantic, `prompt: str = Field(min_length=1, max_length=500)`, `week_start: date`) + `LLMPlanResponse` (`{plan_id: str, picked_count: int, filled_count: int, failed_count: int, reasoning: Optional[str]}`).
- **File:** `backend/app/main.py:65-66` — registered `llm_plan_api.router` at the `/api/llm` prefix. No collision with the pre-existing WIP `recipes.py` (which is at `/api/recipes`).
### T7.3 · Frontend — `Dashboard.tsx` modal + LLM handler
- **File:** `frontend/src/pages/Dashboard.tsx` — added the prompt modal + new state (`showPromptModal`, `promptMode`, `promptText`, `promptBusy`). The Sprint 11 `handleGenerateFirstPlan` body was extracted into two functions:
- `generateFromLibrary()` — unchanged Sprint 11 flow (`meals.create` + `meals.fillEmptySlots`).
- `generateFromLLM()` — new, calls `mealPlannerApi.llm.plan({prompt, week_start})`. On success, toasts `"Planned N meals (LLM picked K, library filled the rest)"`. On error, uses `showApiError` (Sprint 4 F7) which surfaces the backend's 503 / 422 / 400 detail.
- The modal is inline (not a separate component) because it depends on 4 local states + 3 handlers. Click-outside-to-dismiss is disabled while `promptBusy` is true. The "Generate" button label flips to `"Asking LLM…"` (with a spinning Loader2 icon) when LLM mode is selected, or `"Generating…"` when library mode is selected.
- The textarea `autoFocus`es when LLM mode is selected. The character counter shows `current / 500` (right-aligned, screen-reader-accessible via the textarea's `maxLength`).
- **File:** `frontend/src/api/index.ts` — added `llm.plan(data)` method.
### T7.4 · Sprint 13 verification gate
- [x] `npm run build` green (tsc 0 errors, vite 0 errors). Bundle: 500.28 → 503.82 kB (+3.5 kB for the modal + the LLM handler).
- [x] Backend AST clean on all 3 changed files (`llm_plan.py`, `schemas/__init__.py`, `main.py`).
- [ ] Browser smoke (3 steps) on `http://100.108.208.56:8082/` per `Review/sprint13-verification.md`.
- [ ] Manual API smoke (4 curls): happy path + OLLAMA_API_KEY unset (503) + empty prompt (422) + duplicate week (400).
- [ ] No regression in Sprints 1-12.
### T7.5 · `Review/sprint13-verification.md` (NEW)
- Deploy + 3-step browser smoke + 4 API curls + a11y check + 6-risk table + future work section. Source of truth for the operator deploy + smoke flow.