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:
@@ -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
|
||||
|
||||
150
agent/core/language_detector.py
Normal file
150
agent/core/language_detector.py
Normal 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
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user