File size: 20,032 Bytes
880d7e9
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
# Groq AI Helper for Natural Language Understanding with Structured Output

from groq import Groq
from pydantic import BaseModel, Field
from typing import Optional, Literal
import re
import json
from voice_config import GROQ_API_KEY, USE_GROQ_AI


# Pydantic Models for Structured Output
class RequestTypeResponse(BaseModel):
    intent: Literal["exchange", "return"] = Field(description="Whether customer wants exchange or return")
    confidence: Literal["high", "medium", "low"] = Field(description="Confidence level")
    reasoning: str = Field(description="Brief explanation")


class OrderIdResponse(BaseModel):
    order_id: Optional[str] = Field(description="Order ID extracted, null if not found")
    found: bool = Field(description="Whether order ID was found")
    interpretation: str = Field(description="How it was interpreted")


class ReasonResponse(BaseModel):
    reason: str = Field(description="Professional reason in 2nd person (You...), max 10 words")
    category: Literal["size", "defect", "wrong_item", "preference", "delivery", "other"] = Field(description="Issue category")


class PreferenceResponse(BaseModel):
    preference: str = Field(description="Product description, max 15 words")
    type: Literal["size", "color", "model", "feature", "other"] = Field(description="Preference type")


class ConfirmationResponse(BaseModel):
    confirmed: bool = Field(description="True if confirming, False if declining")
    confidence: Literal["high", "medium", "low"] = Field(description="Confidence level")


class CorrectionField(BaseModel):
    field: Literal["request_type", "order_id", "reason", "exchange_preference", "everything"] = Field(description="Which field the customer wants to correct")
    reasoning: str = Field(description="Why this field needs correction")


class GroqAI:
    def __init__(self):
        print(f"\n{'='*60}")
        print("πŸ”§ INITIALIZING GROQ AI")
        print(f"USE_GROQ_AI = {USE_GROQ_AI}")
        print(f"GROQ_API_KEY = {'SET ('+str(len(GROQ_API_KEY))+' chars)' if GROQ_API_KEY else 'NOT SET'}")
        print(f"{'='*60}\n")
        
        self.enabled = USE_GROQ_AI and GROQ_API_KEY
        if self.enabled:
            try:
                self.client = Groq(api_key=GROQ_API_KEY)
                self.model_name = "llama-3.3-70b-versatile"  # Fast and powerful model
                print("βœ… Groq AI initialized successfully")
            except Exception as e:
                print(f"⚠️ Groq AI initialization failed: {e}")
                self.enabled = False
        else:
            print("⚠️ Groq AI is DISABLED - will use basic pattern matching")
            if not USE_GROQ_AI:
                print("   Reason: USE_GROQ_AI is False")
            if not GROQ_API_KEY:
                print("   Reason: GROQ_API_KEY is not set")
    
    def extract_request_type(self, user_text, question_asked="Would you like exchange or return?"):
        print(f"\n{'='*60}")
        print(f"🎯 EXTRACT_REQUEST_TYPE")
        print(f"πŸ“ User Input (raw): '{user_text}'")
        print(f"πŸ“ User Input (repr): {repr(user_text)}")
        print(f"πŸ“ Input length: {len(user_text)} chars")
        print(f"❓ Question: {question_asked}")
        print(f"πŸ”Œ Groq Enabled: {self.enabled}")
        print(f"{'='*60}")
        
        # Clean input
        user_text_clean = user_text.strip().lower()

        # Fast path: deterministic keyword detection to avoid model mistakes
        strong_return_keywords = [
            'return', 'refund', 'money back', 'send back', 'want to return',
            'need to return', 'request a return', 'two kind of return'
        ]
        strong_exchange_keywords = [
            'exchange', 'swap', 'replace', 'replacement', 'change it', 'different product'
        ]

        if any(keyword in user_text_clean for keyword in strong_return_keywords):
            print("   βœ… FAST PATH: RETURN detected via keyword match")
            return 'return'
        if any(keyword in user_text_clean for keyword in strong_exchange_keywords):
            print("   βœ… FAST PATH: EXCHANGE detected via keyword match")
            return 'exchange'
        
        # Try Groq AI first if enabled
        if self.enabled:
            try:
                prompt = (
                    f"You are analyzing customer service dialogue.\n\n"
                    f"QUESTION: {question_asked}\n"
                    f"CUSTOMER SAYS: {user_text}\n\n"
                    f"TASK: Determine what the customer wants.\n\n"
                    f"YOU MUST CHOOSE EXACTLY ONE:\n"
                    f"A) 'exchange' - if they want to swap/replace/exchange the product\n"
                    f"B) 'return' - if they want to send back/refund/return the product\n\n"
                    f"EXAMPLES:\n"
                    f"'exchange' β†’ exchange\n"
                    f"'return' β†’ return\n"
                    f"'I want to exchange this' β†’ exchange\n"
                    f"'can I get a refund' β†’ return\n"
                    f"'I would like you to process an exchange' β†’ exchange\n"
                    f"'swap it for another one' β†’ exchange\n\n"
                    f"Look for these keywords:\n"
                    f"- exchange, swap, replace, change, different = EXCHANGE\n"
                    f"- return, refund, send back, money back = RETURN\n\n"
                    f"RESPOND WITH JSON ONLY:\n"
                    f'{{"intent": "exchange", "confidence": "high", "reasoning": "..."}}\n'
                    f'OR\n'
                    f'{{"intent": "return", "confidence": "high", "reasoning": "..."}}\n\n'
                    f"The 'intent' MUST be EXACTLY either 'exchange' or 'return' (lowercase).\n"
                    f"Choose the BEST match even if you're uncertain. NEVER return anything other than 'exchange' or 'return'."
                )
                
                response = self.client.chat.completions.create(
                    model=self.model_name,
                    messages=[{"role": "user", "content": prompt}],
                    response_format={"type": "json_object"},
                    temperature=0.0,  # More deterministic
                )
                
                json_text = response.choices[0].message.content
                print(f"[LLM_OUTPUT request_type] {json_text}")
                result = RequestTypeResponse.model_validate_json(json_text)
                print(f"βœ… Groq Result: {result.intent} ({result.confidence}) - {result.reasoning}")
                
                # Validate result is one of the two options
                if result.intent in ['exchange', 'return']:
                    return result.intent
                else:
                    print(f"⚠️ Invalid intent from Groq: {result.intent}, falling back")
                    
            except Exception as e:
                print(f"❌ Groq Error: {e}")
                print(f"❌ Raw response: {json_text if 'json_text' in locals() else 'No response'}")
                # ensure downstream always gets a value
                return 'exchange'
        
        # Fallback: Basic keyword matching (ALWAYS returns a result)
        print(f"\nπŸ” BASIC KEYWORD MATCHING")
        print(f"   Cleaned input: '{user_text_clean}'")
        
        # Check for return keywords first
        return_keywords = ['return', 'refund', 'money back', 'send back', 'don\'t want', 'cancel']
        for keyword in return_keywords:
            if keyword in user_text_clean:
                print(f"   βœ… RETURN detected (keyword: '{keyword}')")
                return 'return'
        
        # Check for exchange keywords
        exchange_keywords = ['exchange', 'swap', 'replace', 'change', 'different']
        for keyword in exchange_keywords:
            if keyword in user_text_clean:
                print(f"   βœ… EXCHANGE detected (keyword: '{keyword}')")
                return 'exchange'
        
        # If no keywords found, make best guess based on partial matches
        print(f"   ⚠️ No exact keywords found, checking partial matches...")
        
        if any(word in user_text_clean for word in ['exch', 'swp', 'replac']):
            print(f"   βœ… EXCHANGE detected (partial match)")
            return 'exchange'
        
        if any(word in user_text_clean for word in ['retur', 'refun', 'back']):
            print(f"   βœ… RETURN detected (partial match)")
            return 'return'
        
        # Absolute last resort: default to exchange
        print(f"   ⚠️ No matches found, defaulting to EXCHANGE")
        return 'exchange'
    
    def extract_order_id(self, user_text, question_asked="Please provide your order ID"):
        print(f"\n🎯 EXTRACT_ORDER_ID: {user_text}")
        
        if not self.enabled:
            return self._basic_extract_order_id(user_text)
        
        try:
            prompt = (
                f"Question: {question_asked}\n"
                f"Response: {user_text}\n\n"
                "Extract any order number/ID from the customer's response.\n"
                "Numbers can be spoken as words (e.g., 'one two three' = 123).\n"
                "Look for patterns like: 'order 123', 'order number ABC', 'ORD-456', etc.\n\n"
                "You MUST respond with valid JSON in this exact format:\n"
                '{"order_id": "12345", "found": true, "interpretation": "Extracted from order 12345"}\n'
                'OR if no order ID found:\n'
                '{"order_id": null, "found": false, "interpretation": "No order ID mentioned"}'
            )
            
            response = self.client.chat.completions.create(
                model=self.model_name,
                messages=[{"role": "user", "content": prompt}],
                response_format={"type": "json_object"},
                temperature=0.1,
            )
            
            json_text = response.choices[0].message.content
            print(f"πŸ” Groq JSON Response: {json_text}")
            result = OrderIdResponse.model_validate_json(json_text)
            
            if result.found and result.order_id:
                order_id = re.sub(r'\D', '', result.order_id)
                if order_id:
                    print(f"βœ… Order ID: {order_id}")
                    return order_id
            
            return self._basic_extract_order_id(user_text)
                
        except Exception as e:
            print(f"❌ Groq Error: {e}")
            print(f"❌ Raw response: {json_text if 'json_text' in locals() else 'No response'}")
            return self._basic_extract_order_id(user_text)
    
    def extract_reason(self, user_text, request_type, question_asked=None):
        if not self.enabled:
            return user_text
        
        if not question_asked:
            question_asked = f"Why do you want to {request_type}?"
        
        try:
            prompt = (
                f"Question: {question_asked}\n"
                f"Response: {user_text}\n\n"
                "Convert their reason to professional format (max 10 words).\n\n"
                "CRITICAL: Convert 1st person to 2nd person:\n"
                "- 'I don't like' β†’ 'You didn't like'\n"
                "- 'I changed my mind' β†’ 'You changed your mind'\n"
                "- 'It's too small' β†’ 'It's too small' (already neutral)\n"
                "- 'I ordered wrong size' β†’ 'You ordered wrong size'\n\n"
                "Categorize as: size, defect, wrong_item, preference, delivery, other\n\n"
                "You MUST respond with valid JSON in this exact format:\n"
                '{"reason": "You didn\'t like the product", "category": "preference"}\n'
                'OR\n'
                '{"reason": "Wrong size ordered", "category": "size"}\n\n'
                "The 'category' MUST be one of: size, defect, wrong_item, preference, delivery, other"
            )
            
            response = self.client.chat.completions.create(
                model=self.model_name,
                messages=[{"role": "user", "content": prompt}],
                response_format={"type": "json_object"},
                temperature=0.1,
            )
            
            json_text = response.choices[0].message.content
            print(f"πŸ” Groq JSON Response: {json_text}")
            result = ReasonResponse.model_validate_json(json_text)
            print(f"βœ… Reason: {result.reason} ({result.category})")
            return result.reason
                
        except Exception as e:
            print(f"❌ Groq Error: {e}")
            print(f"❌ Raw response: {json_text if 'json_text' in locals() else 'No response'}")
            return user_text
    
    def extract_exchange_preference(self, user_text, question_asked="What would you prefer instead?"):
        if not self.enabled:
            return user_text
        
        try:
            prompt = (
                f"Question: {question_asked}\n"
                f"Response: {user_text}\n\n"
                "Extract what product they want (max 15 words).\n"
                "Categorize type as: size, color, model, feature, other\n\n"
                "You MUST respond with valid JSON in this exact format:\n"
                '{"preference": "Size large in black", "type": "size"}\n'
                'OR\n'
                '{"preference": "Different color - blue", "type": "color"}\n\n'
                "The 'type' MUST be one of: size, color, model, feature, other"
            )
            
            response = self.client.chat.completions.create(
                model=self.model_name,
                messages=[{"role": "user", "content": prompt}],
                response_format={"type": "json_object"},
                temperature=0.1,
            )
            
            json_text = response.choices[0].message.content
            print(f"πŸ” Groq JSON Response: {json_text}")
            result = PreferenceResponse.model_validate_json(json_text)
            print(f"βœ… Preference: {result.preference} ({result.type})")
            return result.preference
                
        except Exception as e:
            print(f"❌ Groq Error: {e}")
            print(f"❌ Raw response: {json_text if 'json_text' in locals() else 'No response'}")
            return user_text
    
    def is_confirmation(self, user_text, question_asked="Is this correct?"):
        if not self.enabled:
            return self._basic_is_confirmation(user_text)
        
        try:
            prompt = (
                f"Question: {question_asked}\n"
                f"Response: {user_text}\n\n"
                "Is the customer confirming (yes) or declining (no)?\n\n"
                "AFFIRMATIVE (confirming): yes, yeah, yep, sure, ok, okay, correct, right, go ahead, proceed, confirm\n"
                "NEGATIVE (declining): no, nope, nah, wrong, incorrect, cancel, stop, wait\n\n"
                "You MUST respond with valid JSON in this exact format:\n"
                '{"confirmed": true, "confidence": "high"}\n'
                'OR\n'
                '{"confirmed": false, "confidence": "high"}\n\n'
                "The 'confirmed' field MUST be boolean (true or false).\n"
                "The 'confidence' MUST be 'high', 'medium', or 'low'."
            )
            
            response = self.client.chat.completions.create(
                model=self.model_name,
                messages=[{"role": "user", "content": prompt}],
                response_format={"type": "json_object"},
                temperature=0.1,
            )
            
            json_text = response.choices[0].message.content
            print(f"πŸ” Groq JSON Response: {json_text}")
            result = ConfirmationResponse.model_validate_json(json_text)
            print(f"βœ… Confirmed: {result.confirmed} ({result.confidence})")
            return result.confirmed
                
        except Exception as e:
            print(f"❌ Groq Error: {e}")
            print(f"❌ Raw response: {json_text if 'json_text' in locals() else 'No response'}")
            return self._basic_is_confirmation(user_text)
    
    def _basic_extract_request_type(self, text):
        """Legacy method - not used anymore, kept for compatibility"""
        text_lower = text.lower().strip()
        
        return_keywords = ['return', 'refund', 'money back', 'send back']
        for keyword in return_keywords:
            if keyword in text_lower:
                return 'return'
        
        exchange_keywords = ['exchange', 'swap', 'replace', 'change', 'different']
        for keyword in exchange_keywords:
            if keyword in text_lower:
                return 'exchange'
        
        # Default to exchange if nothing found
        return 'exchange'
    
    def _basic_extract_order_id(self, text):
        print(f"πŸ” Basic order ID extraction: {text}")
        match = re.search(r'order[\s#]*(\d+)|(\d+)', text.lower())
        if match:
            order_id = match.group(1) or match.group(2)
            print(f"βœ… Found: {order_id}")
            return order_id
        print("❌ No order ID found")
        return None
    
    def _basic_is_confirmation(self, text):
        confirmations = ['yes', 'yeah', 'yep', 'correct', 'right', 'sure', 'ok', 'okay']
        return any(word in text.lower() for word in confirmations)


    def identify_correction_field(self, user_text, current_data):
        """When user says no to confirmation, identify which field they want to correct"""
        if not self.enabled:
            return "everything"
        
        try:
            confirmation_summary = (
                f"Request type: {current_data.get('request_type', 'N/A')}\n"
                f"Order ID: {current_data.get('order_id', 'N/A')}\n"
                f"Reason: {current_data.get('reason', 'N/A')}\n"
            )
            if current_data.get('request_type') == 'exchange':
                confirmation_summary += f"Exchange preference: {current_data.get('exchange_preference', 'N/A')}\n"
            
            prompt = (
                f"The customer declined the confirmation. Here's what we have:\n"
                f"{confirmation_summary}\n"
                f"Customer said: {user_text}\n\n"
                "Which field do they want to correct?\n\n"
                "Options: request_type, order_id, reason, exchange_preference, everything\n\n"
                "Examples:\n"
                "- 'wrong order number' = order_id\n"
                "- 'I want exchange not return' = request_type\n"
                "- 'reason is wrong' = reason\n"
                "- 'start over' = everything\n\n"
                "You MUST respond with valid JSON in this exact format:\n"
                '{\"field\": \"order_id\", \"reasoning\": \"Customer mentioned wrong order number\"}\n'
                'OR\n'
                '{\"field\": \"everything\", \"reasoning\": \"Customer wants to start over\"}\n\n'
                "The 'field' MUST be one of: request_type, order_id, reason, exchange_preference, everything"
            )
            
            response = self.client.chat.completions.create(
                model=self.model_name,
                messages=[{"role": "user", "content": prompt}],
                response_format={"type": "json_object"},
                temperature=0.1,
            )
            
            json_text = response.choices[0].message.content
            print(f"πŸ” Groq JSON Response: {json_text}")
            result = CorrectionField.model_validate_json(json_text)
            print(f"βœ… Correction needed for: {result.field} - {result.reasoning}")
            return result.field
                
        except Exception as e:
            print(f"❌ Groq Error: {e}")
            print(f"❌ Raw response: {json_text if 'json_text' in locals() else 'No response'}")
            return "everything"


# Global instance
groq_ai = GroqAI()

# Backwards compatibility alias
gemini_ai = groq_ai