v0.3 step 5: lean shopping list — claude on-demand foods + game strip
Two changes:
1. foods catalog grows organically. Switch the canonical seed from the
noisy USDA dump (2462 rows of "'s, classic chicken noodle soup")
to the Sonnet-curated cut (229 clean rows). search_food() is now
exact + case-insensitive — Mealie's parser already canonicalizes
food names household-side, so cauldron just needs to look them up
verbatim. On miss, the /list view calls forge.fetch_food_info() to
ask Sonnet for {density_g_per_ml, default_unit_class, common_size_g,
category}, persists the row with source='claude', and the household's
actual kitchen catalog builds itself out as Abby uses it.
Killer case verified end-to-end: "2 cups + 50g + 1.25 lb rice"
collapses to a single "2.25 lb rice" line on the shopping list once
rice has a density row.
2. Game system stripped from /plan. Scoreboard panel, streak banner,
"first to lock takes the week" / "🏆 you locked this one in" copy
all gone. award_pick_points calls in /api/plan/generate +
/api/plan/regenerate stopped firing. household_scoreboard /
household_streak DB methods kept as dead code; cauldron_pick_points
table left in place — non-destructive, easy to revive later if
gamification comes back. Goal: get the base flow (pick → plan →
list) working for Abby first, layer features on after.
This commit is contained in:
parent
36aba73f66
commit
d649b99aef
6 changed files with 2444 additions and 100 deletions
|
|
@ -215,6 +215,73 @@ class Forge:
|
|||
)
|
||||
|
||||
|
||||
def fetch_food_info(self, name: str, *, model: str | None = None) -> dict:
|
||||
"""Ask Sonnet for density + unit class + common size of a single
|
||||
food. Returns a dict shaped like:
|
||||
|
||||
{"density_g_per_ml": 1.04 | null,
|
||||
"default_unit_class": "mass"|"volume"|"count",
|
||||
"common_size_g": 150.0 | null,
|
||||
"category": "produce"|"dairy"|... | null}
|
||||
|
||||
density_g_per_ml is null when the food doesn't sensibly convert
|
||||
between mass and volume (e.g., whole onions, eggs — these are
|
||||
count-style). common_size_g lets the aggregator handle "1 onion"
|
||||
as a count → mass conversion. Cheap call, cached forever once
|
||||
persisted to cauldron_foods.
|
||||
"""
|
||||
prompt = (
|
||||
f"Give nutritional/cooking metadata for the food: {name!r}.\n\n"
|
||||
"Output JSON ONLY, no prose: "
|
||||
'{"density_g_per_ml": float|null, '
|
||||
'"default_unit_class": "mass"|"volume"|"count", '
|
||||
'"common_size_g": float|null, '
|
||||
'"category": "produce"|"dairy"|"meat"|"grain"|"baking"|"pantry"'
|
||||
'|"spice"|"oil"|"beverage"|"other"|null}\n\n'
|
||||
"Rules:\n"
|
||||
"- density_g_per_ml: typical packed/cooking density. Null if "
|
||||
"the food is count-based (whole onions, eggs).\n"
|
||||
"- default_unit_class: how this food is most often measured "
|
||||
"(salt=mass; milk=volume; egg=count).\n"
|
||||
"- common_size_g: the typical mass of one whole unit (1 onion "
|
||||
"≈ 150g; 1 egg ≈ 50g). Null if the food isn't naturally counted.\n"
|
||||
"- category: best single fit; null if uncertain.\n"
|
||||
)
|
||||
result = self.run(prompt, model=model or "sonnet", timeout_secs=60)
|
||||
return _extract_food_info(result)
|
||||
|
||||
|
||||
def _extract_food_info(forge_result: dict) -> dict:
|
||||
"""Normalize clawdforge wrapper → food info dict. Defensive on shapes."""
|
||||
if not isinstance(forge_result, dict):
|
||||
raise ForgeError("forge result not a dict")
|
||||
inner = forge_result.get("result", forge_result)
|
||||
if isinstance(inner, str):
|
||||
inner = _parse_json_blob(inner)
|
||||
if not isinstance(inner, dict):
|
||||
raise ForgeError(f"forge result not a dict: {str(inner)[:200]}")
|
||||
|
||||
cls = (inner.get("default_unit_class") or "mass").strip().lower()
|
||||
if cls not in ("mass", "volume", "count", "mixed"):
|
||||
cls = "mass"
|
||||
|
||||
def _f(v):
|
||||
if v is None:
|
||||
return None
|
||||
try:
|
||||
x = float(v)
|
||||
return x if x > 0 else None
|
||||
except (TypeError, ValueError):
|
||||
return None
|
||||
|
||||
return {
|
||||
"density_g_per_ml": _f(inner.get("density_g_per_ml")),
|
||||
"default_unit_class": cls,
|
||||
"common_size_g": _f(inner.get("common_size_g")),
|
||||
"category": (inner.get("category") or None) and str(inner["category"])[:64],
|
||||
}
|
||||
|
||||
|
||||
def _extract_plan_slots(forge_result: dict):
|
||||
"""clawdforge wraps its return; the JSON we asked for can sit in a few
|
||||
different shapes. Normalize aggressively."""
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue