ecommerce-agent / src /nodes /intent_classifier.py
Mohitcr1
Add escalation summary generation and product search rejection
2e83d00
Raw
History Blame Contribute Delete
6.54 kB
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]}"
]
}