Spaces:
Sleeping
Sleeping
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
|