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
|
||||
|
||||
Reference in New Issue
Block a user