From 1445e0cbab188d637e6dc2f85e74b3612d2d6a8e Mon Sep 17 00:00:00 2001 From: Kayos Date: Thu, 30 Apr 2026 22:12:29 -0700 Subject: [PATCH] =?UTF-8?q?enrich=20v5:=20more=20agent=20context=20?= =?UTF-8?q?=E2=80=94=20cooking,=20kid-fit,=20expanded=20macros,=20occasion?= =?UTF-8?q?,=20hecate's=20quip=20+=20macro=20confidence=20+=20chain-of-tho?= =?UTF-8?q?ught=20macro=20estimation?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Eight new fields packed in before Cobb's force re-enrich rerun: (1) active_minutes + hands_off_minutes — split estimated_minutes into "you must be present" vs "rise/marinade/braise unattended". Lets weeknight planning match real available active time. A 4-hour beef stew might be 30 active + 210 hands-off — fine for Sunday but misleading as "240 minutes" on a busy weeknight. (2) equipment[] — required appliances/modes from a fixed enum: oven, stovetop, grill, instant-pot, slow-cooker, sheet-pan, cast-iron, air-fryer, food-processor, blender, mixer, smoker, sous-vide, no-cook. Foundation for "no oven this week" filters. (3) flavor_profile[] — 2-5 dominant flavor notes (spicy, sweet, savory, umami, tangy, smoky, herby, citrusy, rich, fresh, bitter, earthy). Lets the planner enforce variety so we don't get five spicy nights in a row. (4) kid_friendly_score 1-5 — separate from comfort_tier. Cobb has Leia and Luna; this is real signal. 1=adults-only (capers, blue cheese, super spicy), 5=kids beg for it (mac and cheese, pancakes). (5) fiber_g + sodium_mg per serving — extends the macro coverage. Sodium for heart-health weeks, fiber for gut weeks. (6) cost_per_serving_estimate — rough USD per serving (2026 prices). Bean bowl $2, salmon for two $8-12, fancy roast $15+. Foundation for budget-week preference. Set null for too-volatile items. (7) occasion_fit[] — when is this dish AT HOME? weeknight, weekend, brunch, date-night, party, picnic, camping, holiday, game-day, kids-birthday, quiet-night-in. (8) hecate_quip — one-line voice description in Hecate's mythic-witch tone. Pure flavor for tooltips/detail pages. ~10-20 words. Examples: "Pure midwinter comfort — asks for a fire and quiet." "Sharp and bright like a Tuesday morning resolution." MACRO QUALITY (Cobb's separate ask): - Prompt now instructs Sonnet to compute macros chain-of-thought: list each major ingredient, approximate its per-serving contribution in grams, sum, output. Plus a sanity check: protein×4 + carbs×4 + fat×9 ≈ calories. Not free precision but better than guess-the-total. - New macros_confidence field (low/medium/high) — Sonnet rates own certainty so users know when to trust the numbers and the planner can avoid summing low-confidence macros over a 7-day budget. - Future commit: deterministic USDA-FDC backfill into cauldron_food_ metadata for true ingredient-by-ingredient sums. Same shape as the existing density backfill we did in Step 2. Plan generator pool prompt expanded — every recipe entry now carries inline: active=Nm+offhand=Mm eq:oven,stovetop flavor:savory/herby/rich kid=4 ~$5/svg for:weeknight/date-night fiber=Ng / sodium=Nmg ENRICH_VERSION 4→5. Cobb's force re-enrich gets all of these on the existing 222 recipes in one walk. --- cauldron/db.py | 5 ++- cauldron/forge.py | 105 +++++++++++++++++++++++++++++++++++++++++++--- 2 files changed, 104 insertions(+), 6 deletions(-) diff --git a/cauldron/db.py b/cauldron/db.py index ae91939..3c9f60a 100644 --- a/cauldron/db.py +++ b/cauldron/db.py @@ -1714,7 +1714,10 @@ class DB: # sesame,pork} allergen booleans # v4: added pairings, mood scores, leftover_potential, plus second-pass # allergen verification to clean false-positive contains.* booleans - ENRICH_VERSION = 4 + # v5: split estimated_minutes → active+hands_off, equipment[], flavor_profile[], + # kid_friendly_score, fiber_g + sodium_mg, cost_per_serving_estimate, + # occasion_fit[], hecate_quip + ENRICH_VERSION = 5 def get_recipe_meta(self, household_id: int, recipe_slug: str) -> dict | None: with self.conn() as c, c.cursor() as cur: diff --git a/cauldron/forge.py b/cauldron/forge.py index 0d522a7..d2e1afa 100644 --- a/cauldron/forge.py +++ b/cauldron/forge.py @@ -241,6 +241,38 @@ class Forge: extras.append(f"carbs={meta['carbs_g']}g") if meta.get("fat_g"): extras.append(f"fat={meta['fat_g']}g") + if meta.get("fiber_g"): + extras.append(f"fiber={meta['fiber_g']}g") + if meta.get("sodium_mg"): + extras.append(f"sodium={meta['sodium_mg']}mg") + # v5: active/hands-off split lets the planner match a slot's + # available active time (e.g., "Wednesday I have 25 min hands-on") + am = meta.get("active_minutes") + ho = meta.get("hands_off_minutes") + if am is not None and ho is not None: + extras.append(f"active={am}m+offhand={ho}m") + elif am is not None: + extras.append(f"active={am}m") + # Equipment for "no oven this week" / "grill week" filters + eq = meta.get("equipment") or [] + if eq: + extras.append("eq:" + ",".join(eq[:3])) + # Flavor variety so the planner doesn't run 5 spicy nights + fp = meta.get("flavor_profile") or [] + if fp: + extras.append("flavor:" + "/".join(fp[:3])) + # Kid-fit signal for households with kids (Cobb has Leia + Luna) + kf = meta.get("kid_friendly_score") + if isinstance(kf, int) and kf > 0: + extras.append(f"kid={kf}") + # Cost — for budget-week filtering + cs = meta.get("cost_per_serving_estimate") + if cs: + extras.append(f"~${cs}/svg") + # Occasion fit — when is this dish best? + of = meta.get("occasion_fit") or [] + if of: + extras.append("for:" + "/".join(of[:3])) # Allergen flags — short-circuit list of "what's in this" contains = meta.get("contains") or {} if isinstance(contains, dict): @@ -686,6 +718,7 @@ class Forge: ' "protein_g": ,\n' ' "carbs_g": ,\n' ' "fat_g": ,\n' + ' "macros_confidence": "",\n' ' "contains": {\n' ' "dairy": , // milk/cream/butter/cheese/yogurt/whey\n' ' "gluten": , // wheat/barley/rye/regular pasta/bread/flour (not GF)\n' @@ -709,6 +742,18 @@ class Forge: ' "comfort": \n' ' },\n' ' "leftover_potential": ,\n' + ' "active_minutes": ,\n' + ' "hands_off_minutes": ,\n' + ' "equipment": [],\n' + ' "flavor_profile": [<2-5 from: "spicy","sweet","savory","umami","tangy","smoky","herby","citrusy","rich","fresh","bitter","earthy">],\n' + ' "kid_friendly_score": ,\n' + ' "fiber_g": ,\n' + ' "sodium_mg": ,\n' + ' "cost_per_serving_estimate": ,\n' + ' "occasion_fit": [],\n' + ' "hecate_quip": "",\n' ' "summary": "",\n' ' "best_for": ""\n' "}\n\n" @@ -716,11 +761,19 @@ class Forge: "- Return ONLY the JSON object, no markdown fences, no prose.\n" "- Be concrete: 'high-protein' goes in tags ONLY if the recipe genuinely " "qualifies (≥30g protein per serving is a useful threshold).\n" - "- Macros (calories, protein_g, carbs_g, fat_g): best-effort PER-SERVING " - "estimate from the ingredient list and yields. Use rough USDA averages — " - "we want signal not precision. If yields aren't clear, assume 4 servings. " - "If the recipe is a sauce/seasoning/drink with no useful per-serving notion, " - "set them to null.\n" + "- Macros (calories, protein_g, carbs_g, fat_g, fiber_g, sodium_mg): " + "PER-SERVING estimate computed step-by-step. Don't just guess a round " + "number — internally list each major ingredient (the protein, carb, " + "fat, dairy, anything substantial), approximate its contribution per " + "serving in grams using rough USDA averages, sum, then output. " + "Cross-check that protein_g × 4 + carbs_g × 4 + fat_g × 9 ≈ calories. " + "If yields aren't clear, assume 4 servings. If the recipe is a sauce/" + "seasoning/drink with no useful per-serving notion, set them to null.\n" + "- macros_confidence: \"high\" when the recipe has explicit yields and " + "named cuts/quantities, \"medium\" when yields are inferred or some " + "ingredients are unspecified amounts, \"low\" when you had to guess " + "significantly (no yields, free-form 'to taste' amounts dominating, " + "exotic ingredients without clear USDA equivalents).\n" "- contains.* booleans: TRUE if the ingredient appears anywhere in the " "recipe. dairy=true for butter, cream, cheese, milk, yogurt, ghee, whey, " "casein. gluten=true for regular flour/bread/pasta/soy sauce/beer/seitan; " @@ -746,6 +799,35 @@ class Forge: "- leftover_potential 1-5: 1=eat-fresh-only (crispy fries, salads with " "wilting lettuce), 3=fine the next day, 5=actually BETTER as leftovers " "(stews, braises, lasagna).\n" + "- active_minutes vs hands_off_minutes: split the total cook time. " + "Active = chopping, sautéing, stirring, plating — you must be present. " + "Hands-off = rise, marinate, braise, bake unattended, chill, ferment. " + "Together they should sum to ~estimated_minutes. A 4-hour beef stew " + "might be 30 active + 210 hands-off.\n" + "- equipment: pick from the listed set what's REQUIRED to make the " + "recipe. A recipe that uses both stovetop AND oven gets both. Don't " + "include fundamental tools (knife, pan) — only the bigger appliances " + "or cooking modes that gate availability.\n" + "- flavor_profile: 2-5 dominant taste/mood notes. Don't overlist; " + "pick the ones that actually characterize this dish.\n" + "- kid_friendly_score 1-5: 1=adults-only (super spicy, weird textures, " + "olives + capers), 3=most kids will eat, 5=kids LOVE this (mac and " + "cheese, pancakes, chicken nuggets).\n" + "- fiber_g, sodium_mg: per-serving estimates from the ingredient list. " + "Same precision as the other macros — rough USDA averages.\n" + "- cost_per_serving_estimate: rough USD per serving. A bean-and-rice " + "bowl is $2-3, salmon for two is $8-12, a fancy roast is $15+. Best-effort, " + "current-ish (2026) US prices. Set null if too volatile (game meat, " + "regional specialty).\n" + "- occasion_fit: when is this dish AT HOME? A roast chicken can be " + "weeknight AND date-night. Mac and cheese is weeknight + kids-birthday. " + "Be generous but discerning.\n" + "- hecate_quip: one line, ~10-20 words, in Hecate's voice. Not a " + "description of WHAT the dish is — a description of what it FEELS " + "like. Mythic-witch tone, evocative, a little theatrical. Examples: " + "\"Pure midwinter comfort — asks for a fire and quiet.\" / \"Sharp " + "and bright like a Tuesday morning resolution.\" / \"The kind of " + "feast that announces itself.\"\n" "- estimated_minutes: best guess from prep + cook implied by steps. Dishes " "needing rise/marinade time count that time.\n" "- complexity: 'easy' = ≤30 min + ≤7 ingredients + simple technique; " @@ -961,27 +1043,40 @@ def _extract_recipe_meta(forge_result: dict) -> dict: mood_raw = {} mood = {k: _score(mood_raw.get(k)) for k in ("cozy", "summer_fresh", "energizing", "comfort")} + # v5 additions: split-time, equipment, flavor, kid-fit, fiber/sodium/cost, + # occasion fit, hecate's quip return { "tags": _str_list(inner.get("tags")), "cuisine": _str(inner.get("cuisine"), "unknown"), "complexity": _str(inner.get("complexity"), "medium"), "estimated_minutes": _int(inner.get("estimated_minutes")), + "active_minutes": _int_or_none(inner.get("active_minutes")), + "hands_off_minutes": _int_or_none(inner.get("hands_off_minutes")), + "equipment": _str_list(inner.get("equipment")), + "flavor_profile": _str_list(inner.get("flavor_profile")), "meal_type": _str(inner.get("meal_type"), "dinner"), "primary_protein": _str(inner.get("primary_protein"), "none"), "primary_carb": _str(inner.get("primary_carb"), "none"), "veg_forward": _str(inner.get("veg_forward"), "mixed"), "comfort_tier": _str(inner.get("comfort_tier"), "weeknight-easy"), + "kid_friendly_score": _score(inner.get("kid_friendly_score")), "season_fit": _str_list(inner.get("season_fit")) or ["year-round"], + "occasion_fit": _str_list(inner.get("occasion_fit")), "calories": _int_or_none(inner.get("calories")), "protein_g": _int_or_none(inner.get("protein_g")), "carbs_g": _int_or_none(inner.get("carbs_g")), "fat_g": _int_or_none(inner.get("fat_g")), + "macros_confidence": _str(inner.get("macros_confidence"), "medium"), + "fiber_g": _int_or_none(inner.get("fiber_g")), + "sodium_mg": _int_or_none(inner.get("sodium_mg")), + "cost_per_serving_estimate": _int_or_none(inner.get("cost_per_serving_estimate")), "contains": contains, "pairings": pairings, "mood": mood, "leftover_potential": _score(inner.get("leftover_potential")), "summary": _str_long(inner.get("summary")), "best_for": _str_long(inner.get("best_for")), + "hecate_quip": _str_long(inner.get("hecate_quip")), }