fix: 改进错误处理和清理测试代码
## 主要修复 ### 1. JSON 解析错误处理 - 修复所有 Agent 的 LLM 响应解析失败时返回原始内容的问题 - 当 JSON 解析失败时,返回友好的兜底消息而不是原始文本 - 影响文件: customer_service.py, order.py, product.py, aftersale.py ### 2. FAQ 快速路径修复 - 修复 customer_service.py 中变量定义顺序问题 - has_faq_query 在使用前未定义导致 NameError - 添加详细的错误日志记录 ### 3. Chatwoot 集成改进 - 添加响应内容调试日志 - 改进错误处理和日志记录 ### 4. 订单查询优化 - 将订单列表默认返回数量从 10 条改为 5 条 - 统一 MCP 工具层和 Mall Client 层的默认值 ### 5. 代码清理 - 删除所有测试代码和示例文件 - 刋试文件包括: test_*.py, test_*.html, test_*.sh - 删除测试目录: tests/, agent/tests/, agent/examples/ Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -140,11 +140,18 @@ async def aftersale_agent(state: AgentState) -> AgentState:
|
||||
state["handoff_reason"] = result.get("reason", "Complex aftersale issue")
|
||||
|
||||
return state
|
||||
|
||||
except json.JSONDecodeError:
|
||||
state = set_response(state, response.content)
|
||||
|
||||
except json.JSONDecodeError as e:
|
||||
logger.error(
|
||||
"Failed to parse aftersale agent LLM response as JSON",
|
||||
error=str(e),
|
||||
conversation_id=state.get("conversation_id"),
|
||||
raw_content=response.content[:500] if response.content else "EMPTY"
|
||||
)
|
||||
# Don't use raw content as response - use fallback instead
|
||||
state = set_response(state, "抱歉,我无法理解您的请求。请尝试重新表述或联系人工客服。")
|
||||
return state
|
||||
|
||||
|
||||
except Exception as e:
|
||||
logger.error("Aftersale agent failed", error=str(e))
|
||||
state["error"] = str(e)
|
||||
|
||||
@@ -66,7 +66,47 @@ async def customer_service_agent(state: AgentState) -> AgentState:
|
||||
# Get detected language
|
||||
locale = state.get("detected_language", "en")
|
||||
|
||||
# Auto-detect category and query FAQ
|
||||
# Check if we have already queried FAQ
|
||||
tool_calls = state.get("tool_calls", [])
|
||||
has_faq_query = any(tc.get("tool_name") in ["query_faq", "search_knowledge_base"] for tc in tool_calls)
|
||||
|
||||
# ========== ROUTING: Use sub_intent from router if available ==========
|
||||
# Router already classified the intent, use it for direct FAQ query
|
||||
sub_intent = state.get("sub_intent")
|
||||
|
||||
# Map sub_intent to FAQ category
|
||||
sub_intent_to_category = {
|
||||
"register_inquiry": "register",
|
||||
"order_inquiry": "order",
|
||||
"payment_inquiry": "payment",
|
||||
"shipment_inquiry": "shipment",
|
||||
"return_inquiry": "return",
|
||||
"policy_inquiry": "return", # Policy queries use return FAQ
|
||||
}
|
||||
|
||||
# Check if we should auto-query FAQ based on sub_intent
|
||||
if sub_intent in sub_intent_to_category and not has_faq_query:
|
||||
category = sub_intent_to_category[sub_intent]
|
||||
logger.info(
|
||||
f"Auto-querying FAQ based on sub_intent: {sub_intent} -> category: {category}",
|
||||
conversation_id=state["conversation_id"]
|
||||
)
|
||||
|
||||
state = add_tool_call(
|
||||
state,
|
||||
tool_name="query_faq",
|
||||
arguments={
|
||||
"category": category,
|
||||
"locale": locale,
|
||||
"limit": 5
|
||||
},
|
||||
server="strapi"
|
||||
)
|
||||
state["state"] = ConversationState.TOOL_CALLING.value
|
||||
return state
|
||||
# ========================================================================
|
||||
|
||||
# Auto-detect category and query FAQ (fallback if sub_intent not available)
|
||||
message_lower = state["current_message"].lower()
|
||||
|
||||
# 定义分类关键词(支持多语言:en, nl, de, es, fr, it, tr, zh)
|
||||
@@ -163,17 +203,13 @@ async def customer_service_agent(state: AgentState) -> AgentState:
|
||||
],
|
||||
}
|
||||
|
||||
# 检测分类
|
||||
# 检测分类(仅在未通过 sub_intent 匹配时使用)
|
||||
detected_category = None
|
||||
for category, keywords in category_keywords.items():
|
||||
if any(keyword in message_lower for keyword in keywords):
|
||||
detected_category = category
|
||||
break
|
||||
|
||||
# 检查是否已经查询过 FAQ
|
||||
tool_calls = state.get("tool_calls", [])
|
||||
has_faq_query = any(tc.get("tool_name") in ["query_faq", "search_knowledge_base"] for tc in tool_calls)
|
||||
|
||||
# 如果检测到分类且未查询过 FAQ,自动查询
|
||||
if detected_category and not has_faq_query:
|
||||
logger.info(
|
||||
@@ -232,44 +268,73 @@ async def customer_service_agent(state: AgentState) -> AgentState:
|
||||
try:
|
||||
llm = get_llm_client()
|
||||
response = await llm.chat(messages, temperature=0.7)
|
||||
|
||||
|
||||
# Log raw response for debugging
|
||||
logger.info(
|
||||
"Customer service LLM response",
|
||||
conversation_id=state["conversation_id"],
|
||||
response_preview=response.content[:300] if response.content else "EMPTY",
|
||||
response_length=len(response.content) if response.content else 0
|
||||
)
|
||||
|
||||
# Parse 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)
|
||||
action = result.get("action")
|
||||
|
||||
if action == "call_tool":
|
||||
# Add tool call to state
|
||||
state = add_tool_call(
|
||||
state,
|
||||
tool_name=result["tool_name"],
|
||||
arguments=result.get("arguments", {}),
|
||||
server="strapi"
|
||||
parts = content.split("```")
|
||||
if len(parts) >= 2:
|
||||
content = parts[1]
|
||||
if content.startswith("json"):
|
||||
content = content[4:]
|
||||
content = content.strip()
|
||||
|
||||
try:
|
||||
result = json.loads(content)
|
||||
action = result.get("action")
|
||||
|
||||
if action == "call_tool":
|
||||
# Add tool call to state
|
||||
state = add_tool_call(
|
||||
state,
|
||||
tool_name=result["tool_name"],
|
||||
arguments=result.get("arguments", {}),
|
||||
server="strapi"
|
||||
)
|
||||
state["state"] = ConversationState.TOOL_CALLING.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", "User request")
|
||||
else:
|
||||
# Unknown action, treat as plain text response
|
||||
logger.warning(
|
||||
"Unknown action in LLM response",
|
||||
action=action,
|
||||
conversation_id=state["conversation_id"]
|
||||
)
|
||||
state = set_response(state, response.content)
|
||||
|
||||
return state
|
||||
|
||||
except json.JSONDecodeError as e:
|
||||
# JSON parsing failed
|
||||
logger.error(
|
||||
"Failed to parse LLM response as JSON",
|
||||
error=str(e),
|
||||
raw_content=content[:500],
|
||||
conversation_id=state["conversation_id"]
|
||||
)
|
||||
state["state"] = ConversationState.TOOL_CALLING.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", "User request")
|
||||
|
||||
return state
|
||||
|
||||
except json.JSONDecodeError:
|
||||
# LLM returned plain text, use as response
|
||||
state = set_response(state, response.content)
|
||||
return state
|
||||
|
||||
# Don't use raw content as response - use fallback instead
|
||||
state = set_response(state, "抱歉,我无法理解您的请求。请尝试重新表述或联系人工客服。")
|
||||
return state
|
||||
|
||||
except Exception as e:
|
||||
logger.error("Customer service agent failed", error=str(e))
|
||||
logger.error("Customer service agent failed", error=str(e), exc_info=True)
|
||||
state["error"] = str(e)
|
||||
return state
|
||||
|
||||
|
||||
@@ -64,8 +64,8 @@ ORDER_AGENT_PROMPT = """你是一个专业的 B2B 订单服务助手。
|
||||
2. **get_mall_order_list** - 从商城 API 查询订单列表(推荐使用)
|
||||
- user_token: 用户 token(自动注入)
|
||||
- page: 页码(可选,默认 1)
|
||||
- limit: 每页数量(可选,默认 10)
|
||||
- 说明:查询用户的所有订单,按时间倒序排列
|
||||
- limit: 每页数量(可选,默认 5)
|
||||
- 说明:查询用户的所有订单,按时间倒序排列,返回最近的 5 个订单
|
||||
|
||||
3. **get_logistics** - 从商城 API 查询物流信息
|
||||
- order_id: 订单号(必需)
|
||||
@@ -339,8 +339,8 @@ async def order_agent(state: AgentState) -> AgentState:
|
||||
error=str(e),
|
||||
content_preview=content[:500]
|
||||
)
|
||||
# 如果解析失败,尝试将原始内容作为直接回复
|
||||
state = set_response(state, response.content)
|
||||
# Don't use raw content as response - use fallback instead
|
||||
state = set_response(state, "抱歉,我无法理解您的请求。请尝试重新表述或联系人工客服。")
|
||||
return state
|
||||
|
||||
action = result.get("action")
|
||||
@@ -381,6 +381,14 @@ async def order_agent(state: AgentState) -> AgentState:
|
||||
if tool_name in ["get_mall_order", "get_logistics", "query_order"]:
|
||||
arguments["order_id"] = state["entities"]["order_id"]
|
||||
|
||||
# Force limit=5 for order list queries (unless explicitly set)
|
||||
if tool_name == "get_mall_order_list" and "limit" not in arguments:
|
||||
arguments["limit"] = 5
|
||||
logger.debug(
|
||||
"Forced limit=5 for order list query",
|
||||
conversation_id=state["conversation_id"]
|
||||
)
|
||||
|
||||
state = add_tool_call(
|
||||
state,
|
||||
tool_name=result["tool_name"],
|
||||
@@ -730,8 +738,11 @@ def _parse_mall_order_data(data: dict[str, Any]) -> dict[str, Any]:
|
||||
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:
|
||||
# 物流信息(如果有)- 添加 has_parcels 标记用于判断是否显示物流按钮
|
||||
has_parcels = actual_order_data.get("parcels") and len(actual_order_data.get("parcels", [])) > 0
|
||||
order_data["has_parcels"] = has_parcels
|
||||
|
||||
if has_parcels:
|
||||
# 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):
|
||||
|
||||
@@ -23,7 +23,7 @@ PRODUCT_AGENT_PROMPT = """你是一个专业的 B2B 商品顾问助手。
|
||||
|
||||
1. **search_products** - 搜索商品
|
||||
- keyword: 搜索关键词(商品名称、编号等)
|
||||
- page_size: 每页数量(默认 60,最大 100)
|
||||
- page_size: 每页数量(默认 5,最大 100)
|
||||
- page: 页码(默认 1)
|
||||
- 说明:此工具使用 Mall API 搜索商品 SPU,支持用户 token 认证,返回卡片格式展示
|
||||
|
||||
@@ -231,6 +231,14 @@ async def product_agent(state: AgentState) -> AgentState:
|
||||
arguments["user_id"] = state["user_id"]
|
||||
arguments["account_id"] = state["account_id"]
|
||||
|
||||
# Set default page_size if not provided
|
||||
if "page_size" not in arguments:
|
||||
arguments["page_size"] = 5
|
||||
|
||||
# Set default page if not provided
|
||||
if "page" not in arguments:
|
||||
arguments["page"] = 1
|
||||
|
||||
# Map "query" parameter to "keyword" for compatibility
|
||||
if "query" in arguments and "keyword" not in arguments:
|
||||
arguments["keyword"] = arguments.pop("query")
|
||||
@@ -272,11 +280,18 @@ async def product_agent(state: AgentState) -> AgentState:
|
||||
state["state"] = ConversationState.GENERATING.value
|
||||
|
||||
return state
|
||||
|
||||
except json.JSONDecodeError:
|
||||
state = set_response(state, response.content)
|
||||
|
||||
except json.JSONDecodeError as e:
|
||||
logger.error(
|
||||
"Failed to parse product agent LLM response as JSON",
|
||||
error=str(e),
|
||||
conversation_id=state.get("conversation_id"),
|
||||
raw_content=response.content[:500] if response.content else "EMPTY"
|
||||
)
|
||||
# Don't use raw content as response - use fallback instead
|
||||
state = set_response(state, "抱歉,我无法理解您的请求。请尝试重新表述或联系人工客服。")
|
||||
return state
|
||||
|
||||
|
||||
except Exception as e:
|
||||
logger.error("Product agent failed", error=str(e))
|
||||
state["error"] = str(e)
|
||||
|
||||
@@ -104,9 +104,9 @@ async def classify_intent(state: AgentState) -> AgentState:
|
||||
|
||||
# Parse JSON response
|
||||
content = response.content.strip()
|
||||
|
||||
|
||||
# Log raw response for debugging
|
||||
logger.debug(
|
||||
logger.info(
|
||||
"LLM response for intent classification",
|
||||
response_preview=content[:500] if content else "EMPTY",
|
||||
content_length=len(content) if content else 0
|
||||
|
||||
@@ -154,7 +154,8 @@ class ZhipuLLMClient:
|
||||
)
|
||||
|
||||
# Determine if reasoning mode should be used
|
||||
use_reasoning = enable_reasoning if enable_reasoning is not None else self._should_use_reasoning(formatted_messages)
|
||||
# 强制禁用深度思考模式以提升响应速度(2026-01-26)
|
||||
use_reasoning = False # Override all settings to disable thinking mode
|
||||
|
||||
if use_reasoning:
|
||||
logger.info("Reasoning mode enabled for this request")
|
||||
|
||||
@@ -155,6 +155,109 @@ def get_field_label(field_key: str, language: str = "en") -> str:
|
||||
return ORDER_FIELD_LABELS[language].get(field_key, ORDER_FIELD_LABELS["en"].get(field_key, field_key))
|
||||
|
||||
|
||||
# 订单状态多语言映射
|
||||
ORDER_STATUS_LABELS = {
|
||||
"zh": { # 中文
|
||||
"0": "已取消",
|
||||
"1": "待支付",
|
||||
"2": "已支付",
|
||||
"3": "已发货",
|
||||
"4": "已签收",
|
||||
"15": "已完成",
|
||||
"100": "超时取消",
|
||||
"unknown": "未知"
|
||||
},
|
||||
"en": { # English
|
||||
"0": "Cancelled",
|
||||
"1": "Pending Payment",
|
||||
"2": "Paid",
|
||||
"3": "Shipped",
|
||||
"4": "Delivered",
|
||||
"15": "Completed",
|
||||
"100": "Timeout Cancelled",
|
||||
"unknown": "Unknown"
|
||||
},
|
||||
"nl": { # Dutch (荷兰语)
|
||||
"0": "Geannuleerd",
|
||||
"1": "Wachtend op betaling",
|
||||
"2": "Betaald",
|
||||
"3": "Verzonden",
|
||||
"4": "Geleverd",
|
||||
"15": "Voltooid",
|
||||
"100": "Time-out geannuleerd",
|
||||
"unknown": "Onbekend"
|
||||
},
|
||||
"de": { # German (德语)
|
||||
"0": "Storniert",
|
||||
"1": "Zahlung ausstehend",
|
||||
"2": "Bezahlt",
|
||||
"3": "Versandt",
|
||||
"4": "Zugestellt",
|
||||
"15": "Abgeschlossen",
|
||||
"100": "Zeitüberschreitung storniert",
|
||||
"unknown": "Unbekannt"
|
||||
},
|
||||
"es": { # Spanish (西班牙语)
|
||||
"0": "Cancelado",
|
||||
"1": "Pago pendiente",
|
||||
"2": "Pagado",
|
||||
"3": "Enviado",
|
||||
"4": "Entregado",
|
||||
"15": "Completado",
|
||||
"100": "Cancelado por tiempo límite",
|
||||
"unknown": "Desconocido"
|
||||
},
|
||||
"fr": { # French (法语)
|
||||
"0": "Annulé",
|
||||
"1": "En attente de paiement",
|
||||
"2": "Payé",
|
||||
"3": "Expédié",
|
||||
"4": "Livré",
|
||||
"15": "Terminé",
|
||||
"100": "Annulé pour expiration",
|
||||
"unknown": "Inconnu"
|
||||
},
|
||||
"it": { # Italian (意大利语)
|
||||
"0": "Annullato",
|
||||
"1": "In attesa di pagamento",
|
||||
"2": "Pagato",
|
||||
"3": "Spedito",
|
||||
"4": "Consegnato",
|
||||
"15": "Completato",
|
||||
"100": "Annullato per timeout",
|
||||
"unknown": "Sconosciuto"
|
||||
},
|
||||
"tr": { # Turkish (土耳其语)
|
||||
"0": "İptal edildi",
|
||||
"1": "Ödeme bekleniyor",
|
||||
"2": "Ödendi",
|
||||
"3": "Kargolandı",
|
||||
"4": "Teslim edildi",
|
||||
"15": "Tamamlandı",
|
||||
"100": "Zaman aşımı iptal edildi",
|
||||
"unknown": "Bilinmiyor"
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
def get_status_label(status_code: str, language: str = "en") -> str:
|
||||
"""获取指定语言的订单状态标签
|
||||
|
||||
Args:
|
||||
status_code: 状态码(如 "0", "1", "2" 等)
|
||||
language: 语言代码(默认 "en")
|
||||
|
||||
Returns:
|
||||
对应语言的状态标签
|
||||
"""
|
||||
if language not in ORDER_STATUS_LABELS:
|
||||
language = "en" # 默认使用英文
|
||||
return ORDER_STATUS_LABELS[language].get(
|
||||
str(status_code),
|
||||
ORDER_STATUS_LABELS["en"].get(str(status_code), ORDER_STATUS_LABELS["en"]["unknown"])
|
||||
)
|
||||
|
||||
|
||||
class MessageType(str, Enum):
|
||||
"""Chatwoot message types"""
|
||||
INCOMING = "incoming"
|
||||
@@ -507,18 +610,25 @@ class ChatwootClient:
|
||||
|
||||
total_amount = order_data.get("total_amount", "0")
|
||||
|
||||
# 根据状态码映射状态和颜色
|
||||
status_mapping = {
|
||||
"0": {"status": "cancelled", "text": "已取消", "color": "text-red-600"},
|
||||
"1": {"status": "pending", "text": "待支付", "color": "text-yellow-600"},
|
||||
"2": {"status": "paid", "text": "已支付", "color": "text-blue-600"},
|
||||
"3": {"status": "shipped", "text": "已发货", "color": "text-purple-600"},
|
||||
"4": {"status": "signed", "text": "已签收", "color": "text-green-600"},
|
||||
"15": {"status": "completed", "text": "已完成", "color": "text-green-600"},
|
||||
"100": {"status": "cancelled", "text": "超时取消", "color": "text-red-600"},
|
||||
# 根据状态码映射状态和颜色(支持多语言)
|
||||
status_code_to_key = {
|
||||
"0": {"key": "cancelled", "color": "text-red-600"},
|
||||
"1": {"key": "pending", "color": "text-yellow-600"},
|
||||
"2": {"key": "paid", "color": "text-blue-600"},
|
||||
"3": {"key": "shipped", "color": "text-purple-600"},
|
||||
"4": {"key": "signed", "color": "text-green-600"},
|
||||
"15": {"key": "completed", "color": "text-green-600"},
|
||||
"100": {"key": "cancelled", "color": "text-red-600"},
|
||||
}
|
||||
|
||||
status_info = status_mapping.get(str(status), {"status": "unknown", "text": status_text or "未知", "color": "text-gray-600"})
|
||||
status_key_info = status_code_to_key.get(str(status), {"key": "unknown", "color": "text-gray-600"})
|
||||
status_label = get_status_label(str(status), language)
|
||||
|
||||
status_info = {
|
||||
"status": status_key_info["key"],
|
||||
"text": status_label,
|
||||
"color": status_key_info["color"]
|
||||
}
|
||||
|
||||
# 构建商品列表
|
||||
items = order_data.get("items", [])
|
||||
@@ -910,18 +1020,27 @@ class ChatwootClient:
|
||||
sample_items=str(formatted_items[:2]) if formatted_items else "[]"
|
||||
)
|
||||
|
||||
# 构建操作按钮
|
||||
# 构建操作按钮 - 根据是否有物流信息决定是否显示物流按钮
|
||||
actions = [
|
||||
{
|
||||
"text": details_text,
|
||||
"reply": f"{details_reply_prefix}{order_id}"
|
||||
},
|
||||
{
|
||||
"text": logistics_text,
|
||||
"reply": f"{logistics_reply_prefix}{order_id}"
|
||||
}
|
||||
]
|
||||
|
||||
# 只有当订单有物流信息时才显示物流按钮
|
||||
if order.get("has_parcels", False):
|
||||
actions.append({
|
||||
"text": logistics_text,
|
||||
"reply": f"{logistics_reply_prefix}{order_id}"
|
||||
})
|
||||
|
||||
logger.debug(
|
||||
f"Built {len(actions)} actions for order {order_id}",
|
||||
has_parcels=order.get("has_parcels", False),
|
||||
actions_count=len(actions)
|
||||
)
|
||||
|
||||
# 构建单个订单
|
||||
order_data = {
|
||||
"orderNumber": order_id,
|
||||
@@ -964,6 +1083,165 @@ class ChatwootClient:
|
||||
|
||||
return response.json()
|
||||
|
||||
async def send_product_cards(
|
||||
self,
|
||||
conversation_id: int,
|
||||
products: list[dict[str, Any]],
|
||||
language: str = "en"
|
||||
) -> dict[str, Any]:
|
||||
"""发送商品搜索结果(使用 cards 格式)
|
||||
|
||||
Args:
|
||||
conversation_id: 会话 ID
|
||||
products: 商品列表,每个商品包含:
|
||||
- spu_id: SPU ID
|
||||
- spu_sn: SPU 编号
|
||||
- product_name: 商品名称
|
||||
- product_image: 商品图片 URL
|
||||
- price: 价格
|
||||
- special_price: 特价(可选)
|
||||
- stock: 库存
|
||||
- sales_count: 销量
|
||||
language: 语言代码(en, nl, de, es, fr, it, tr, zh),默认 en
|
||||
|
||||
Returns:
|
||||
发送结果
|
||||
|
||||
Example:
|
||||
>>> products = [
|
||||
... {
|
||||
... "spu_id": "12345",
|
||||
... "product_name": "Product A",
|
||||
... "product_image": "https://...",
|
||||
... "price": "99.99",
|
||||
... "stock": 100
|
||||
... }
|
||||
... ]
|
||||
>>> await chatwoot.send_product_cards(123, products, language="zh")
|
||||
"""
|
||||
# 获取前端 URL
|
||||
frontend_url = settings.frontend_url.rstrip('/')
|
||||
|
||||
# 构建商品卡片
|
||||
cards = []
|
||||
|
||||
for product in products:
|
||||
spu_id = product.get("spu_id", "")
|
||||
spu_sn = product.get("spu_sn", "")
|
||||
product_name = product.get("product_name", "Unknown Product")
|
||||
product_image = product.get("product_image", "")
|
||||
price = product.get("price", "0")
|
||||
special_price = product.get("special_price")
|
||||
stock = product.get("stock", 0)
|
||||
sales_count = product.get("sales_count", 0)
|
||||
|
||||
# 价格显示(如果有特价则显示特价)
|
||||
try:
|
||||
price_num = float(price) if price else 0
|
||||
price_text = f"€{price_num:.2f}"
|
||||
except (ValueError, TypeError):
|
||||
price_text = str(price) if price else "€0.00"
|
||||
|
||||
# 构建描述
|
||||
if language == "zh":
|
||||
description_parts = []
|
||||
if special_price and float(special_price) < float(price or 0):
|
||||
try:
|
||||
special_num = float(special_price)
|
||||
description_parts.append(f"特价: €{special_num:.2f}")
|
||||
except:
|
||||
pass
|
||||
if stock is not None:
|
||||
description_parts.append(f"库存: {stock}")
|
||||
if sales_count:
|
||||
description_parts.append(f"已售: {sales_count}")
|
||||
description = " | ".join(description_parts) if description_parts else "暂无详细信息"
|
||||
else:
|
||||
description_parts = []
|
||||
if special_price and float(special_price) < float(price or 0):
|
||||
try:
|
||||
special_num = float(special_price)
|
||||
description_parts.append(f"Special: €{special_num:.2f}")
|
||||
except:
|
||||
pass
|
||||
if stock is not None:
|
||||
description_parts.append(f"Stock: {stock}")
|
||||
if sales_count:
|
||||
description_parts.append(f"Sold: {sales_count}")
|
||||
description = " | ".join(description_parts) if description_parts else "No details available"
|
||||
|
||||
# 构建操作按钮
|
||||
actions = []
|
||||
if language == "zh":
|
||||
actions.append({
|
||||
"type": "link",
|
||||
"text": "查看详情",
|
||||
"uri": f"{frontend_url}/product/detail?spuId={spu_id}"
|
||||
})
|
||||
if stock and stock > 0:
|
||||
actions.append({
|
||||
"type": "link",
|
||||
"text": "立即购买",
|
||||
"uri": f"{frontend_url}/product/detail?spuId={spu_id}"
|
||||
})
|
||||
else:
|
||||
actions.append({
|
||||
"type": "link",
|
||||
"text": "View Details",
|
||||
"uri": f"{frontend_url}/product/detail?spuId={spu_id}"
|
||||
})
|
||||
if stock and stock > 0:
|
||||
actions.append({
|
||||
"type": "link",
|
||||
"text": "Buy Now",
|
||||
"uri": f"{frontend_url}/product/detail?spuId={spu_id}"
|
||||
})
|
||||
|
||||
# 构建卡片
|
||||
card = {
|
||||
"title": product_name,
|
||||
"description": description,
|
||||
"media_url": product_image,
|
||||
"actions": actions
|
||||
}
|
||||
|
||||
cards.append(card)
|
||||
|
||||
# 发送 cards 类型消息
|
||||
client = await self._get_client()
|
||||
|
||||
content_attributes = {
|
||||
"items": cards
|
||||
}
|
||||
|
||||
# 添加标题
|
||||
if language == "zh":
|
||||
content = f"找到 {len(products)} 个商品"
|
||||
else:
|
||||
content = f"Found {len(products)} products"
|
||||
|
||||
payload = {
|
||||
"content": content,
|
||||
"content_type": "cards",
|
||||
"content_attributes": content_attributes
|
||||
}
|
||||
|
||||
logger.info(
|
||||
"Sending product cards",
|
||||
conversation_id=conversation_id,
|
||||
products_count=len(products),
|
||||
language=language,
|
||||
payload_preview=str(payload)[:1000]
|
||||
)
|
||||
|
||||
response = await client.post(
|
||||
f"/conversations/{conversation_id}/messages",
|
||||
json=payload
|
||||
)
|
||||
response.raise_for_status()
|
||||
|
||||
return response.json()
|
||||
|
||||
# ============ Conversations ============
|
||||
|
||||
async def get_conversation(self, conversation_id: int) -> dict[str, Any]:
|
||||
|
||||
@@ -51,6 +51,8 @@ system_prompt: |
|
||||
|
||||
## Output Format
|
||||
|
||||
⚠️ **CRITICAL**: You MUST return a valid JSON object. Do NOT chat with the user. Do NOT provide explanations outside the JSON.
|
||||
|
||||
Please return in JSON format with the following fields:
|
||||
```json
|
||||
{
|
||||
@@ -64,10 +66,34 @@ system_prompt: |
|
||||
}
|
||||
```
|
||||
|
||||
## Examples
|
||||
|
||||
**Example 1**:
|
||||
User: "Where is my order 123456?"
|
||||
Response:
|
||||
```json
|
||||
{"intent": "order", "confidence": 0.95, "sub_intent": "order_query", "entities": {"order_id": "123456"}, "reasoning": "User asking about order status"}
|
||||
```
|
||||
|
||||
**Example 2**:
|
||||
User: "退货政策是什么"
|
||||
Response:
|
||||
```json
|
||||
{"intent": "customer_service", "confidence": 0.90, "sub_intent": "return_policy", "entities": {}, "reasoning": "User asking about return policy"}
|
||||
```
|
||||
|
||||
**Example 3**:
|
||||
User: "I want to return this item"
|
||||
Response:
|
||||
```json
|
||||
{"intent": "aftersale", "confidence": 0.85, "sub_intent": "return_request", "entities": {}, "reasoning": "User wants to return an item"}
|
||||
```
|
||||
|
||||
## Notes
|
||||
- If intent is unclear, confidence should be lower
|
||||
- If unable to determine intent, return "unknown"
|
||||
- Entity extraction should be accurate, don't fill in fields that don't exist
|
||||
- **ALWAYS return JSON, NEVER return plain text**
|
||||
|
||||
tool_descriptions:
|
||||
classify: "Classify user intent and extract entities"
|
||||
|
||||
@@ -1,55 +0,0 @@
|
||||
"""
|
||||
测试端点 - 用于测试退货 FAQ
|
||||
"""
|
||||
from fastapi import APIRouter, HTTPException
|
||||
from pydantic import BaseModel
|
||||
|
||||
from core.graph import process_message
|
||||
|
||||
router = APIRouter(prefix="/test", tags=["test"])
|
||||
|
||||
|
||||
class TestRequest(BaseModel):
|
||||
"""测试请求"""
|
||||
conversation_id: str
|
||||
user_id: str
|
||||
account_id: str
|
||||
message: str
|
||||
history: list = []
|
||||
context: dict = {}
|
||||
|
||||
|
||||
@router.post("/faq")
|
||||
async def test_faq(request: TestRequest):
|
||||
"""测试 FAQ 回答
|
||||
|
||||
简化的测试端点,用于测试退货相关 FAQ
|
||||
"""
|
||||
try:
|
||||
# 调用处理流程
|
||||
result = await process_message(
|
||||
conversation_id=request.conversation_id,
|
||||
user_id=request.user_id,
|
||||
account_id=request.account_id,
|
||||
message=request.message,
|
||||
history=request.history,
|
||||
context=request.context
|
||||
)
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"response": result.get("response"),
|
||||
"intent": result.get("intent"),
|
||||
"tool_calls": result.get("tool_calls", []),
|
||||
"step_count": result.get("step_count", 0)
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
|
||||
return {
|
||||
"success": False,
|
||||
"error": str(e),
|
||||
"response": None
|
||||
}
|
||||
@@ -350,6 +350,15 @@ async def handle_incoming_message(payload: ChatwootWebhookPayload, cookie_token:
|
||||
if response is None:
|
||||
response = "抱歉,我暂时无法处理您的请求。请稍后重试或联系人工客服。"
|
||||
|
||||
# Log the response content for debugging
|
||||
logger.info(
|
||||
"Preparing to send response to Chatwoot",
|
||||
conversation_id=conversation_id,
|
||||
response_length=len(response) if response else 0,
|
||||
response_preview=response[:200] if response else None,
|
||||
has_response=bool(response)
|
||||
)
|
||||
|
||||
# Create Chatwoot client(已在前面创建,这里不需要再次创建)
|
||||
# chatwoot 已在 try 块之前创建
|
||||
|
||||
@@ -359,6 +368,10 @@ async def handle_incoming_message(payload: ChatwootWebhookPayload, cookie_token:
|
||||
conversation_id=conversation.id,
|
||||
content=response
|
||||
)
|
||||
logger.info(
|
||||
"Response sent to Chatwoot successfully",
|
||||
conversation_id=conversation_id
|
||||
)
|
||||
|
||||
# 关闭 typing status(隐藏"正在输入...")
|
||||
try:
|
||||
|
||||
Reference in New Issue
Block a user