- 添加本地 FAQ 库快速路径(问候语等社交响应) - 修复 Chatwoot 重启循环问题(PID 文件清理) - 添加 LLM 响应缓存(Redis 缓存,提升性能) - 添加智能推理模式(根据查询复杂度自动启用) - 添加订单卡片消息功能(Chatwoot 富媒体) - 增加 LLM 超时时间至 60 秒 Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
228 lines
8.0 KiB
Python
228 lines
8.0 KiB
Python
"""
|
||
Customer Service Agent - Handles FAQ and general inquiries
|
||
"""
|
||
import json
|
||
from typing import Any
|
||
|
||
from core.state import AgentState, ConversationState, add_tool_call, set_response
|
||
from core.llm import get_llm_client, Message
|
||
from prompts import get_prompt
|
||
from utils.logger import get_logger
|
||
from utils.faq_library import get_faq_library
|
||
|
||
logger = get_logger(__name__)
|
||
|
||
|
||
async def customer_service_agent(state: AgentState) -> AgentState:
|
||
"""Customer service agent node
|
||
|
||
Handles FAQ, company info, and general inquiries using Strapi MCP tools.
|
||
|
||
Args:
|
||
state: Current agent state
|
||
|
||
Returns:
|
||
Updated state with tool calls or response
|
||
"""
|
||
logger.info(
|
||
"Customer service agent processing",
|
||
conversation_id=state["conversation_id"]
|
||
)
|
||
|
||
state["current_agent"] = "customer_service"
|
||
state["agent_history"].append("customer_service")
|
||
state["state"] = ConversationState.PROCESSING.value
|
||
|
||
# Check if we have tool results to process
|
||
if state["tool_results"]:
|
||
return await _generate_response_from_results(state)
|
||
|
||
# ========== FAST PATH: Check if FAQ was already matched at router ==========
|
||
# Router already checked FAQ and stored response if found
|
||
if "faq_response" in state and state["faq_response"]:
|
||
logger.info(
|
||
"Using FAQ response from router",
|
||
conversation_id=state["conversation_id"],
|
||
response_length=len(state["faq_response"])
|
||
)
|
||
return set_response(state, state["faq_response"])
|
||
# =========================================================================
|
||
|
||
# ========== FAST PATH: Check local FAQ library first (backup) ==========
|
||
# This provides instant response for common questions without API calls
|
||
# This is a fallback in case FAQ wasn't matched at router level
|
||
faq_library = get_faq_library()
|
||
faq_response = faq_library.find_match(state["current_message"])
|
||
|
||
if faq_response:
|
||
logger.info(
|
||
"FAQ match found, returning instant response",
|
||
conversation_id=state["conversation_id"],
|
||
response_length=len(faq_response)
|
||
)
|
||
return set_response(state, faq_response)
|
||
# ============================================================
|
||
|
||
# Get detected language
|
||
locale = state.get("detected_language", "en")
|
||
|
||
# Auto-detect category and query FAQ
|
||
message_lower = state["current_message"].lower()
|
||
|
||
# 定义分类关键词
|
||
category_keywords = {
|
||
"register": ["register", "sign up", "account", "login", "password", "forgot"],
|
||
"order": ["order", "place order", "cancel order", "modify order", "change order"],
|
||
"payment": ["pay", "payment", "checkout", "voucher", "discount", "promo"],
|
||
"shipment": ["ship", "shipping", "delivery", "courier", "transit", "logistics", "tracking"],
|
||
"return": ["return", "refund", "exchange", "defective", "damaged"],
|
||
}
|
||
|
||
# 检测分类
|
||
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(
|
||
f"Auto-querying FAQ for category: {detected_category}",
|
||
conversation_id=state["conversation_id"]
|
||
)
|
||
|
||
# 自动添加 FAQ 工具调用
|
||
state = add_tool_call(
|
||
state,
|
||
tool_name="query_faq",
|
||
arguments={
|
||
"category": detected_category,
|
||
"locale": locale,
|
||
"limit": 5
|
||
},
|
||
server="strapi"
|
||
)
|
||
state["state"] = ConversationState.TOOL_CALLING.value
|
||
return state
|
||
|
||
# 如果询问营业时间或联系方式,自动查询公司信息
|
||
if any(keyword in message_lower for keyword in ["opening hour", "contact", "address", "phone", "email"]) and not has_faq_query:
|
||
logger.info(
|
||
"Auto-querying company info",
|
||
conversation_id=state["conversation_id"]
|
||
)
|
||
|
||
state = add_tool_call(
|
||
state,
|
||
tool_name="get_company_info",
|
||
arguments={
|
||
"section": "contact",
|
||
"locale": locale
|
||
},
|
||
server="strapi"
|
||
)
|
||
state["state"] = ConversationState.TOOL_CALLING.value
|
||
return state
|
||
|
||
# Build messages for LLM
|
||
# Load prompt in detected language
|
||
system_prompt = get_prompt("customer_service", locale)
|
||
|
||
messages = [
|
||
Message(role="system", content=system_prompt),
|
||
]
|
||
|
||
# Add conversation history
|
||
for msg in state["messages"][-6:]:
|
||
messages.append(Message(role=msg["role"], content=msg["content"]))
|
||
|
||
# Add current message
|
||
messages.append(Message(role="user", content=state["current_message"]))
|
||
|
||
try:
|
||
llm = get_llm_client()
|
||
response = await llm.chat(messages, temperature=0.7)
|
||
|
||
# Parse response
|
||
content = response.content.strip()
|
||
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"
|
||
)
|
||
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
|
||
|
||
except Exception as e:
|
||
logger.error("Customer service agent failed", error=str(e))
|
||
state["error"] = str(e)
|
||
return state
|
||
|
||
|
||
async def _generate_response_from_results(state: AgentState) -> AgentState:
|
||
"""Generate response based on tool results"""
|
||
|
||
# Build context from tool results
|
||
tool_context = []
|
||
for result in state["tool_results"]:
|
||
if result["success"]:
|
||
tool_context.append(f"Tool {result['tool_name']} returned:\n{json.dumps(result['data'], ensure_ascii=False, indent=2)}")
|
||
else:
|
||
tool_context.append(f"Tool {result['tool_name']} failed: {result['error']}")
|
||
|
||
prompt = f"""Based on the following tool returned information, generate a response to the user.
|
||
|
||
User question: {state["current_message"]}
|
||
|
||
Tool returned information:
|
||
{chr(10).join(tool_context)}
|
||
|
||
Please generate a friendly and professional response. If the tool did not return useful information, honestly inform the user and suggest other ways to get help.
|
||
Return only the response content, do not return JSON."""
|
||
|
||
messages = [
|
||
Message(role="system", content="You are a professional B2B customer service assistant, please answer user questions based on tool returned information."),
|
||
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("Response generation failed", error=str(e))
|
||
state = set_response(state, "Sorry, there was a problem processing your request. Please try again later or contact customer support.")
|
||
return state
|