Public Access
docs: Sprint 13 — F9-lite (Ollama Cloud free-text plan synthesis) across all 6 running docs
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:
@@ -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)
|
||||
- `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)
|
||||
|
||||
|
||||
@@ -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).
|
||||
- **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.
|
||||
|
||||
---
|
||||
|
||||
## 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.
|
||||
|
||||
Reference in New Issue
Block a user