- 配置 Docker Compose 多服务编排 - 实现 Chatwoot + Agent 集成 - 配置 Strapi MCP 知识库 - 支持 7 种语言的 FAQ 系统 - 实现 LangGraph AI 工作流 Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
221 lines
6.8 KiB
Python
221 lines
6.8 KiB
Python
"""
|
||
Router Agent - Intent recognition and routing
|
||
"""
|
||
import json
|
||
from typing import Any, Optional
|
||
|
||
from core.state import AgentState, Intent, ConversationState, set_intent, add_entity
|
||
from core.llm import get_llm_client, Message
|
||
from utils.logger import get_logger
|
||
|
||
logger = get_logger(__name__)
|
||
|
||
|
||
# Intent classification prompt
|
||
CLASSIFICATION_PROMPT = """你是一个 B2B 购物网站的智能助手路由器。
|
||
你的任务是分析用户消息,识别用户意图并提取关键实体。
|
||
|
||
## 可用意图分类
|
||
|
||
1. **customer_service** - 通用咨询
|
||
- FAQ 问答
|
||
- 产品使用问题
|
||
- 公司信息查询
|
||
- 政策咨询(退换货政策、隐私政策等)
|
||
|
||
2. **order** - 订单相关
|
||
- 订单查询("我的订单在哪"、"查一下订单")
|
||
- 物流跟踪("快递到哪了"、"什么时候到货")
|
||
- 订单修改("改一下收货地址"、"修改订单数量")
|
||
- 订单取消("取消订单"、"不想要了")
|
||
- 发票查询("开发票"、"要发票")
|
||
|
||
3. **aftersale** - 售后服务
|
||
- 退货申请("退货"、"不满意想退")
|
||
- 换货申请("换货"、"换一个")
|
||
- 投诉("投诉"、"服务态度差")
|
||
- 工单/问题反馈
|
||
|
||
4. **product** - 商品相关
|
||
- 商品搜索("有没有xx"、"找一下xx")
|
||
- 商品推荐("推荐"、"有什么好的")
|
||
- 询价("多少钱"、"批发价"、"大量购买价格")
|
||
- 库存查询("有货吗"、"还有多少")
|
||
|
||
5. **human_handoff** - 需要转人工
|
||
- 用户明确要求转人工
|
||
- 复杂问题 AI 无法处理
|
||
- 敏感问题需要人工处理
|
||
|
||
## 实体提取
|
||
|
||
请从消息中提取以下实体(如果存在):
|
||
- order_id: 订单号(如 ORD123456)
|
||
- product_id: 商品ID
|
||
- product_name: 商品名称
|
||
- quantity: 数量
|
||
- date_reference: 时间引用(今天、昨天、上周、具体日期等)
|
||
- tracking_number: 物流单号
|
||
- phone: 电话号码
|
||
- address: 地址信息
|
||
|
||
## 输出格式
|
||
|
||
请以 JSON 格式返回,包含以下字段:
|
||
```json
|
||
{
|
||
"intent": "意图分类",
|
||
"confidence": 0.95,
|
||
"sub_intent": "子意图(可选)",
|
||
"entities": {
|
||
"entity_type": "entity_value"
|
||
},
|
||
"reasoning": "简短的推理说明"
|
||
}
|
||
```
|
||
|
||
## 注意事项
|
||
- 如果意图不明确,置信度应该较低
|
||
- 如果无法确定意图,返回 "unknown"
|
||
- 实体提取要准确,没有的字段不要填写
|
||
"""
|
||
|
||
|
||
async def classify_intent(state: AgentState) -> AgentState:
|
||
"""Classify user intent and extract entities
|
||
|
||
This is the first node in the workflow that analyzes the user's message
|
||
and determines which agent should handle it.
|
||
|
||
Args:
|
||
state: Current agent state
|
||
|
||
Returns:
|
||
Updated state with intent and entities
|
||
"""
|
||
logger.info(
|
||
"Classifying intent",
|
||
conversation_id=state["conversation_id"],
|
||
message=state["current_message"][:100]
|
||
)
|
||
|
||
state["state"] = ConversationState.CLASSIFYING.value
|
||
state["step_count"] += 1
|
||
|
||
# Build context from conversation history
|
||
context_summary = ""
|
||
if state["context"]:
|
||
context_parts = []
|
||
if state["context"].get("order_id"):
|
||
context_parts.append(f"当前讨论的订单: {state['context']['order_id']}")
|
||
if state["context"].get("product_id"):
|
||
context_parts.append(f"当前讨论的商品: {state['context']['product_id']}")
|
||
if context_parts:
|
||
context_summary = "\n".join(context_parts)
|
||
|
||
# Build messages for LLM
|
||
messages = [
|
||
Message(role="system", content=CLASSIFICATION_PROMPT),
|
||
]
|
||
|
||
# Add recent conversation history for context
|
||
for msg in state["messages"][-6:]: # Last 3 turns
|
||
messages.append(Message(role=msg["role"], content=msg["content"]))
|
||
|
||
# Add current message with context
|
||
user_content = f"用户消息: {state['current_message']}"
|
||
if context_summary:
|
||
user_content += f"\n\n当前上下文:\n{context_summary}"
|
||
|
||
messages.append(Message(role="user", content=user_content))
|
||
|
||
try:
|
||
llm = get_llm_client()
|
||
response = await llm.chat(messages, temperature=0.3)
|
||
|
||
# Parse JSON response
|
||
content = response.content.strip()
|
||
# Handle markdown code blocks
|
||
if content.startswith("```"):
|
||
content = content.split("```")[1]
|
||
if content.startswith("json"):
|
||
content = content[4:]
|
||
|
||
result = json.loads(content)
|
||
|
||
# Extract intent
|
||
intent_str = result.get("intent", "unknown")
|
||
try:
|
||
intent = Intent(intent_str)
|
||
except ValueError:
|
||
intent = Intent.UNKNOWN
|
||
|
||
confidence = float(result.get("confidence", 0.5))
|
||
sub_intent = result.get("sub_intent")
|
||
|
||
# Set intent in state
|
||
state = set_intent(state, intent, confidence, sub_intent)
|
||
|
||
# Extract entities
|
||
entities = result.get("entities", {})
|
||
for entity_type, entity_value in entities.items():
|
||
if entity_value:
|
||
state = add_entity(state, entity_type, entity_value)
|
||
|
||
logger.info(
|
||
"Intent classified",
|
||
intent=intent.value,
|
||
confidence=confidence,
|
||
entities=list(entities.keys())
|
||
)
|
||
|
||
# Check if human handoff is needed
|
||
if intent == Intent.HUMAN_HANDOFF or confidence < 0.5:
|
||
state["requires_human"] = True
|
||
state["handoff_reason"] = result.get("reasoning", "Intent unclear")
|
||
|
||
return state
|
||
|
||
except json.JSONDecodeError as e:
|
||
logger.error("Failed to parse intent response", error=str(e))
|
||
state["intent"] = Intent.UNKNOWN.value
|
||
state["intent_confidence"] = 0.0
|
||
return state
|
||
|
||
except Exception as e:
|
||
logger.error("Intent classification failed", error=str(e))
|
||
state["error"] = str(e)
|
||
state["intent"] = Intent.UNKNOWN.value
|
||
return state
|
||
|
||
|
||
def route_by_intent(state: AgentState) -> str:
|
||
"""Route to appropriate agent based on intent
|
||
|
||
This is the routing function used by conditional edges in the graph.
|
||
|
||
Args:
|
||
state: Current agent state
|
||
|
||
Returns:
|
||
Name of the next node to execute
|
||
"""
|
||
intent = state.get("intent")
|
||
requires_human = state.get("requires_human", False)
|
||
|
||
# Human handoff takes priority
|
||
if requires_human:
|
||
return "human_handoff"
|
||
|
||
# Route based on intent
|
||
routing_map = {
|
||
Intent.CUSTOMER_SERVICE.value: "customer_service_agent",
|
||
Intent.ORDER.value: "order_agent",
|
||
Intent.AFTERSALE.value: "aftersale_agent",
|
||
Intent.PRODUCT.value: "product_agent",
|
||
Intent.HUMAN_HANDOFF.value: "human_handoff",
|
||
Intent.UNKNOWN.value: "customer_service_agent" # Default to customer service
|
||
}
|
||
|
||
return routing_map.get(intent, "customer_service_agent")
|