feat: 增强 Agent 系统和完善项目结构

主要改进:
- Agent 增强: 订单查询、售后支持、客服路由等功能优化
- 新增语言检测和 Token 管理模块
- 改进 Chatwoot webhook 处理和用户标识
- MCP 服务器增强: 订单 MCP 和 Strapi MCP 功能扩展
- 新增商城客户端、知识库、缓存和同步模块
- 添加多语言提示词系统 (YAML)
- 完善项目结构: 整理文档、脚本和测试文件
- 新增调试和测试工具脚本

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
wangliang
2026-01-16 16:28:47 +08:00
parent 0e59f3067e
commit e093995368
48 changed files with 5263 additions and 395 deletions

View File

@@ -7,11 +7,7 @@ import httpx
from langgraph.graph import StateGraph, END
from .state import AgentState, ConversationState, mark_finished, add_tool_result, set_response
from agents.router import classify_intent, route_by_intent
from agents.customer_service import customer_service_agent
from agents.order import order_agent
from agents.aftersale import aftersale_agent
from agents.product import product_agent
# 延迟导入以避免循环依赖
from config import settings
from utils.logger import get_logger
@@ -197,20 +193,36 @@ async def handle_error(state: AgentState) -> AgentState:
def should_call_tools(state: AgentState) -> Literal["call_tools", "send_response", "back_to_agent"]:
"""Determine if tools need to be called"""
logger.debug(
"Checking if tools should be called",
conversation_id=state.get("conversation_id"),
has_tool_calls=bool(state.get("tool_calls")),
tool_calls_count=len(state.get("tool_calls", [])),
has_response=bool(state.get("response")),
state_value=state.get("state")
)
# If there are pending tool calls, execute them
if state.get("tool_calls"):
logger.info(
"Routing to tool execution",
tool_count=len(state["tool_calls"])
)
return "call_tools"
# If we have a response ready, send it
if state.get("response"):
logger.debug("Routing to send_response (has response)")
return "send_response"
# If we're waiting for info, send the question
if state.get("state") == ConversationState.AWAITING_INFO.value:
logger.debug("Routing to send_response (awaiting info)")
return "send_response"
# Otherwise, something went wrong
logger.warning("Unexpected state, routing to send_response", state=state.get("state"))
return "send_response"
@@ -255,13 +267,20 @@ def check_completion(state: AgentState) -> Literal["continue", "end", "error"]:
def create_agent_graph() -> StateGraph:
"""Create the main agent workflow graph
Returns:
Compiled LangGraph workflow
"""
# 延迟导入以避免循环依赖
from agents.router import classify_intent, route_by_intent
from agents.customer_service import customer_service_agent
from agents.order import order_agent
from agents.aftersale import aftersale_agent
from agents.product import product_agent
# Create graph with AgentState
graph = StateGraph(AgentState)
# Add nodes
graph.add_node("receive", receive_message)
graph.add_node("classify", classify_intent)
@@ -347,10 +366,11 @@ async def process_message(
account_id: str,
message: str,
history: list[dict] = None,
context: dict = None
context: dict = None,
user_token: str = None
) -> AgentState:
"""Process a user message through the agent workflow
Args:
conversation_id: Chatwoot conversation ID
user_id: User identifier
@@ -358,12 +378,13 @@ async def process_message(
message: User's message
history: Previous conversation history
context: Existing conversation context
user_token: User JWT token for API calls
Returns:
Final agent state with response
"""
from .state import create_initial_state
# Create initial state
initial_state = create_initial_state(
conversation_id=conversation_id,
@@ -371,7 +392,8 @@ async def process_message(
account_id=account_id,
current_message=message,
messages=history,
context=context
context=context,
user_token=user_token
)
# Get compiled graph

View File

@@ -0,0 +1,150 @@
"""
Language Detection Module
Automatically detects user message language and maps to Strapi-supported locales.
"""
from typing import Optional
from langdetect import detect, LangDetectException
from utils.logger import get_logger
logger = get_logger(__name__)
# Strapi-supported locales
SUPPORTED_LOCALES = ["en", "nl", "de", "es", "fr", "it", "tr"]
# Language code to locale mapping
LOCALE_MAP = {
"en": "en", # English
"nl": "nl", # Dutch
"de": "de", # German
"es": "es", # Spanish
"fr": "fr", # French
"it": "it", # Italian
"tr": "tr", # Turkish
# Fallback mappings for unsupported languages
"af": "en", # Afrikaans -> English
"no": "en", # Norwegian -> English
"sv": "en", # Swedish -> English
"da": "en", # Danish -> English
"pl": "en", # Polish -> English
"pt": "en", # Portuguese -> English
"ru": "en", # Russian -> English
"zh": "en", # Chinese -> English
"ja": "en", # Japanese -> English
"ko": "en", # Korean -> English
"ar": "en", # Arabic -> English
"hi": "en", # Hindi -> English
}
# Minimum confidence threshold
MIN_CONFIDENCE = 0.7
# Minimum message length for reliable detection
MIN_LENGTH = 10
def detect_language(text: str) -> tuple[str, float]:
"""Detect language from text
Args:
text: Input text to detect language from
Returns:
Tuple of (locale_code, confidence_score)
locale_code: Strapi locale (en, nl, de, etc.)
confidence_score: Detection confidence (0-1), 0.0 if detection failed
"""
# Check minimum length
if len(text.strip()) < MIN_LENGTH:
logger.debug("Message too short for reliable detection", length=len(text))
return "en", 0.0
try:
# Detect language using langdetect
detected = detect(text)
logger.debug("Language detected", language=detected, text_length=len(text))
# Map to Strapi locale
locale = map_to_locale(detected)
return locale, 0.85 # langdetect doesn't provide confidence, use default
except LangDetectException as e:
logger.warning("Language detection failed", error=str(e))
return "en", 0.0
def map_to_locale(lang_code: str) -> str:
"""Map detected language code to Strapi locale
Args:
lang_code: ISO 639-1 language code (e.g., "en", "nl", "de")
Returns:
Strapi locale code, or "en" as default if not supported
"""
# Direct mapping
if lang_code in SUPPORTED_LOCALES:
return lang_code
# Use locale map
locale = LOCALE_MAP.get(lang_code, "en")
if locale != lang_code and locale == "en":
logger.info(
"Unsupported language mapped to default",
detected_language=lang_code,
mapped_locale=locale
)
return locale
def get_cached_or_detect(state, text: str) -> str:
"""Get language from cache or detect from text
Priority:
1. Use state.detected_language if available
2. Use state.context["language"] if available
3. Detect from text
Args:
state: Agent state
text: Input text to detect language from
Returns:
Detected locale code
"""
# Check state first
if state.get("detected_language"):
logger.debug("Using cached language from state", language=state["detected_language"])
return state["detected_language"]
# Check context cache
if state.get("context", {}).get("language"):
logger.debug("Using cached language from context", language=state["context"]["language"])
return state["context"]["language"]
# Detect from text
locale, confidence = detect_language(text)
if confidence < MIN_CONFIDENCE and confidence > 0:
logger.warning(
"Low detection confidence, using default",
locale=locale,
confidence=confidence
)
return locale
def is_supported_locale(locale: str) -> bool:
"""Check if locale is supported
Args:
locale: Locale code to check
Returns:
True if locale is in supported list
"""
return locale in SUPPORTED_LOCALES

View File

@@ -65,6 +65,7 @@ class AgentState(TypedDict):
conversation_id: str # Chatwoot conversation ID
user_id: str # User identifier
account_id: str # B2B account identifier
user_token: Optional[str] # User JWT token for API calls
# ============ Message Content ============
messages: list[dict[str, Any]] # Conversation history [{role, content}]
@@ -74,6 +75,10 @@ class AgentState(TypedDict):
intent: Optional[str] # Recognized intent (Intent enum value)
intent_confidence: float # Intent confidence score (0-1)
sub_intent: Optional[str] # Sub-intent for more specific routing
# ============ Language Detection ============
detected_language: Optional[str] # Detected user language (en, nl, de, etc.)
language_confidence: float # Language detection confidence (0-1)
# ============ Entity Extraction ============
entities: dict[str, Any] # Extracted entities {type: value}
@@ -111,10 +116,11 @@ def create_initial_state(
account_id: str,
current_message: str,
messages: Optional[list[dict[str, Any]]] = None,
context: Optional[dict[str, Any]] = None
context: Optional[dict[str, Any]] = None,
user_token: Optional[str] = None
) -> AgentState:
"""Create initial agent state for a new message
Args:
conversation_id: Chatwoot conversation ID
user_id: User identifier
@@ -122,7 +128,8 @@ def create_initial_state(
current_message: User's message to process
messages: Previous conversation history
context: Existing conversation context
user_token: User JWT token for API calls
Returns:
Initialized AgentState
"""
@@ -131,6 +138,7 @@ def create_initial_state(
conversation_id=conversation_id,
user_id=user_id,
account_id=account_id,
user_token=user_token,
# Messages
messages=messages or [],
@@ -140,6 +148,10 @@ def create_initial_state(
intent=None,
intent_confidence=0.0,
sub_intent=None,
# Language
detected_language=None,
language_confidence=0.0,
# Entities
entities={},
@@ -270,3 +282,21 @@ def mark_finished(state: AgentState) -> AgentState:
state["finished"] = True
state["state"] = ConversationState.COMPLETED.value
return state
def set_language(state: AgentState, language: str, confidence: float) -> AgentState:
"""Set the detected language in state
Args:
state: Agent state
language: Detected locale code (en, nl, de, etc.)
confidence: Detection confidence (0-1)
Returns:
Updated state
"""
state["detected_language"] = language
state["language_confidence"] = confidence
# Also cache in context for future reference
state["context"]["language"] = language
return state