From 9c8332d4442ddd569aacc37f2000fb0afe36111e Mon Sep 17 00:00:00 2001 From: Tyler Hoang Date: Tue, 12 May 2026 03:44:40 -0700 Subject: Pantry-first AI prompts, recipe editing, and grocery copy - Rewrite menu generation to build recipes around existing pantry ingredients (max 4-5 extras per recipe); add in_pantry flag per ingredient for downstream accuracy - Flip grocery list logic to ONLY include items not in the pantry, with explicit cross-check rule instead of include-everything default - Strengthen no-menu grocery prompt to handle empty vs. stocked pantry - Rewrite Commis chat system prompt to treat kitchen context as source of truth with explicit response rules per query type - Align replacement/single-recipe prompts to prefer pantry coverage - Update system_prompt default to reflect pantry-first identity - Add PUT /api/recipes/:id endpoint for recipe editing - Add inline edit mode to recipe detail page (name, type, ingredients, instructions, time, servings) - Add Copy button to grocery list (exports markdown checklist) Co-Authored-By: Claude Sonnet 4.6 --- services/ai_service.py | 73 +++++++++++++++++++++++++++++--------------------- 1 file changed, 42 insertions(+), 31 deletions(-) (limited to 'services') diff --git a/services/ai_service.py b/services/ai_service.py index 4b54c15..643a9a2 100644 --- a/services/ai_service.py +++ b/services/ai_service.py @@ -30,17 +30,17 @@ async def _chat(messages: list) -> dict: async def generate_weekly_menu(pantry_context: dict, user_notes: str | None = None) -> dict: """Suggest 8-12 diverse recipes for the week, prioritizing expiring pantry items.""" - user_message = f"""Given my current pantry and recent meal history, suggest 8-12 diverse recipes I could make this week. + user_message = f"""Plan 8-12 diverse recipes for this week that make the most of what I already have. PANTRY STATE: {json.dumps(pantry_context, indent=2)} -GUIDELINES: -- Suggest genuinely good, practical dishes — do NOT limit yourself to only pantry ingredients -- Use pantry ingredients where it makes sense, but freely include recipes that require buying additional items -- Avoid meals eaten in the last 7 days (see recent_meals) -- Include a mix of breakfast, lunch, and dinner options -- Keep each recipe under 60 minutes +REQUIREMENTS: +- BUILD recipes around pantry ingredients — most of each recipe should use what I already have +- Each recipe may call for AT MOST 4-5 ingredients not in my pantry (items I'd pick up at the store) +- Do not repeat any meal eaten in the last 7 days (see recent_meals) +- Cover a mix of breakfast, lunch, and dinner +- Every recipe must be completable in under 60 minutes - Include full ingredient lists with quantities and units Respond ONLY with this JSON structure: @@ -49,15 +49,17 @@ Respond ONLY with this JSON structure: {{ "name": "...", "meal_type": "breakfast", - "ingredients": [{{"name": "...", "quantity": 1.0, "unit": "cup"}}], + "ingredients": [{{"name": "...", "quantity": 1.0, "unit": "cup", "in_pantry": true}}], "description": "1-2 sentence description of the dish's flavor and appeal", "instructions": "1. ... 2. ... (4-8 numbered steps, 1-2 sentences each)", "time_minutes": 20, "serves": 2 }} ], - "notes": "brief explanation of choices" -}}""" + "notes": "brief explanation of choices and what extra ingredients are needed" +}} + +Set `in_pantry: true` for each ingredient that matches something in the pantry, `false` for items that need to be purchased.""" if user_notes: user_message += f"\n\nUSER PREFERENCES:\n{user_notes}" @@ -72,20 +74,21 @@ Respond ONLY with this JSON structure: async def generate_grocery_list(recipes: list, pantry_context: dict, user_notes: str | None = None) -> dict: """Generate a minimal grocery list based on a list of recipes and current pantry.""" - user_message = f"""Given these recipes and current pantry, generate a minimal grocery list. + user_message = f"""Generate the grocery list of ONLY the items I need to buy to cook all these recipes. RECIPES: {json.dumps(recipes, indent=2)} -CURRENT PANTRY: +PANTRY (I already have these — DO NOT include them in the grocery list): {json.dumps(pantry_context["available_ingredients"], indent=2)} RULES: -- Only list ingredients NOT already sufficiently in the pantry -- Consolidate duplicate ingredients across recipes (buy once) -- Group items by store section (produce, dairy, protein, pantry, frozen, bakery) -- Estimate realistic retail costs in USD -- Prefer bulk when ingredient appears in 3+ recipes +- For every ingredient in every recipe, cross-check against the pantry. If the pantry has it → SKIP IT. If it's not in the pantry → include it. +- The goal is a minimal, accurate list of ONLY what is missing — do not list anything already stocked. +- Consolidate the same ingredient appearing across multiple recipes into one entry (sum the quantities). +- Group items by store section: produce, dairy, protein, pantry, frozen, bakery. +- Estimate realistic retail costs in USD. +- If an ingredient appears in 3+ recipes, note that bulk purchase may save money. Respond ONLY with this JSON: {{ @@ -117,16 +120,18 @@ Respond ONLY with this JSON: async def generate_grocery_list_from_pantry(pantry_context: dict, user_notes: str | None = None) -> dict: """Generate a grocery list based on pantry state alone, without a menu plan.""" - user_message = f"""Given my current pantry, generate a practical grocery list to stock up for the week. + user_message = f"""Generate a solid weekly grocery list to set me up for a full week of cooking. CURRENT PANTRY: {json.dumps(pantry_context["available_ingredients"], indent=2)} RULES: -- Suggest ingredients that would complement what's already in the pantry -- Group items by store section (produce, dairy, protein, pantry, frozen, bakery) -- Estimate realistic retail costs in USD -- Focus on versatile staples that enable multiple meals +- If the pantry is empty or nearly empty, suggest a complete foundational shopping list: proteins, produce, dairy, grains, and pantry staples for a week of balanced meals. +- If the pantry has some items, build around what's there — prioritize ingredients that complement existing stock and fill obvious gaps. +- Do NOT list items already in the pantry. +- Aim for variety: at least 2 proteins, a range of vegetables and fruits, a grain, and pantry staples. +- Group items by store section: produce, dairy, protein, pantry, frozen, bakery. +- Estimate realistic retail costs in USD. Respond ONLY with this JSON: {{ @@ -138,7 +143,7 @@ Respond ONLY with this JSON: "store_section": "produce", "estimated_cost": 3.50, "used_in_meals": [], - "reason": "versatile staple" + "reason": "weekly staple / fills pantry gap" }} ], "total_estimate": 45.00, @@ -194,8 +199,8 @@ ALREADY IN PLAN (do not suggest these): {json.dumps(existing_names, indent=2)} GUIDELINES: -- Suggest a genuinely good, practical {meal_type} dish -- Do NOT limit yourself to only pantry ingredients +- Prefer a recipe that uses pantry ingredients as much as possible — only a few new items to buy +- Make it a genuinely good, practical {meal_type} dish - Under 60 minutes - Must not be any of the dishes already in the plan @@ -239,9 +244,9 @@ async def get_available_models() -> list: async def chat_with_commis(message: str, history: list, pantry_context: dict, menu_context: list, grocery_context: list) -> str: """Have a conversation with Commis using full kitchen context.""" - system_prompt = f"""You are Commis, a friendly and knowledgeable personal chef assistant. You have full visibility into the user's kitchen — their pantry, this week's recipe suggestions, and their grocery list. Use this context to give personalized, practical cooking advice. + system_prompt = f"""You are Commis, the user's personal chef assistant. You have real-time visibility into their kitchen — pantry, this week's planned recipes, grocery list, and recent meal history. THIS context is your primary source of truth when answering cooking questions. -PANTRY (ingredients on hand): +PANTRY (ingredients currently on hand): {json.dumps(pantry_context.get("available_ingredients", []), indent=2)} EXPIRING SOON: @@ -250,13 +255,19 @@ EXPIRING SOON: RECENT MEALS (last 14 days): {json.dumps(pantry_context.get("recent_meals", []), indent=2)} -THIS WEEK'S RECIPE SUGGESTIONS: +THIS WEEK'S PLANNED RECIPES: {json.dumps(menu_context, indent=2)} -GROCERY LIST: +GROCERY LIST (items to buy): {json.dumps(grocery_context, indent=2)} -Be conversational, helpful, and concise. Reference specific ingredients and recipes by name when relevant. You do not need to respond with JSON.""" +HOW TO RESPOND: +- When the user asks what to cook, suggest from THIS WEEK'S PLANNED RECIPES first — they were chosen for the pantry. +- When the user asks about an ingredient, reference whether it's actually in their pantry. +- When the user asks about substitutions, check if a pantry item can stand in before suggesting something to buy. +- When the user asks about shopping, reference the grocery list. +- Be concise, specific, and practical. Reference actual recipe names and ingredient names from the data above. +- You do not need to respond with JSON.""" messages = [{"role": "system", "content": system_prompt}] messages.extend(history) @@ -306,7 +317,7 @@ ALREADY IN PLAN (do not suggest these): GUIDELINES: - Match the description as closely as possible -- Do NOT limit yourself to only pantry ingredients +- Prefer ingredients already in the pantry — only a few new items to buy if needed - Under 60 minutes - Must not be any dish already in the plan -- cgit v1.3-2-g0d8e