feat(backend): recipe enrichment with side dishes & detailed instructions

- Add SideDish/SideDishIngredient schemas and recipe.side_dishes JSONB column
- Add recipe_enrichment.py service using Ollama LLM to:
  - Rewrite vague instructions with specific temps, quantities, timing, sauce breakdowns
  - Suggest 1-2 complementary side dishes with ingredients & prep notes
- Wire enrichment into recipe_ingestion.py discovery pipeline
- Add admin trigger endpoint /api/recipes/{id}/enrich for on-demand enrichment
- Migration 0014: Add side_dishes JSONB to recipe table
- Fix schemas/__init__.py imports: restore RecipeBase/Create/Read exports, add datetime/date for PydanticOptional compatibility
- Deployed to docker-willester and migrated to alembic 0014
This commit is contained in:
2026-05-28 06:42:46 -07:00
parent de51e2e8d3
commit c364b8b222
7 changed files with 243 additions and 1 deletions
+21 -1
View File
@@ -13,6 +13,7 @@ from rapidfuzz import fuzz
from sqlalchemy.dialects.postgresql import insert as _pg_insert
from sqlalchemy.orm import Session
from app.services.recipe_enrichment import RecipeEnrichmentService
from app.models import Ingredient, Recipe
logger = logging.getLogger(__name__)
@@ -24,6 +25,9 @@ _MAX_INGREDIENTS = 20
class RecipeIngestionService:
"""Ingest external recipes into our database."""
def __init__(self) -> None:
self.enrichment = RecipeEnrichmentService()
def ingest(
self,
db: Session,
@@ -78,6 +82,21 @@ class RecipeIngestionService:
top_signals = [s["value"] for s in analysis_dict.get("positive_signals", [])]
discovery_reason = f"Matched queries: {', '.join(queries[:2])}. Signals: {', '.join(top_signals[:2])}."
# --- Enrich: detailed instructions + side dishes ---
enriched = self.enrichment.enrich(ext.name, mapped_ingredients, instructions)
final_instructions = enriched.get("instructions") or instructions
side_dishes = enriched.get("side_dishes") or []
# Append side-dish ingredients to the main ingredient list so shopping lists pick them up
for side in side_dishes:
for sd_ing in side.get("ingredients", []):
mapped_ingredients.append({
"ingredient_id": None,
"name": sd_ing.get("name", ""),
"qty": sd_ing.get("qty"),
"unit": sd_ing.get("unit", ""),
})
recipe = Recipe(
id=_uuid_mod.uuid4(),
family_profile_id=family_profile_id,
@@ -93,7 +112,8 @@ class RecipeIngestionService:
protein_type=ext.protein_type,
calories_per_serving=ext.calories_per_serving,
ingredients=mapped_ingredients,
instructions=instructions,
side_dishes=side_dishes,
instructions=final_instructions,
source_url=ext.source_url,
external_source=ext.external_source,
external_id=ext.external_id,