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
+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)
- 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.