diff --git a/agent/agents/aftersale.py b/agent/agents/aftersale.py index 5d1ce6b..bf8deef 100644 --- a/agent/agents/aftersale.py +++ b/agent/agents/aftersale.py @@ -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) diff --git a/agent/agents/customer_service.py b/agent/agents/customer_service.py index 96d9651..ce3c6e3 100644 --- a/agent/agents/customer_service.py +++ b/agent/agents/customer_service.py @@ -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 diff --git a/agent/agents/order.py b/agent/agents/order.py index 47cca05..203e46a 100644 --- a/agent/agents/order.py +++ b/agent/agents/order.py @@ -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): diff --git a/agent/agents/product.py b/agent/agents/product.py index f2ba0f7..d43f7ad 100644 --- a/agent/agents/product.py +++ b/agent/agents/product.py @@ -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) diff --git a/agent/agents/router.py b/agent/agents/router.py index f469760..fec0d64 100644 --- a/agent/agents/router.py +++ b/agent/agents/router.py @@ -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 diff --git a/agent/core/llm.py b/agent/core/llm.py index f815a6c..43dc06c 100644 --- a/agent/core/llm.py +++ b/agent/core/llm.py @@ -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") diff --git a/agent/integrations/chatwoot.py b/agent/integrations/chatwoot.py index a448a31..9812a43 100644 --- a/agent/integrations/chatwoot.py +++ b/agent/integrations/chatwoot.py @@ -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]: diff --git a/agent/prompts/router/en.yaml b/agent/prompts/router/en.yaml index b23586f..025326a 100644 --- a/agent/prompts/router/en.yaml +++ b/agent/prompts/router/en.yaml @@ -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" diff --git a/agent/test_endpoint.py b/agent/test_endpoint.py deleted file mode 100644 index 46bbae7..0000000 --- a/agent/test_endpoint.py +++ /dev/null @@ -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 - } diff --git a/agent/tests/__init__.py b/agent/tests/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/agent/webhooks/chatwoot_webhook.py b/agent/webhooks/chatwoot_webhook.py index c9744c8..e0de0f9 100644 --- a/agent/webhooks/chatwoot_webhook.py +++ b/agent/webhooks/chatwoot_webhook.py @@ -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: diff --git a/docs/test-chat-debug.html b/docs/test-chat-debug.html deleted file mode 100644 index 1cda6fa..0000000 --- a/docs/test-chat-debug.html +++ /dev/null @@ -1,337 +0,0 @@ - - -
- - -实时监控 Widget 状态和消息流
- -- 💡 提示:点击按钮后,在右下角聊天窗口中按 Ctrl+V 粘贴并发送 -
-基于 LangGraph + MCP 的智能客服系统
- -点击以下问题直接复制到聊天窗口:
-自动识别客户需求并分类
-快速检索 FAQ 和政策文档
-查询订单、售后等服务
-支持上下文理解的连续对话
-如果看不到 AI 回复:
-docker logs ai_agent --tail 50简化测试页面 - Chatwoot 官方集成方式
- -点击以下问题复制到剪贴板,然后在聊天窗口粘贴(Ctrl+V)并发送:
-自动识别客户需求并分类
-快速检索 FAQ 和政策文档
-查询订单、售后等服务
-支持上下文理解的连续对话
-Chatwoot 服务:http://localhost:3000
-Website Token:39PNCMvbMk3NvB7uaDNucc6o
-集成方式:Chatwoot 官方 SDK
-