Mealie-backed AI meal planner + shopping list for the family
Find a file
Kayos 9334d161e4 sterilize Phase 2: pass Mealie's food catalog into Sonnet's prompt
Sonnet was picking food names blindly. We then tried to match those
back to Mealie's catalog post-hoc. When Sonnet's natural pick didn't
match Mealie's exact convention, we'd create a duplicate row instead
of reusing the existing one. Lucky alignment with the seed kept the
dupe rate low, but the architecture had no real "Mealie is source of
truth" guarantee.

This change makes that guarantee explicit:

1. Sterilizer now lazy-loads Mealie's full food catalog on first
   _parse_batch call (one fetch per Sterilizer instance, so a bulk
   job pulls 2895 rows once and reuses across all 226 recipe parses).
   Uses the underlying mealie._get with per_page=2000 + page-walk for
   defensive coverage of really large catalogs.

2. STERILIZE_SYSTEM is now STERILIZE_SYSTEM_TEMPLATE — a string with
   a {foods} placeholder. _system_prompt() splices in a bullet list of
   every Mealie food (name, plural, aliases) at runtime.

3. New CATALOG RULES in the prompt instruct Sonnet to:
   - Match against name / pluralName / aliases first, return canonical
     name verbatim with is_new_food=false
   - Strip prep modifiers into note
   - Singularize plurals when canonical is singular
   - Treat brand variations as canonical+note ("Heinz ketchup" →
     food: "ketchup", note: "Heinz")
   - Set is_new_food=true ONLY when no reasonable catalog match exists,
     since adding aliases to fix mismatches later is way easier than
     cleaning duplicate food rows after the fact

4. New is_new_food field on IngredientParse and per-item schema. Will
   eventually drive an "alias suggestion" UI, but for v1 just gives
   us telemetry on how often Sonnet falls back to inventing names.

Net effect for the family-internal goal: zero duplicate food creates
from convention mismatches, smarter parses that respect the catalog
Cobb spent time curating, foundation laid for the Step 2 re-key
migration where cauldron_food_metadata gets keyed by Mealie food.id.
2026-04-30 11:48:40 -07:00
cauldron sterilize Phase 2: pass Mealie's food catalog into Sonnet's prompt 2026-04-30 11:48:40 -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