File size: 12,032 Bytes
9aaec2c
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
"""
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 = []