from __future__ import annotations from typing import Any, Mapping from app.models import MealType _COMPLETE_MEAL_TERMS = { "bowl", "burger", "burrito", "casserole", "chili", "curry", "fried rice", "lasagna", "noodle", "paella", "pasta", "pizza", "quesadilla", "ramen", "rice bowl", "risotto", "salad", "sandwich", "soup", "spaghetti", "stew", "stir fry", "stir-fry", "taco", "tacos", "wrap", } _CARB_TERMS = { "bread", "bun", "couscous", "farro", "grain", "noodle", "orzo", "pasta", "pita", "potato", "quinoa", "rice", "tortilla", } _VEG_TERMS = { "asparagus", "beans", "broccoli", "brussels", "cabbage", "carrot", "cauliflower", "corn", "greens", "kale", "pepper", "salad", "spinach", "vegetable", "zucchini", } _PROTEIN_TERMS = { "beef", "breast", "chicken", "chop", "cod", "cutlet", "fish", "pork", "salmon", "shrimp", "steak", "tilapia", "tofu", "turkey", } _PAIRINGS_BY_CUISINE = { "asian": ("sesame broccoli", "steamed jasmine rice"), "chinese": ("garlic green beans", "steamed jasmine rice"), "indian": ("roasted cauliflower", "basmati rice"), "italian": ("garlicky green beans", "orzo or crusty bread"), "mediterranean": ("cucumber tomato salad", "warm pita or couscous"), "mexican": ("sauteed peppers and onions", "cilantro lime rice"), "thai": ("cucumber salad", "steamed jasmine rice"), } _DEFAULT_PAIRING = ("roasted broccoli", "rice pilaf or roasted potatoes") def components_with_suggested_sides( recipe: Any, meal_type: MealType | str, base_components: Mapping[str, Any] | None = None, ) -> dict[str, Any]: components = dict(base_components or {}) suggestion = suggest_sides_for_recipe(recipe, meal_type) if suggestion: components["suggested_sides"] = suggestion return components def suggest_sides_for_recipe(recipe: Any, meal_type: MealType | str) -> dict[str, Any] | None: meal_value = meal_type.value if isinstance(meal_type, MealType) else str(meal_type).lower() if meal_value == MealType.BREAKFAST.value: return None explicit_sides = [ side.get("name") for side in (getattr(recipe, "side_dishes", None) or []) if isinstance(side, dict) and side.get("name") ] if explicit_sides: return { "needed": True, "items": explicit_sides[:2], "note": "Use the recipe's recommended side dish pairing.", } text = _recipe_text(recipe) if not _looks_like_simple_protein(recipe, text): return None vegetable, carb = _pairing_for_cuisine(getattr(recipe, "cuisine_tags", None) or []) return { "needed": True, "vegetable": vegetable, "carb": carb, "note": "Simple protein entree; add a vegetable and carb to make it a complete meal.", } def _recipe_text(recipe: Any) -> str: parts = [ getattr(recipe, "name", "") or "", getattr(recipe, "protein_type", "") or "", " ".join(getattr(recipe, "cuisine_tags", None) or []), ] for ingredient in getattr(recipe, "ingredients", None) or []: if isinstance(ingredient, dict): parts.append(str(ingredient.get("name") or ingredient.get("ingredient") or "")) return " ".join(parts).lower() def _looks_like_simple_protein(recipe: Any, text: str) -> bool: if any(term in text for term in _COMPLETE_MEAL_TERMS): return False has_protein = bool(getattr(recipe, "protein_type", None)) or any( term in text for term in _PROTEIN_TERMS ) if not has_protein: return False has_carb = any(term in text for term in _CARB_TERMS) has_veg = any(term in text for term in _VEG_TERMS) return not (has_carb and has_veg) def _pairing_for_cuisine(tags: list[str]) -> tuple[str, str]: lowered = {tag.lower() for tag in tags} for key, pairing in _PAIRINGS_BY_CUISINE.items(): if key in lowered: return pairing return _DEFAULT_PAIRING