import ollama import json import asyncio from config import settings SYSTEM_PROMPT = """You are a professional chef assistant. You MUST respond with valid JSON only — no markdown, no explanation outside the JSON. You prioritize: 1. Using ingredients that expire soonest 2. Nutritional variety across the week 3. Avoiding meals eaten in the past 7 days 4. Practical home recipes under 60 minutes""" def _get_client(): return ollama.Client(host=settings.ollama_host) def _chat_sync(messages: list) -> str: client = _get_client() response = client.chat( model=settings.model_name, messages=messages, format="json", options={"temperature": 0.7, "num_predict": 4096}, ) return response["message"]["content"] async def _chat(messages: list) -> dict: loop = asyncio.get_event_loop() raw = await loop.run_in_executor(None, lambda: _chat_sync(messages)) try: return json.loads(raw) except json.JSONDecodeError as e: raise ValueError(f"Ollama returned invalid JSON: {e}. Raw: {raw[:200]}") async def generate_weekly_menu(pantry_context: dict) -> dict: """Generate a 7-day rotating meal plan based on pantry state.""" user_message = f"""Given the following pantry and recent meal history, generate a 7-day rotating meal plan. PANTRY STATE: {json.dumps(pantry_context, indent=2)} RULES: - Each day needs breakfast, lunch, and dinner - No meal should repeat within the same week - No meal should repeat a meal eaten in the last 7 days (see recent_meals) - Prioritize ingredients expiring within 3 days (see expiring_soon) - Each recipe should only use pantry ingredients OR common staples (salt, pepper, oil, etc.) - Keep each meal practical — under 60 minutes Respond ONLY with this JSON structure: {{ "week_plan": {{ "monday": {{ "breakfast": {{"name": "...", "ingredients": ["item1", "item2"], "instructions": "...", "time_minutes": 20}}, "lunch": {{"name": "...", "ingredients": [...], "instructions": "...", "time_minutes": 30}}, "dinner": {{"name": "...", "ingredients": [...], "instructions": "...", "time_minutes": 45}} }}, "tuesday": {{ ... }}, "wednesday": {{ ... }}, "thursday": {{ ... }}, "friday": {{ ... }}, "saturday": {{ ... }}, "sunday": {{ ... }} }}, "notes": "brief explanation of choices" }}""" messages = [ {"role": "system", "content": SYSTEM_PROMPT}, {"role": "user", "content": user_message}, ] return await _chat(messages) async def generate_grocery_list(menu_plan: dict, pantry_context: dict) -> dict: """Generate a minimal grocery list based on the menu plan and current pantry.""" user_message = f"""Given the weekly meal plan and current pantry, generate a minimal grocery list. MEAL PLAN: {json.dumps(menu_plan, indent=2)} CURRENT PANTRY: {json.dumps(pantry_context["available_ingredients"], indent=2)} RULES: - Only list ingredients NOT already sufficiently in the pantry - Consolidate duplicate ingredients across meals (buy once for the week) - Group items by store section (produce, dairy, protein, pantry, frozen, bakery) - Estimate realistic retail costs in USD - Prefer bulk when ingredient appears in 3+ meals Respond ONLY with this JSON: {{ "items": [ {{ "name": "...", "quantity": 2.0, "unit": "lbs", "store_section": "produce", "estimated_cost": 3.50, "used_in_meals": ["Chicken Stir Fry", "Chicken Soup"], "reason": "not in pantry" }} ], "total_estimate": 45.00, "shopping_notes": "..." }}""" messages = [ {"role": "system", "content": SYSTEM_PROMPT}, {"role": "user", "content": user_message}, ] return await _chat(messages) async def suggest_recipe(pantry_context: dict) -> dict: """Suggest one recipe using available pantry ingredients.""" user_message = f"""Suggest ONE recipe I can make right now using only these pantry ingredients (plus common staples). PANTRY: {json.dumps(pantry_context["available_ingredients"], indent=2)} Respond ONLY with this JSON: {{ "recipe": {{ "name": "...", "meal_type": "dinner", "ingredients": [{{"name": "...", "quantity": 1.0, "unit": "cup"}}], "instructions": "Step 1... Step 2...", "time_minutes": 30, "serves": 2 }} }}""" messages = [ {"role": "system", "content": SYSTEM_PROMPT}, {"role": "user", "content": user_message}, ] return await _chat(messages) async def get_available_models() -> list: """Get list of available Ollama model names.""" def _list_models_sync(): client = _get_client() try: models_resp = client.list() return [m["name"] for m in models_resp.get("models", [])] except Exception: return [] loop = asyncio.get_event_loop() return await loop.run_in_executor(None, _list_models_sync)