Spaces:
Sleeping
Sleeping
File size: 6,542 Bytes
cf796c5 b6ae869 cf796c5 b6ae869 cf796c5 d2c5868 cf796c5 b6ae869 cf796c5 f9e8eed cf796c5 d2c5868 f9e8eed d2c5868 cf796c5 d2c5868 cf796c5 b6ae869 cf796c5 b6ae869 2e83d00 b6ae869 cf796c5 f9e8eed cf796c5 2bfa9ba f9e8eed 2bfa9ba f9e8eed 2bfa9ba f9e8eed 2bfa9ba f9e8eed 2bfa9ba f9e8eed | 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 | 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]}"
]
}
|