Mealie-backed AI meal planner + shopping list for the family
Find a file
Kayos 89f33f237c plan agent: per-user fit + pairings + mood + leftover_potential +
Hecate's weekly reading + 2nd-pass allergen verification

Five new context dimensions for the planner agent (and one quality
fix), bumping ENRICH_VERSION 3→4 so existing meta gets refreshed on
the next walk:

(A) Per-user fit score. Computed at plan-gen time from picker_profiles
    × meta — no extra Sonnet calls. _compute_fit_score scores each
    recipe 1-5 per household member based on cuisine match, protein
    match, comfort_tier match, and tag overlap with that user's
    historical picks. Renders in the recipe pool prompt as
    'fit:cobb=5,abby=2,leia=3'. Gives Sonnet per-user signal to
    bias AI-chosen slots toward whoever's home that week, AND makes
    pick rationale explainable.

(B) Pairings per recipe. New meta fields:
        pairings.serves_well_with: ["crusty bread","green salad",...]
        pairings.drinks: ["pinot noir","iced tea",...]
    Sonnet enriches both during the main pass. Foundation for future
    "auto-suggest matching side" UX in multi-meal slots.

(C) Leftover potential 1-5. Per-recipe score: 1=eat-now-only (crispy
    things, fresh salads), 5=actually BETTER as leftovers (stews,
    braises, lasagna). Lets the planner thread Sunday's slow-cooked
    pot roast into Monday's lunch slots intentionally.

(D) Mood scores. Per-recipe 1-5 ratings on:
        cozy / summer_fresh / energizing / comfort
    Independent dimensions — a recipe can score high on multiple.
    Foundation for future weather/mood-aware planning ("rainy week
    → bias cozy>=4").

(E) Hecate's weekly reading. New TEXT column hecate_reading on
    cauldron_meal_plans (migration 032). The plan-gen prompt asks
    Hecate to write a 1-paragraph narrative voice description of the
    week — "this week leans into the brisk turn of the season, three
    hearty one-pots front-load your weekday energy, salmon Wednesday
    for Cobb's gym push..." Confident, wise, theatrical voice. Pure
    flavor, no functional impact, but makes Hecate FEEL like an
    advisor not an opaque function. Plus the planner system prompt
    now opens with the Hecate persona ("You are Hecate, Greek-
    mythology witch goddess of crossroads, herbs, and magic — and
    the family's meal planner"). Output schema gains a "reading"
    field alongside "slots".

ALLERGEN VERIFICATION (the quality fix Cobb explicitly asked for):
- Sonnet sometimes flags pork=true on a sweet potato recipe via the
  conservative-default rule. Cobb wants CLEAN data.
- New forge.verify_allergens — second Sonnet pass after main enrich
  with a strict prompt: "name the SPECIFIC ingredient triggering each
  allergen flag, or set FALSE." For non-anaphylaxis exclusions like
  pork, set FALSE unless an actual pork ingredient is named. For
  ANAPHYLAXIS allergens, conservative TRUE still applies.
- Cost: ~3s/recipe extra. Wired into enrich_recipes.run_enrich after
  initial enrich; failure is non-fatal (falls through to original).
- Eliminates the pork-on-sweet-potatoes class of false positives.

Code restructure: forge.generate_plan now returns {slots, reading}
instead of just slots. _extract_plan_payload pulls both. Server-side
generate + regenerate paths unwrap and persist reading via the new
db.set_plan_hecate_reading. Plan template renders the reading in a
purple-bordered serif callout above the day grid.

Schema:
- migration 032 adds hecate_reading TEXT to cauldron_meal_plans.
- Cauldron_recipe_meta gets new fields persisted in meta_json (no
  schema change there — JSON column already accommodates).
2026-04-30 22:04:46 -07:00
cauldron plan agent: per-user fit + pairings + mood + leftover_potential + 2026-04-30 22:04:46 -07:00
scripts v0.3 step 5: lean shopping list — claude on-demand foods + game strip 2026-04-29 22:02:20 -07:00
tests v0.3 step 3+4: AI plan generator + /list shopping aggregation 2026-04-29 06:26:54 -07:00
.env.example v0.2 foundation — Authentik OIDC + sulkta-mariadb DB + Fernet crypto 2026-04-28 19:47:47 -07:00
.gitignore v0.1 — backend bones + ingredient sterilizer 2026-04-28 16:59:11 -07:00
compose.yml compose: also join sulkta-db-net so cauldron can reach sulkta-mariadb 2026-04-28 19:48:59 -07:00
Dockerfile v0.1 — backend bones + ingredient sterilizer 2026-04-28 16:59:11 -07:00
LICENSE Initial commit 2026-04-28 16:35:30 -07:00
README.md v0.1 — backend bones + ingredient sterilizer 2026-04-28 16:59:11 -07:00
requirements.txt search: local fuzzy recipe index — way smarter than Mealie's lexical default 2026-04-28 21:37:12 -07:00

cauldron

Mealie-backed AI meal planner + shopping list for the family. LAN-only, internal tool. Mealie at recipes.sulkta.com is the source of truth for recipes / meal plans / shopping lists; cauldron is the AI layer + Abby's branded UI on top.

Status

v0.1 — backend bones (current). Ingredient sterilizer endpoint working. No UI yet; bearer-auth API only. Frontend + Authentik OIDC arrives in v0.2. Native Kotlin Android in v0.5.

Surface (v0.1)

GET  /healthz                              liveness + clawdforge upstream
GET  /api/recipes                          list Mealie recipes (paginated)
POST /api/sterilize/preview/<slug>         dry-run AI parse, return proposals
POST /api/sterilize/apply/<slug>           write parses back to Mealie

All routes except /healthz require Authorization: Bearer <ADMIN_BEARER>.

Architecture

Abby's phone (later: Kotlin app)
        │
        ▼
  cauldron (Flask, port 7790, LAN-only)
   ├─ Mealie API client  ─── recipes.sulkta.com  (source of truth)
   ├─ clawdforge client  ─── 192.168.0.5:8800   (claude -p runner)
   └─ Authentik OIDC (v0.2)

cauldron does NOT hold its own database in v0.1 — all state lives in Mealie. A small Postgres/MariaDB schema lands in v0.2 for Abby-specific prefs + chat history.

Ingredient sterilizer

Mealie's CRF parser is mediocre. Cobb's hand-typed recipes have lots of free-form quantity strings ("about 2 cups cooked white rice", "1 small handful kale", "a pinch of salt") that don't aggregate cleanly into a shopping list.

The sterilizer batches all ingredients of one recipe into a single Sonnet call (via clawdforge), gets back parallel structured parses, then on apply links each parse to existing Mealie food/unit records (creating any missing by name) and PUTs the recipe back.

Preview is non-destructive — review proposals before apply.

# Dry-run preview
curl -sS -X POST -H "Authorization: Bearer $ADMIN_BEARER" \
  http://192.168.0.5:7790/api/sterilize/preview/spaghetti-bolognese | jq .

# Apply (creates missing foods/units by default)
curl -sS -X POST -H "Authorization: Bearer $ADMIN_BEARER" \
  http://192.168.0.5:7790/api/sterilize/apply/spaghetti-bolognese | jq .

Deploy

  1. ssh lucy
  2. cd /mnt/user/appdata && git clone <gitea-url> cauldron && cd cauldron/build (or wherever the deploy convention lands)
  3. Drop .env at /mnt/cache/appdata/secrets/cauldron.env (chmod 600 root:root)
    • CLAWDFORGE_TOKEN is already populated by the bootstrap (see memory/2026-04-28.md)
    • MEALIE_API_TOKEN — mint at recipes.sulkta.com → user → API tokens
    • ADMIN_BEARER — pick 32 bytes of entropy
    • SECRET_KEY — 32 bytes for Flask sessions
  4. docker compose up -d --build
  5. Smoke: curl http://192.168.0.5:7790/healthz

Roadmap

  • v0.1 ✓ — sterilizer backend + Flask shell
  • v0.2 — Authentik OIDC, Abby-branded web UI, palette CSS, postgres for prefs
  • v0.3 — meal plan generator (week → Mealie meal plan write)
  • v0.4 — shopping list aggregator (read meal plan → consolidated grocery list)
  • v0.5 — native Kotlin + Compose Android app (read-only shopping list + plan view)

Repo layout

cauldron/
├─ cauldron/
│  ├─ config.py            env-driven config
│  ├─ forge.py             clawdforge HTTP client
│  ├─ mealie.py            Mealie API client
│  ├─ sterilizer.py        ingredient parse + apply pipeline
│  └─ server.py            Flask app
├─ Dockerfile
├─ compose.yml
├─ requirements.txt
└─ .env.example