From 5f094eb37a89364d3427bd7eb2c2f56940c8b095 Mon Sep 17 00:00:00 2001 From: pyr0ball Date: Sun, 12 Apr 2026 13:58:03 -0700 Subject: [PATCH] =?UTF-8?q?feat(services/bsl):=20add=20llm=5Ftiming=20?= =?UTF-8?q?=E2=80=94=20estimate=20cook=20times=20via=20LLM=20for=20missing?= =?UTF-8?q?=20corpus=20data=20(Paid/BYOK)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/services/meal_plan/llm_timing.py | 61 ++++++++++++++++++++++++++++ 1 file changed, 61 insertions(+) create mode 100644 app/services/meal_plan/llm_timing.py diff --git a/app/services/meal_plan/llm_timing.py b/app/services/meal_plan/llm_timing.py new file mode 100644 index 0000000..7847c69 --- /dev/null +++ b/app/services/meal_plan/llm_timing.py @@ -0,0 +1,61 @@ +# app/services/meal_plan/llm_timing.py +# BSL 1.1 — LLM feature +"""Estimate cook times for recipes missing corpus prep/cook time fields. + +Used only when tier allows `meal_plan_llm_timing`. Falls back gracefully +when LLMRouter is unavailable. +""" +from __future__ import annotations + +import logging + +logger = logging.getLogger(__name__) + +_TIMING_PROMPT = """\ +You are a practical cook. Given a recipe name and its ingredients, estimate: +1. prep_time: minutes of active prep work (chopping, mixing, etc.) +2. cook_time: minutes of cooking (oven, stovetop, etc.) + +Respond with ONLY two integers on separate lines: +prep_time +cook_time + +If you cannot estimate, respond with: +0 +0 +""" + + +def estimate_timing(recipe_name: str, ingredients: list[str], router) -> tuple[int | None, int | None]: + """Return (prep_minutes, cook_minutes) for a recipe using LLMRouter. + + Returns (None, None) if the router is unavailable or the response is + unparseable. Never raises. + + Args: + recipe_name: Name of the recipe. + ingredients: List of raw ingredient strings from the corpus. + router: An LLMRouter instance (from circuitforge_core.llm). + """ + if router is None: + return None, None + + ingredient_list = "\n".join(f"- {i}" for i in (ingredients or [])[:15]) + prompt = f"Recipe: {recipe_name}\n\nIngredients:\n{ingredient_list}" + + try: + response = router.complete( + system=_TIMING_PROMPT, + user=prompt, + max_tokens=16, + temperature=0.0, + ) + lines = response.strip().splitlines() + prep = int(lines[0].strip()) if lines else 0 + cook = int(lines[1].strip()) if len(lines) > 1 else 0 + if prep == 0 and cook == 0: + return None, None + return prep or None, cook or None + except Exception as exc: + logger.debug("LLM timing estimation failed for %r: %s", recipe_name, exc) + return None, None