Files
assistant/agent/agents/order.py
wangliang 0b5d0a8086 feat: 重构订单和物流信息展示格式
主要改动:
- 订单列表:使用 order_list 格式,展示 5 个订单(全部状态)
- 订单详情:使用 order_detail 格式,优化价格和时间显示
- 物流信息:使用 logistics 格式,根据 track id 动态生成步骤
- 商品图片:从 orderProduct.imageUrl 字段获取
- 时间格式:统一为 YYYY-MM-DD HH:MM:SS
- 多语言支持:amountLabel、orderTime 支持中英文
- 配置管理:新增 FRONTEND_URL 环境变量
- API 集成:改进 Mall API tracks 数据解析
- 认证优化:account_id 从 webhook 动态获取

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-01-23 18:49:40 +08:00

864 lines
34 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
from integrations.chatwoot import ChatwootClient
logger = get_logger(__name__)
# Order status constants (Mall API)
ORDER_STATUS = {
0: "已取消", # ORDER_STATUS_CANCEL
1: "待支付", # ORDER_STATUS_WAIT_PAY
2: "已支付", # ORDER_STATUS_PAID
3: "已发货", # ORDER_STATUS_SHIPPED
4: "已签收", # ORDER_STATUS_SIGNED
15: "已完成", # ORDER_STATUS_FINISH
100: "超时取消", # ORDER_STATUS_CANCEL_OVER_TIME
110: "已废弃", # ORDER_STATUS_PAY_SUCCESS (deprecated)
10000: "全部" # All
}
def get_status_text(status_value: Any) -> str:
"""获取订单状态文本
Args:
status_value: 状态值(数字或字符串)
Returns:
状态文本
"""
if status_value is None:
return ""
# 转换为整数
try:
status_int = int(status_value)
return ORDER_STATUS.get(status_int, str(status_value))
except (ValueError, TypeError):
return str(status_value)
ORDER_AGENT_PROMPT = """你是一个专业的 B2B 订单服务助手。
你的职责是帮助用户处理订单相关的问题,包括:
- 订单查询
- 物流跟踪
- 订单修改
- 订单取消
- 发票获取
## 可用工具
1. **get_mall_order** - 从商城 API 查询单个订单详情(推荐使用)
- order_id: 订单号(必需)
- user_token: 用户 token自动注入
- 说明:此工具会自动使用用户的身份 token 查询商城订单详情
2. **get_mall_order_list** - 从商城 API 查询订单列表(推荐使用)
- user_token: 用户 token自动注入
- page: 页码(可选,默认 1
- limit: 每页数量(可选,默认 10
- 说明:查询用户的所有订单,按时间倒序排列
3. **get_logistics** - 从商城 API 查询物流信息
- order_id: 订单号(必需)
- user_token: 用户 token自动注入
- 说明:查询订单的物流轨迹和配送状态
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": "call_tool",
"tool_name": "get_mall_order_list",
"arguments": {}
}
```
用户: "我的订单发货了吗?"
回复:
```json
{
"action": "ask_info",
"question": "请提供您的订单号,以便查询订单状态"
}
```
用户: "帮我查一下订单 202071324 的物流"
回复:
```json
{
"action": "call_tool",
"tool_name": "get_logistics",
"arguments": {
"order_id": "202071324"
}
}
```
## 重要约束
- **必须返回完整的 JSON 对象**,不要只返回部分内容
- **不要添加任何 markdown 代码块标记**(如 \`\`\`json
- **不要添加任何解释性文字**,只返回 JSON
- **每次调用工具必须指定 tool_name**
- user_id 和 account_id 会自动注入到 arguments 中,无需手动添加
- 如果用户提供了具体订单号,使用 get_mall_order 工具
- 如果用户想查询订单列表或最近的订单,使用 get_mall_order_list 工具
- 如果用户想查询物流状态,使用 get_logistics 工具
- 对于敏感操作(取消、修改),确保有明确的订单号
"""
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
# 优先使用 mall_token用于 Mall API如果没有则使用 user_token
token_to_use = state.get("mall_token") or state.get("user_token")
if token_to_use:
arguments["user_token"] = token_to_use
logger.info(
"Injected token into tool call",
token_type="mall_token" if state.get("mall_token") else "user_token",
token_prefix=token_to_use[:20] if token_to_use else None
)
else:
logger.warning(
"No token available in state, MCP will use default token",
conversation_id=state["conversation_id"]
)
# 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)
# 优先使用 mall_token用于 Mall API如果没有则使用 user_token
token_to_use = state.get("mall_token") or state.get("user_token")
if token_to_use:
arguments["user_token"] = token_to_use
logger.debug(
"Injected token into tool call",
token_type="mall_token" if state.get("mall_token") else "user_token",
token_prefix=token_to_use[:20] if token_to_use else None
)
else:
logger.warning(
"No token available in state, MCP will use default token",
conversation_id=state["conversation_id"]
)
# Use entity if available (only for single-order queries, not for order list)
tool_name = result["tool_name"]
if "order_id" not in arguments and state["entities"].get("order_id"):
# 只在查询单个订单的工具中添加 order_id
if tool_name in ["get_mall_order", "get_logistics", "query_order"]:
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"""
# 检查是否是邮件渠道
is_email = state.get("context", {}).get("is_email", False)
# 邮件渠道:使用纯文本回复(不支持富媒体)
if is_email:
logger.info(
"Email channel detected, using text response instead of rich media",
conversation_id=state.get("conversation_id")
)
return await _generate_text_response(state)
# 获取检测到的语言,默认为英文
detected_language = state.get("detected_language", "en")
# 解析订单数据并尝试使用 form 格式发送
order_data = None
order_list = [] # 订单列表
user_message = ""
logistics_data = None
for result in state["tool_results"]:
if result["success"]:
data = result["data"]
tool_name = result["tool_name"]
# 提取订单ID到上下文
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")})
# 处理 get_mall_order_list 返回的订单列表
if tool_name == "get_mall_order_list" and isinstance(data, dict):
mcp_result = data.get("result", {})
if mcp_result.get("orders") and isinstance(mcp_result["orders"], list):
# 解析订单列表
for order_item in mcp_result["orders"]:
parsed_order = _parse_mall_order_data(order_item)
if parsed_order.get("order_id"):
order_list.append(parsed_order)
# 处理 get_mall_order 返回的单个订单数据
elif tool_name == "get_mall_order" and isinstance(data, dict):
# MCP 返回结构: {"success": true, "result": {...}}
# result 可能包含: {"success": bool, "order": {...}, "order_id": "...", "error": "..."}
mcp_result = data.get("result", {})
# 检查是否有错误(如未登录)
if mcp_result.get("error") or not mcp_result.get("success"):
logger.warning(
"get_mall_order returned error",
error=mcp_result.get("error"),
require_login=mcp_result.get("require_login")
)
# 设置错误消息到状态中
if mcp_result.get("require_login"):
user_message = mcp_result.get("error", "请先登录账户以查询订单信息")
elif mcp_result.get("order"):
# 有订单数据
order_data = _parse_mall_order_data(mcp_result["order"])
# 如果 order_data 中没有 order_id从外层获取
if not order_data.get("order_id") and mcp_result.get("order_id"):
order_data["order_id"] = mcp_result["order_id"]
else:
logger.warning(
"get_mall_order returned success but no order data",
data_keys=list(data.keys()),
result_keys=list(mcp_result.keys()) if isinstance(mcp_result, dict) else None
)
# 处理 query_order 返回的订单数据
elif tool_name == "query_order" and isinstance(data, dict):
if data.get("orders") and len(data["orders"]) > 0:
# 如果有多个订单,添加到列表
if len(data["orders"]) > 1:
for order_item in data["orders"]:
parsed = _parse_order_data(order_item)
if parsed.get("order_id"):
order_list.append(parsed)
user_message = f"找到 {len(data['orders'])} 个订单"
else:
# 只有一个订单,作为单个订单处理
order_data = _parse_order_data(data["orders"][0])
# 处理 get_logistics 返回的物流数据
elif tool_name == "get_logistics" and isinstance(data, dict):
logistics_data = _parse_logistics_data(data)
# 如果之前有订单数据,添加物流信息
if order_data:
order_data["logistics"] = logistics_data
# 如果只有物流数据,单独发送物流信息
elif logistics_data and logistics_data.get("tracking_number"):
# 添加订单号(如果有)
order_id = state.get("order_id", logistics_data.get("order_id", ""))
if order_id:
logistics_data["order_id"] = order_id
try:
chatwoot = ChatwootClient(account_id=int(state.get("account_id", 1)))
conversation_id = state.get("conversation_id")
if conversation_id:
logger.info(
"Preparing to send logistics info",
conversation_id=conversation_id,
tracking_number=logistics_data.get("tracking_number"),
carrier=logistics_data.get("carrier"),
language=detected_language
)
await chatwoot.send_logistics_info(
conversation_id=int(conversation_id),
logistics_data=logistics_data,
language=detected_language
)
logger.info(
"Logistics info sent successfully",
conversation_id=conversation_id,
tracking_number=logistics_data.get("tracking_number")
)
state = set_response(state, "")
state["state"] = ConversationState.GENERATING.value
return state
except Exception as e:
logger.error(
"Failed to send logistics info, falling back to text response",
error=str(e),
tracking_number=logistics_data.get("tracking_number")
)
# 如果有订单列表(多个订单),使用订单列表格式
if order_list and len(order_list) > 1:
try:
chatwoot = ChatwootClient(account_id=int(state.get("account_id", 1)))
conversation_id = state.get("conversation_id")
if conversation_id:
logger.info(
"Preparing to send order list",
conversation_id=conversation_id,
orders_count=len(order_list),
language=detected_language
)
await chatwoot.send_order_list(
conversation_id=int(conversation_id),
orders=order_list,
language=detected_language
)
logger.info(
"Order list sent successfully",
conversation_id=conversation_id,
orders_count=len(order_list),
language=detected_language
)
state = set_response(state, "")
state["state"] = ConversationState.GENERATING.value
return state
except Exception as e:
logger.error(
"Failed to send order list, falling back to text response",
error=str(e),
orders_count=len(order_list)
)
# 尝试使用 Chatwoot form 格式发送单个订单
if order_data:
try:
# 检查是否有有效的 order_id
if not order_data.get("order_id"):
logger.warning(
"No valid order_id in order_data, falling back to text response",
order_data=order_data
)
return await _generate_text_response(state)
chatwoot = ChatwootClient(account_id=int(state.get("account_id", 1)))
conversation_id = state.get("conversation_id")
if conversation_id:
# 记录订单数据(用于调试)
logger.info(
"Preparing to send order card",
conversation_id=conversation_id,
order_id=order_data.get("order_id"),
items_count=len(order_data.get("items", [])),
language=detected_language
)
await chatwoot.send_order_form(
conversation_id=int(conversation_id),
order_data=order_data,
language=detected_language
)
logger.info(
"Order card sent successfully",
conversation_id=conversation_id,
order_id=order_data.get("order_id"),
language=detected_language
)
state = set_response(state, "")
state["state"] = ConversationState.GENERATING.value
return state
except Exception as e:
logger.error(
"Failed to send order card, falling back to text response",
error=str(e),
order_id=order_data.get("order_id")
)
# 降级处理:使用原来的 LLM 生成逻辑
return await _generate_text_response(state)
def _parse_mall_order_data(data: dict[str, Any]) -> dict[str, Any]:
"""解析商城 API 返回的订单数据"""
# 记录原始数据结构(用于调试)
logger.info(
"Parsing mall order data",
data_keys=list(data.keys()),
has_order_id=bool(data.get("order_id")),
has_order_sn=bool(data.get("order_sn")),
has_nested_order=bool(data.get("order")),
order_id_preview=data.get("order_id", data.get("order_sn", "")),
# 如果有 order 字段,记录其内容类型和键
nested_order_type=type(data.get("order")).__name__ if data.get("order") else None,
nested_order_keys=list(data.get("order", {}).keys()) if isinstance(data.get("order"), dict) else None
)
# Mall API 返回结构:外层包含 userId, reqContext 等,实际的订单数据在 order 字段中
# 如果有嵌套的 order 字段,提取出来
actual_order_data = data.get("order", data) if data.get("order") else data
# 记录提取的订单数据结构(用于调试)
logger.info(
"Extracted order data structure",
actual_order_keys=list(actual_order_data.keys()) if isinstance(actual_order_data, dict) else type(actual_order_data).__name__,
has_items=bool(actual_order_data.get("items")),
has_order_items=bool(actual_order_data.get("order_items")),
has_products=bool(actual_order_data.get("products")),
has_orderProduct=bool(actual_order_data.get("orderProduct")),
has_orderGoods=bool(actual_order_data.get("orderGoods")),
has_goods=bool(actual_order_data.get("goods"))
)
order_data = {
"order_id": actual_order_data.get("orderId", actual_order_data.get("order_id", actual_order_data.get("order_sn", ""))),
"order_type": actual_order_data.get("orderType", actual_order_data.get("order_type", "")),
"status": actual_order_data.get("orderStatusId", actual_order_data.get("status", "unknown")),
"status_text": actual_order_data.get("statusText", ""),
"total_amount": actual_order_data.get("total", actual_order_data.get("total_amount", actual_order_data.get("order_amount", "0.00"))),
"shipping_fee": actual_order_data.get("shipping_fee", actual_order_data.get("freight_amount", "0")),
"payment_method": actual_order_data.get("paymentCode", actual_order_data.get("paymentMethod", "")),
}
# 如果 statusText 为空,使用 orderStatusId 映射获取状态文本
if not order_data["status_text"]:
status_id = actual_order_data.get("orderStatusId", actual_order_data.get("status"))
order_data["status_text"] = get_status_text(status_id)
# 下单时间
if actual_order_data.get("created_at"):
order_data["created_at"] = actual_order_data["created_at"]
elif actual_order_data.get("add_time"):
order_data["created_at"] = actual_order_data["add_time"]
elif actual_order_data.get("dateAdded"):
order_data["created_at"] = actual_order_data["dateAdded"]
# 商品列表 - 直接使用 Mall API 返回的 orderProduct 字段
order_product = actual_order_data.get("orderProduct", [])
# 记录商品列表数据结构(用于调试)
logger.info(
"Parsing orderProduct array",
has_order_product=bool(order_product),
order_product_count=len(order_product) if isinstance(order_product, list) else 0,
order_product_type=type(order_product).__name__
)
if order_product and isinstance(order_product, list) and len(order_product) > 0:
order_data["items"] = []
for product in order_product:
if not isinstance(product, dict):
continue
item_data = {
"name": product.get("productName", product.get("name", product.get("product_name", "未知商品"))),
"quantity": product.get("quantity", product.get("num", product.get("productNum", 1))),
"price": product.get("price", product.get("productPrice", product.get("product_price", "0.00")))
}
# 商品图片直接从 product 的 imageUrl 字段获取
image_url = product.get("imageUrl")
if image_url:
item_data["image_url"] = image_url
order_data["items"].append(item_data)
# 备注
if actual_order_data.get("remark") or actual_order_data.get("user_remark"):
order_data["remark"] = actual_order_data.get("remark", actual_order_data.get("user_remark", ""))
# 物流信息(如果有)
if actual_order_data.get("parcels") and len(actual_order_data.get("parcels", [])) > 0:
# parcels 是一个数组,包含物流信息
first_parcel = actual_order_data["parcels"][0] if isinstance(actual_order_data["parcels"], list) else actual_order_data["parcels"]
if isinstance(first_parcel, dict):
logistics_info = {
"carrier": first_parcel.get("courier", first_parcel.get("carrier", first_parcel.get("company", ""))),
"tracking_number": first_parcel.get("trackingCode", first_parcel.get("tracking_number", first_parcel.get("trackingNumber", ""))),
}
# 只有在有有效数据时才添加
if logistics_info["carrier"] or logistics_info["tracking_number"]:
order_data["logistics"] = logistics_info
# 收货地址
shipping_firstname = actual_order_data.get("shippingFirstname", "")
shipping_lastname = actual_order_data.get("shippingLastname", "")
shipping_company = actual_order_data.get("shippingCompany", "")
shipping_address_1 = actual_order_data.get("shippingAddress_1", "")
shipping_address_2 = actual_order_data.get("shippingAddress_2", "")
shipping_city = actual_order_data.get("shippingCity", "")
shipping_postcode = actual_order_data.get("shippingPostcode", "")
shipping_country = actual_order_data.get("shippingCountry", "")
shipping_zone = actual_order_data.get("shippingZone", "")
# 如果有任何地址字段存在,构建收货地址
if any([shipping_firstname, shipping_lastname, shipping_address_1, shipping_city]):
order_data["shipping_address"] = {
"name": f"{shipping_firstname} {shipping_lastname}".strip() or "",
"line1": shipping_address_1 or "",
"line2": shipping_address_2 or "",
"city": shipping_city or "",
"state": shipping_zone or "",
"postal_code": shipping_postcode or "",
"country": shipping_country or ""
}
# 发票地址
billing_firstname = actual_order_data.get("paymentFirstname", "")
billing_lastname = actual_order_data.get("paymentLastname", "")
billing_company = actual_order_data.get("paymentCompany", "")
billing_address_1 = actual_order_data.get("paymentAddress_1", "")
billing_address_2 = actual_order_data.get("paymentAddress_2", "")
billing_city = actual_order_data.get("paymentCity", "")
billing_postcode = actual_order_data.get("paymentPostcode", "")
billing_country = actual_order_data.get("paymentCountry", "")
billing_zone = actual_order_data.get("paymentZone", "")
# 如果有任何地址字段存在,构建发票地址
if any([billing_firstname, billing_lastname, billing_address_1, billing_city]):
order_data["billing_address"] = {
"name": f"{billing_firstname} {billing_lastname}".strip() or "",
"line1": billing_address_1 or "",
"line2": billing_address_2 or "",
"city": billing_city or "",
"state": billing_zone or "",
"postal_code": billing_postcode or "",
"country": billing_country or ""
}
return order_data
def _parse_order_data(data: dict[str, Any]) -> dict[str, Any]:
"""解析历史订单数据"""
return _parse_mall_order_data(data)
def _parse_logistics_data(data: dict[str, Any]) -> dict[str, Any]:
"""解析物流数据"""
# MCP 返回结构: {"success": true, "result": {...物流数据...}}
mcp_result = data.get("result", data) if data.get("result") else data
logger.info(
"Parsing logistics data",
data_keys=list(data.keys()) if isinstance(data, dict) else None,
has_result_in_data=bool(data.get("result")),
mcp_result_keys=list(mcp_result.keys()) if isinstance(mcp_result, dict) else None,
raw_tracking_number_value=repr(mcp_result.get("tracking_number")) if mcp_result.get("tracking_number") is not None else None,
raw_courier_value=repr(mcp_result.get("courier")) if mcp_result.get("courier") is not None else None,
has_tracking_number=bool(mcp_result.get("tracking_number")),
has_courier=bool(mcp_result.get("courier")),
has_timeline=bool(mcp_result.get("timeline"))
)
return {
"carrier": mcp_result.get("courier", mcp_result.get("carrier", mcp_result.get("express_name", "未知"))),
"tracking_number": mcp_result.get("tracking_number") or "",
"status": mcp_result.get("status"),
"estimated_delivery": mcp_result.get("estimatedDelivery"),
"timeline": mcp_result.get("timeline", [])
}
async def _generate_text_response(state: AgentState) -> AgentState:
"""生成纯文本回复(降级方案)"""
# 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)}")
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