Files
assistant/agent/agents/order.py
wangliang e093995368 feat: 增强 Agent 系统和完善项目结构
主要改进:
- Agent 增强: 订单查询、售后支持、客服路由等功能优化
- 新增语言检测和 Token 管理模块
- 改进 Chatwoot webhook 处理和用户标识
- MCP 服务器增强: 订单 MCP 和 Strapi MCP 功能扩展
- 新增商城客户端、知识库、缓存和同步模块
- 添加多语言提示词系统 (YAML)
- 完善项目结构: 整理文档、脚本和测试文件
- 新增调试和测试工具脚本

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-01-16 16:28:47 +08:00

378 lines
13 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""
Order Agent - Handles order-related queries and operations
"""
import json
from typing import Any
from core.state import AgentState, ConversationState, add_tool_call, set_response, update_context
from core.llm import get_llm_client, Message
from utils.logger import get_logger
logger = get_logger(__name__)
ORDER_AGENT_PROMPT = """你是一个专业的 B2B 订单服务助手。
你的职责是帮助用户处理订单相关的问题,包括:
- 订单查询
- 物流跟踪
- 订单修改
- 订单取消
- 发票获取
## 可用工具
1. **get_mall_order** - 从商城 API 查询订单(推荐使用)
- order_id: 订单号(必需)
- 说明:此工具会自动使用用户的身份 token 查询商城订单详情
2. **query_order** - 查询历史订单
- user_id: 用户 ID自动注入
- account_id: 账户 ID自动注入
- order_id: 订单号(可选,不填则查询最近订单)
- date_start: 开始日期(可选)
- date_end: 结束日期(可选)
- status: 订单状态(可选)
3. **track_logistics** - 物流跟踪
- order_id: 订单号
- tracking_number: 物流单号(可选)
4. **modify_order** - 修改订单
- order_id: 订单号
- user_id: 用户 ID自动注入
- modifications: 修改内容address/items/quantity 等)
5. **cancel_order** - 取消订单
- order_id: 订单号
- user_id: 用户 ID自动注入
- reason: 取消原因
6. **get_invoice** - 获取发票
- order_id: 订单号
- invoice_type: 发票类型normal/vat
## 回复格式要求
**重要**:你必须始终返回完整的 JSON 对象,不要包含任何其他文本或解释。
### 格式 1调用工具
当需要使用工具查询信息时,返回:
```json
{
"action": "call_tool",
"tool_name": "get_mall_order",
"arguments": {
"order_id": "202071324"
}
}
```
### 格式 2询问信息
当需要向用户询问更多信息时,返回:
```json
{
"action": "ask_info",
"question": "请提供您的订单号"
}
```
### 格式 3直接回复
当可以直接回答时,返回:
```json
{
"action": "respond",
"response": "您的订单已发货预计3天内到达"
}
```
## 示例对话
用户: "查询订单 202071324"
回复:
```json
{
"action": "call_tool",
"tool_name": "get_mall_order",
"arguments": {
"order_id": "202071324"
}
}
```
用户: "我的订单发货了吗?"
回复:
```json
{
"action": "ask_info",
"question": "请提供您的订单号,以便查询订单状态"
}
```
## 重要约束
- **必须返回完整的 JSON 对象**,不要只返回部分内容
- **不要添加任何 markdown 代码块标记**(如 \`\`\`json
- **不要添加任何解释性文字**,只返回 JSON
- user_id 和 account_id 会自动注入到 arguments 中,无需手动添加
- 如果用户提供了订单号,优先使用 get_mall_order 工具
- 对于敏感操作(取消、修改),确保有明确的订单号
"""
async def order_agent(state: AgentState) -> AgentState:
"""Order agent node
Handles order queries, tracking, modifications, and cancellations.
Args:
state: Current agent state
Returns:
Updated state with tool calls or response
"""
logger.info(
"Order agent processing",
conversation_id=state["conversation_id"],
sub_intent=state.get("sub_intent")
)
state["current_agent"] = "order"
state["agent_history"].append("order")
state["state"] = ConversationState.PROCESSING.value
# Check if we have tool results to process
if state["tool_results"]:
return await _generate_order_response(state)
# Build messages for LLM
messages = [
Message(role="system", content=ORDER_AGENT_PROMPT),
]
# Add conversation history
for msg in state["messages"][-6:]:
messages.append(Message(role=msg["role"], content=msg["content"]))
# Build context info
context_info = f"用户ID: {state['user_id']}\n账户ID: {state['account_id']}\n"
# Add entities if available
if state["entities"]:
context_info += f"已提取的信息: {json.dumps(state['entities'], ensure_ascii=False)}\n"
# Add existing context
if state["context"].get("order_id"):
context_info += f"当前讨论的订单号: {state['context']['order_id']}\n"
user_content = f"{context_info}\n用户消息: {state['current_message']}"
messages.append(Message(role="user", content=user_content))
try:
llm = get_llm_client()
response = await llm.chat(messages, temperature=0.5)
# Parse response
content = response.content.strip()
logger.info(
"LLM response received",
conversation_id=state["conversation_id"],
response_length=len(content),
response_preview=content[:300]
)
# 检查是否是简化的工具调用格式:工具名称\n{参数}
# 例如get_mall_order\n{"order_id": "202071324"}
if "\n" in content and "{" in content:
lines = content.split("\n")
if len(lines) >= 2:
tool_name_line = lines[0].strip()
json_line = "\n".join(lines[1:]).strip()
# 如果第一行看起来像工具名称(不包含 {),且第二行是 JSON
if "{" not in tool_name_line and "{" in json_line:
logger.info(
"Detected simplified tool call format",
tool_name=tool_name_line,
json_preview=json_line[:200]
)
try:
arguments = json.loads(json_line)
# 直接构建工具调用
arguments["user_id"] = state["user_id"]
arguments["account_id"] = state["account_id"]
# Inject user_token if available
if state.get("user_token"):
arguments["user_token"] = state["user_token"]
logger.info("Injected user_token into tool call")
# Use entity if available
if "order_id" not in arguments and state["entities"].get("order_id"):
arguments["order_id"] = state["entities"]["order_id"]
state = add_tool_call(
state,
tool_name=tool_name_line,
arguments=arguments,
server="order"
)
state["state"] = ConversationState.TOOL_CALLING.value
logger.info(
"Tool call added from simplified format",
tool_name=tool_name_line,
arguments_keys=list(arguments.keys())
)
return state
except json.JSONDecodeError as e:
logger.warning(
"Failed to parse simplified format",
error=str(e),
json_line=json_line[:200]
)
# 清理内容,去除可能的 markdown 代码块标记
# 例如:```json\n{...}\n``` 或 ```\n{...}\n```
if "```" in content:
# 找到第一个 ``` 后的内容
parts = content.split("```")
if len(parts) >= 2:
content = parts[1].strip()
# 去掉可能的 "json" 标记
if content.startswith("json"):
content = content[4:].strip()
# 去掉结尾的 ``` 标记
if content.endswith("```"):
content = content[:-3].strip()
# 尝试提取 JSON 对象(处理周围可能有文本的情况)
json_start = content.find("{")
json_end = content.rfind("}")
if json_start != -1 and json_end != -1 and json_end > json_start:
content = content[json_start:json_end + 1]
logger.info(
"Cleaned content for JSON parsing",
conversation_id=state["conversation_id"],
content_length=len(content),
content_preview=content[:500]
)
try:
result = json.loads(content)
except json.JSONDecodeError as e:
logger.error(
"Failed to parse LLM response as JSON",
conversation_id=state["conversation_id"],
error=str(e),
content_preview=content[:500]
)
# 如果解析失败,尝试将原始内容作为直接回复
state = set_response(state, response.content)
return state
action = result.get("action")
logger.info(
"LLM action parsed",
conversation_id=state["conversation_id"],
action=action,
tool_name=result.get("tool_name")
)
if action == "call_tool":
# Inject user context into arguments
arguments = result.get("arguments", {})
arguments["user_id"] = state["user_id"]
arguments["account_id"] = state["account_id"]
# Inject user_token if available (for Mall API calls)
if state.get("user_token"):
arguments["user_token"] = state["user_token"]
logger.debug("Injected user_token into tool call")
# Use entity if available
if "order_id" not in arguments and state["entities"].get("order_id"):
arguments["order_id"] = state["entities"]["order_id"]
state = add_tool_call(
state,
tool_name=result["tool_name"],
arguments=arguments,
server="order"
)
state["state"] = ConversationState.TOOL_CALLING.value
logger.info(
"Tool call added",
tool_name=result["tool_name"],
arguments_keys=list(arguments.keys())
)
elif action == "ask_info":
state = set_response(state, result["question"])
state["state"] = ConversationState.AWAITING_INFO.value
elif action == "respond":
state = set_response(state, result["response"])
state["state"] = ConversationState.GENERATING.value
elif action == "handoff":
state["requires_human"] = True
state["handoff_reason"] = result.get("reason", "Complex order operation")
return state
except Exception as e:
logger.error("Order agent failed", error=str(e))
state["error"] = str(e)
return state
async def _generate_order_response(state: AgentState) -> AgentState:
"""Generate response based on order tool results"""
# Build context from tool results
tool_context = []
for result in state["tool_results"]:
if result["success"]:
data = result["data"]
tool_context.append(f"工具 {result['tool_name']} 返回:\n{json.dumps(data, ensure_ascii=False, indent=2)}")
# Extract order_id for context
if isinstance(data, dict):
if data.get("order_id"):
state = update_context(state, {"order_id": data["order_id"]})
elif data.get("orders") and len(data["orders"]) > 0:
state = update_context(state, {"order_id": data["orders"][0].get("order_id")})
else:
tool_context.append(f"工具 {result['tool_name']} 执行失败: {result['error']}")
prompt = f"""基于以下订单系统返回的信息,生成对用户的回复。
用户问题: {state["current_message"]}
系统返回信息:
{chr(10).join(tool_context)}
请生成一个清晰、友好的回复,包含订单的关键信息(订单号、状态、金额、物流等)。
如果是物流信息,请按时间线整理展示。
只返回回复内容,不要返回 JSON。"""
messages = [
Message(role="system", content="你是一个专业的订单客服助手,请根据系统返回的信息回答用户的订单问题。"),
Message(role="user", content=prompt)
]
try:
llm = get_llm_client()
response = await llm.chat(messages, temperature=0.7)
state = set_response(state, response.content)
return state
except Exception as e:
logger.error("Order response generation failed", error=str(e))
state = set_response(state, "抱歉,处理订单信息时遇到问题。请稍后重试或联系人工客服。")
return state