Spaces:
Sleeping
Sleeping
| from pydantic import BaseModel | |
| from typing import Literal, List | |
| from langchain_core.messages import SystemMessage, HumanMessage, AIMessage | |
| from src.state import AgentState | |
| from src.utils.llm_factory import get_llm, _invoke_with_backoff | |
| from src.utils.prompt_templates import INTENT_SYSTEM, INTENT_FEW_SHOTS | |
| import json | |
| # Emotion keywords that suggest hybrid intent | |
| EMOTION_KEYWORDS = [ | |
| "frustrated", "angry", "upset", "disappointed", "furious", "livid", | |
| "annoyed", "irritated", "unhappy", "dissatisfied", "terrible", | |
| "awful", "horrible", "unacceptable", "ridiculous", "outrageous" | |
| ] | |
| class IntentClassification(BaseModel): | |
| intent: Literal["transactional", "informational", "sentimental", "hybrid", "out_of_scope"] | |
| confidence: float | |
| sub_intents: List[str] | |
| reasoning: str | |
| def classify_intent(state: AgentState) -> dict: | |
| """Classify user intent with fallback to hybrid on errors""" | |
| user_input = state.get("user_input", "").lower() | |
| # Check for emotion keywords - if found with order context, force hybrid | |
| has_emotion = any(keyword in user_input for keyword in EMOTION_KEYWORDS) | |
| has_order_context = state.get("last_order_id") is not None | |
| if has_emotion and has_order_context: | |
| print(f"[intent_classifier] Emotion keyword detected with order context, forcing hybrid intent") | |
| return { | |
| "intent": "hybrid", | |
| "intent_confidence": 0.85, | |
| "sub_intents": ["order_tracking", "complaint"] | |
| } | |
| try: | |
| llm = get_llm(temperature=0.0) | |
| messages = [SystemMessage(content=INTENT_SYSTEM)] | |
| # Inject few-shots | |
| for shot in INTENT_FEW_SHOTS: | |
| if shot["role"] == "user": | |
| messages.append(HumanMessage(content=shot["content"])) | |
| else: | |
| messages.append(SystemMessage(content=shot["content"])) | |
| # Inject recent conversation history for pronoun resolution | |
| recent_msgs = state.get("messages", [])[-4:] # Last 2 turns | |
| if recent_msgs: | |
| history_context = "\n[Recent conversation context for pronoun resolution:]\n" | |
| for msg in recent_msgs: | |
| if isinstance(msg, HumanMessage): | |
| history_context += f"User: {msg.content[:150]}\n" | |
| else: | |
| history_context += f"Agent: {msg.content[:150]}\n" | |
| messages.append(SystemMessage(content=history_context)) | |
| messages.append(HumanMessage(content=state.get("user_input", ""))) | |
| response = _invoke_with_backoff(llm, messages, provider="groq") | |
| raw = response.content.strip() | |
| # Strip markdown fences if LLM wraps in ```json | |
| raw = raw.replace("```json", "").replace("```", "").strip() | |
| parsed = IntentClassification(**json.loads(raw)) | |
| # Handle out_of_scope as terminal path | |
| if parsed.intent == "out_of_scope": | |
| print(f"[intent_classifier] Out-of-scope query detected (confidence: {parsed.confidence:.2f})") | |
| # Check if it's a product browsing query for custom message | |
| user_input_lower = state.get("user_input", "").lower() | |
| is_product_search = any(keyword in user_input_lower for keyword in [ | |
| "product", "catalog", "browse", "list", "show items", "search products" | |
| ]) | |
| if is_product_search: | |
| final_response = ( | |
| "I'm a customer support assistant focused on helping with existing orders, " | |
| "deliveries, returns, and refunds. For browsing products or searching our " | |
| "catalog, please visit the Olist marketplace directly.\n\n" | |
| "Is there anything I can help you with regarding an existing order?" | |
| ) | |
| else: | |
| final_response = ( | |
| "I'm Olist's customer support assistant — I can help with orders, " | |
| "deliveries, returns, payments, and seller queries. " | |
| "Your question doesn't seem related to Olist support. " | |
| "Could you ask me something about your Olist experience?" | |
| ) | |
| return { | |
| "intent": "out_of_scope", | |
| "intent_confidence": parsed.confidence, | |
| "sub_intents": parsed.sub_intents, | |
| "final_response": final_response, | |
| "messages": [ | |
| HumanMessage(content=state.get("user_input", "")), | |
| AIMessage(content=final_response) | |
| ] | |
| } | |
| # Robustness: low confidence → force hybrid | |
| if parsed.confidence < 0.6: | |
| return { | |
| "intent": "hybrid", | |
| "intent_confidence": parsed.confidence, | |
| "sub_intents": parsed.sub_intents, | |
| "error_log": state.get("error_log", []) + [ | |
| f"[intent_classifier] Low confidence ({parsed.confidence:.2f}), forced hybrid" | |
| ] | |
| } | |
| return { | |
| "intent": parsed.intent, | |
| "intent_confidence": parsed.confidence, | |
| "sub_intents": parsed.sub_intents | |
| } | |
| except Exception as e: | |
| # Rule-based fallback to avoid complete failure | |
| user_input = state.get("user_input", "").lower() | |
| # Simple keyword matching | |
| if any(word in user_input for word in ["order", "package", "delivery", "tracking", "where", "status"]): | |
| intent = "transactional" | |
| confidence = 0.6 | |
| sub_intents = ["order_tracking"] | |
| elif any(word in user_input for word in ["policy", "return", "refund", "warranty", "payment", "shipping"]): | |
| intent = "informational" | |
| confidence = 0.6 | |
| sub_intents = ["policy_query"] | |
| elif any(word in user_input for word in ["angry", "frustrated", "upset", "terrible", "awful"]): | |
| intent = "sentimental" | |
| confidence = 0.6 | |
| sub_intents = ["complaint"] | |
| else: | |
| intent = "hybrid" | |
| confidence = 0.5 | |
| sub_intents = ["default"] | |
| return { | |
| "intent": intent, | |
| "intent_confidence": confidence, | |
| "sub_intents": sub_intents, | |
| "error_log": state.get("error_log", []) + [ | |
| f"[intent_classifier] LLM error, using rule-based fallback: {str(e)[:100]}" | |
| ] | |
| } | |