Spaces:
Runtime error
Runtime error
| """ | |
| AI Assistant Service for ChefCode | |
| Handles intent detection, conversational AI, and action orchestration | |
| """ | |
| import os | |
| import json | |
| import logging | |
| from typing import Dict, Any, List, Optional | |
| from openai import OpenAI | |
| from pydantic import BaseModel | |
| logger = logging.getLogger(__name__) | |
| class IntentResult(BaseModel): | |
| """Structure for intent detection result""" | |
| intent: str | |
| confidence: float | |
| entities: Dict[str, Any] | |
| requires_confirmation: bool = False | |
| response_message: str = "" | |
| class AIAssistantService: | |
| """ | |
| AI Assistant for natural language command processing | |
| Uses GPT-4o-mini for intent detection and conversational responses | |
| """ | |
| # Supported intents | |
| INTENTS = { | |
| "add_inventory": "Add items to inventory", | |
| "update_inventory": "Update inventory quantities", | |
| "delete_inventory": "Remove items from inventory", | |
| "query_inventory": "Query inventory status", | |
| "add_recipe": "Add a new recipe manually", | |
| "edit_recipe": "Edit existing recipe", | |
| "delete_recipe": "Delete a recipe", | |
| "search_recipe_web": "Search recipes online", | |
| "show_recipe": "Display specific recipe", | |
| "import_recipe": "Import recipe from search results", | |
| "show_catalogue": "Show recipe catalogue", | |
| "filter_catalogue": "Filter recipes by category", | |
| "general_query": "General questions", | |
| "unknown": "Cannot determine intent" | |
| } | |
| def __init__(self): | |
| """Initialize OpenAI client""" | |
| api_key = os.getenv("OPENAI_API_KEY") | |
| if not api_key: | |
| raise ValueError("OPENAI_API_KEY environment variable not set") | |
| self.client = OpenAI(api_key=api_key) | |
| self.model = "gpt-4o-mini" | |
| self.conversation_context = [] # Store recent conversation for context | |
| async def detect_intent(self, user_input: str, context: Optional[Dict] = None) -> IntentResult: | |
| """ | |
| Detect user intent from natural language input | |
| Args: | |
| user_input: User's natural language command | |
| context: Optional conversation context | |
| Returns: | |
| IntentResult with detected intent and extracted entities | |
| """ | |
| try: | |
| system_prompt = self._build_intent_detection_prompt() | |
| # Add context if available | |
| context_info = "" | |
| if context: | |
| context_info = f"\n\nConversation context: {json.dumps(context)}" | |
| user_prompt = f"""Analyze this user command and return a structured JSON response: | |
| User Input: "{user_input}"{context_info} | |
| Return JSON with this structure: | |
| {{ | |
| "intent": "intent_name", | |
| "confidence": 0.95, | |
| "entities": {{ | |
| // Extracted entities based on intent | |
| }}, | |
| "requires_confirmation": true/false, | |
| "response_message": "Conversational response to user" | |
| }} | |
| IMPORTANT: Return ONLY the JSON, no markdown formatting.""" | |
| response = self.client.chat.completions.create( | |
| model=self.model, | |
| messages=[ | |
| {"role": "system", "content": system_prompt}, | |
| {"role": "user", "content": user_prompt} | |
| ], | |
| temperature=0.3, | |
| max_tokens=1000 | |
| ) | |
| result_text = response.choices[0].message.content.strip() | |
| # Clean markdown if present | |
| if result_text.startswith("```"): | |
| lines = result_text.split('\n') | |
| result_text = '\n'.join([l for l in lines if not l.startswith("```")]) | |
| result_dict = json.loads(result_text) | |
| return IntentResult(**result_dict) | |
| except Exception as e: | |
| logger.error(f"Intent detection error: {str(e)}") | |
| return IntentResult( | |
| intent="unknown", | |
| confidence=0.0, | |
| entities={}, | |
| response_message=f"I'm not sure what you mean. Could you rephrase that?" | |
| ) | |
| async def parse_recipe_from_text(self, user_input: str) -> Dict[str, Any]: | |
| """ | |
| Parse recipe details from natural language | |
| Example: "Add a recipe called Pizza with flour 100 kg and tomato sauce 200 ml" | |
| Returns: | |
| { | |
| "recipe_name": "Pizza", | |
| "ingredients": [ | |
| {"name": "flour", "quantity": 100, "unit": "kg"}, | |
| {"name": "tomato sauce", "quantity": 200, "unit": "ml"} | |
| ], | |
| "yield_qty": 1, | |
| "yield_unit": "piece" | |
| } | |
| """ | |
| try: | |
| prompt = f"""You are a recipe parsing expert. Extract ALL ingredient information from this command. | |
| User Input: "{user_input}" | |
| CRITICAL RULES: | |
| 1. Extract the recipe name | |
| 2. For EACH ingredient, extract: | |
| - name (ingredient name) | |
| - quantity (numeric value, if missing use null) | |
| - unit (measurement unit like kg, g, ml, liters, pieces, etc. If missing use null) | |
| 3. Extract yield if mentioned (default: null) | |
| EXAMPLES: | |
| "Add recipe Pizza with flour 500 grams and tomato 200 ml" | |
| → {{"recipe_name": "Pizza", "ingredients": [{{"name": "flour", "quantity": 500, "unit": "grams"}}, {{"name": "tomato", "quantity": 200, "unit": "ml"}}], "yield_qty": null, "yield_unit": null}} | |
| "Add recipe spaghetti with flour and salt" | |
| → {{"recipe_name": "spaghetti", "ingredients": [{{"name": "flour", "quantity": null, "unit": null}}, {{"name": "salt", "quantity": null, "unit": null}}], "yield_qty": null, "yield_unit": null}} | |
| Return ONLY valid JSON, no markdown, no explanation: | |
| {{ | |
| "recipe_name": "string", | |
| "ingredients": [ | |
| {{"name": "string", "quantity": number or null, "unit": "string or null"}}, | |
| ... | |
| ], | |
| "yield_qty": number or null, | |
| "yield_unit": "string or null", | |
| "instructions": "string or empty" | |
| }}""" | |
| response = self.client.chat.completions.create( | |
| model=self.model, | |
| messages=[ | |
| {"role": "system", "content": "You are a precise recipe data extractor. Always return valid JSON."}, | |
| {"role": "user", "content": prompt} | |
| ], | |
| temperature=0.1, | |
| max_tokens=1000 | |
| ) | |
| result = response.choices[0].message.content.strip() | |
| # Clean markdown | |
| if result.startswith("```"): | |
| lines = result.split('\n') | |
| result = '\n'.join([l for l in lines if not l.startswith("```")]) | |
| parsed = json.loads(result) | |
| # Convert null to proper values | |
| for ing in parsed.get('ingredients', []): | |
| if ing.get('quantity') is None: | |
| ing['quantity'] = None | |
| if ing.get('unit') is None: | |
| ing['unit'] = None | |
| return parsed | |
| except Exception as e: | |
| logger.error(f"Recipe parsing error: {str(e)}") | |
| raise ValueError(f"Could not parse recipe: {str(e)}") | |
| async def generate_response(self, intent: str, action_result: Dict[str, Any]) -> str: | |
| """ | |
| Generate a conversational response based on the action result | |
| Args: | |
| intent: The detected intent | |
| action_result: Result from the action handler | |
| Returns: | |
| Conversational response string | |
| """ | |
| try: | |
| prompt = f"""Generate a short, friendly response for this action: | |
| Intent: {intent} | |
| Action Result: {json.dumps(action_result)} | |
| Rules: | |
| - Be conversational and concise (max 2 sentences) | |
| - Use emojis sparingly for emphasis | |
| - Confirm what was done | |
| - If error, be helpful and suggest alternatives | |
| Return only the response text, nothing else.""" | |
| response = self.client.chat.completions.create( | |
| model=self.model, | |
| messages=[ | |
| {"role": "system", "content": "You are ChefCode's friendly AI assistant. Be concise and helpful."}, | |
| {"role": "user", "content": prompt} | |
| ], | |
| temperature=0.7, | |
| max_tokens=150 | |
| ) | |
| return response.choices[0].message.content.strip() | |
| except Exception as e: | |
| logger.error(f"Response generation error: {str(e)}") | |
| return "Action completed." if action_result.get("success") else "Something went wrong." | |
| def _build_intent_detection_prompt(self) -> str: | |
| """Build the system prompt for intent detection with examples""" | |
| return """You are ChefCode's intelligent assistant. Analyze user commands and detect their intent. | |
| SUPPORTED INTENTS: | |
| 📦 INVENTORY: | |
| - add_inventory: Add items to inventory | |
| Example: "Add 5 kg of rice at 2.50 euros" | |
| Entities: {"item_name": "rice", "quantity": 5, "unit": "kg", "price": 2.50} | |
| IMPORTANT: Always extract price if mentioned (at, for, cost, price, euro, dollar, etc.) | |
| - update_inventory: Update quantities | |
| Example: "Update flour to 10 kg" | |
| Entities: {"item_name": "flour", "quantity": 10, "unit": "kg"} | |
| - delete_inventory: Remove items | |
| Example: "Remove tomatoes from inventory" | |
| Entities: {"item_name": "tomatoes"} | |
| - query_inventory: Check stock | |
| Example: "How much rice do we have?" | |
| Entities: {"item_name": "rice"} | |
| 🍳 RECIPE MANAGEMENT: | |
| - add_recipe: Create new recipe manually | |
| Example: "Add a recipe called Pizza with flour 100 kg and cheese 50 kg" | |
| Entities: {"recipe_name": "Pizza", "raw_text": "...full input..."} | |
| - edit_recipe: Modify existing recipe (add/remove/change ingredient) | |
| Example: "Edit recipe Pizza by adding 2 grams of salt" | |
| Entities: {"recipe_name": "Pizza", "action": "adding", "ingredient_name": "salt", "quantity": "2", "unit": "grams"} | |
| Example: "Remove flour from Pizza recipe" | |
| Entities: {"recipe_name": "Pizza", "action": "remove", "ingredient_name": "flour"} | |
| Example: "Change tomatoes in Pizza to 500 grams" | |
| Entities: {"recipe_name": "Pizza", "action": "change", "ingredient_name": "tomatoes", "quantity": "500", "unit": "grams"} | |
| - delete_recipe: Remove recipe | |
| Example: "Delete the recipe Pasta" | |
| Entities: {"recipe_name": "Pasta"} | |
| - search_recipe_web: Search recipes online | |
| Example: "Search pasta recipes" or "Find Italian recipes" | |
| Entities: {"query": "pasta", "filters": {"cuisine": "Italian"}} | |
| - show_recipe: Display specific recipe | |
| Example: "Show me the Pizza recipe" | |
| Entities: {"recipe_name": "Pizza"} | |
| - import_recipe: Import from search results | |
| Example: "Import the second one" or "Import that recipe" | |
| Entities: {"index": 2, "recipe_id": "..."} | |
| 📚 CATALOGUE: | |
| - show_catalogue: Show all recipes | |
| Example: "Show all recipes" or "Open recipe catalogue" | |
| Entities: {} | |
| - filter_catalogue: Filter by category | |
| Example: "Show dessert recipes" | |
| Entities: {"category": "dessert"} | |
| ❓ OTHER: | |
| - general_query: General questions | |
| - unknown: Cannot determine | |
| RULES: | |
| 1. Set requires_confirmation=true for destructive actions (add, update, delete) | |
| 2. Extract ALL relevant entities from the input | |
| 3. Be conversational in response_message | |
| 4. If ambiguous, ask clarifying questions | |
| 5. For numbers, always extract both quantity and unit | |
| 6. For recipe commands, capture the full raw text for later parsing | |
| Return JSON only, no markdown.""" | |
| def add_to_context(self, role: str, content: str): | |
| """Add message to conversation context""" | |
| self.conversation_context.append({"role": role, "content": content}) | |
| # Keep only last 10 messages | |
| if len(self.conversation_context) > 10: | |
| self.conversation_context = self.conversation_context[-10:] | |
| def clear_context(self): | |
| """Clear conversation context""" | |
| self.conversation_context = [] | |