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.
|
||
|---|---|---|
| cauldron | ||
| scripts | ||
| tests | ||
| .env.example | ||
| .gitignore | ||
| compose.yml | ||
| Dockerfile | ||
| LICENSE | ||
| README.md | ||
| requirements.txt | ||
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
ssh lucycd /mnt/user/appdata && git clone <gitea-url> cauldron && cd cauldron/build(or wherever the deploy convention lands)- Drop
.envat/mnt/cache/appdata/secrets/cauldron.env(chmod 600 root:root)CLAWDFORGE_TOKENis already populated by the bootstrap (seememory/2026-04-28.md)MEALIE_API_TOKEN— mint atrecipes.sulkta.com→ user → API tokensADMIN_BEARER— pick 32 bytes of entropySECRET_KEY— 32 bytes for Flask sessions
docker compose up -d --build- 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