enrich v5: more agent context — cooking, kid-fit, expanded macros, occasion,
hecate's quip + macro confidence + chain-of-thought macro estimation
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.
This commit is contained in:
parent
89f33f237c
commit
1445e0cbab
2 changed files with 104 additions and 6 deletions
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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": <int per-serving estimate or null>,\n'
|
||||
' "carbs_g": <int per-serving estimate or null>,\n'
|
||||
' "fat_g": <int per-serving estimate or null>,\n'
|
||||
' "macros_confidence": "<low|medium|high>",\n'
|
||||
' "contains": {\n'
|
||||
' "dairy": <bool>, // milk/cream/butter/cheese/yogurt/whey\n'
|
||||
' "gluten": <bool>, // wheat/barley/rye/regular pasta/bread/flour (not GF)\n'
|
||||
|
|
@ -709,6 +742,18 @@ class Forge:
|
|||
' "comfort": <int 1-5: how nostalgic/comforting this is>\n'
|
||||
' },\n'
|
||||
' "leftover_potential": <int 1-5: 1=eat-now-only, 5=tomorrow lunch=this+++>,\n'
|
||||
' "active_minutes": <int hands-on prep+cook time you must be present for>,\n'
|
||||
' "hands_off_minutes": <int unattended time — rise/marinade/braise/bake>,\n'
|
||||
' "equipment": [<from: "oven","stovetop","grill","instant-pot","slow-cooker",\n'
|
||||
' "sheet-pan","cast-iron","air-fryer","food-processor","blender","mixer",\n'
|
||||
' "smoker","sous-vide","no-cook">],\n'
|
||||
' "flavor_profile": [<2-5 from: "spicy","sweet","savory","umami","tangy","smoky","herby","citrusy","rich","fresh","bitter","earthy">],\n'
|
||||
' "kid_friendly_score": <int 1-5: 1=adult-only, 5=kids-love-it>,\n'
|
||||
' "fiber_g": <int per-serving fiber estimate, or null>,\n'
|
||||
' "sodium_mg": <int per-serving sodium estimate, or null>,\n'
|
||||
' "cost_per_serving_estimate": <int rough USD per serving, or null>,\n'
|
||||
' "occasion_fit": [<from: "weeknight","weekend","brunch","date-night","party","picnic","camping","holiday","game-day","kids-birthday","quiet-night-in">],\n'
|
||||
' "hecate_quip": "<one-line voice description in Hecate\'s mythic-witch tone — what does this dish FEEL like? Sample: \\"Pure midwinter comfort — the kind of meal that asks for a fire and quiet.\\">",\n'
|
||||
' "summary": "<one-line vibe — what KIND of meal is this>",\n'
|
||||
' "best_for": "<short phrase: when is this the right pick>"\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")),
|
||||
}
|
||||
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue