feat: 初始化 B2B AI Shopping Assistant 项目

- 配置 Docker Compose 多服务编排
- 实现 Chatwoot + Agent 集成
- 配置 Strapi MCP 知识库
- 支持 7 种语言的 FAQ 系统
- 实现 LangGraph AI 工作流

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
wl
2026-01-14 19:25:22 +08:00
commit 3ad6eee0d9
59 changed files with 8078 additions and 0 deletions

404
agent/core/graph.py Normal file
View File

@@ -0,0 +1,404 @@
"""
LangGraph workflow definition for B2B Shopping AI Assistant
"""
from typing import Literal
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
logger = get_logger(__name__)
# ============ Node Functions ============
async def receive_message(state: AgentState) -> AgentState:
"""Receive and preprocess incoming message
This is the entry point of the workflow.
"""
logger.info(
"Receiving message",
conversation_id=state["conversation_id"],
message_length=len(state["current_message"])
)
# Add user message to history
state["messages"].append({
"role": "user",
"content": state["current_message"]
})
state["state"] = ConversationState.INITIAL.value
return state
async def call_mcp_tools(state: AgentState) -> AgentState:
"""Execute pending MCP tool calls
Calls the appropriate MCP server based on the tool_calls in state.
"""
if not state["tool_calls"]:
logger.debug("No tool calls to execute")
return state
logger.info(
"Executing MCP tools",
tool_count=len(state["tool_calls"])
)
# MCP server URL mapping
mcp_servers = {
"strapi": settings.strapi_mcp_url,
"order": settings.order_mcp_url,
"aftersale": settings.aftersale_mcp_url,
"product": settings.product_mcp_url
}
async with httpx.AsyncClient(timeout=30.0) as client:
for tool_call in state["tool_calls"]:
server = tool_call["server"]
tool_name = tool_call["tool_name"]
arguments = tool_call["arguments"]
server_url = mcp_servers.get(server)
if not server_url:
state = add_tool_result(
state,
tool_name=tool_name,
success=False,
data=None,
error=f"Unknown MCP server: {server}"
)
continue
try:
# Call MCP tool endpoint
response = await client.post(
f"{server_url}/tools/{tool_name}",
json=arguments
)
response.raise_for_status()
result = response.json()
state = add_tool_result(
state,
tool_name=tool_name,
success=True,
data=result
)
logger.debug(
"Tool executed successfully",
tool=tool_name,
server=server
)
except httpx.HTTPStatusError as e:
logger.error(
"Tool HTTP error",
tool=tool_name,
status=e.response.status_code
)
state = add_tool_result(
state,
tool_name=tool_name,
success=False,
data=None,
error=f"HTTP {e.response.status_code}"
)
except Exception as e:
logger.error("Tool execution failed", tool=tool_name, error=str(e))
state = add_tool_result(
state,
tool_name=tool_name,
success=False,
data=None,
error=str(e)
)
# Clear pending tool calls
state["tool_calls"] = []
return state
async def human_handoff(state: AgentState) -> AgentState:
"""Handle transfer to human agent
Sets up the state for human intervention.
"""
logger.info(
"Human handoff requested",
conversation_id=state["conversation_id"],
reason=state.get("handoff_reason")
)
state["state"] = ConversationState.HUMAN_REVIEW.value
# Generate handoff message
reason = state.get("handoff_reason", "您的问题需要人工客服协助")
state = set_response(
state,
f"正在为您转接人工客服,请稍候。\n转接原因:{reason}\n\n人工客服将尽快为您服务。"
)
return state
async def send_response(state: AgentState) -> AgentState:
"""Finalize and send response
This is the final node that marks processing as complete.
"""
logger.info(
"Sending response",
conversation_id=state["conversation_id"],
response_length=len(state.get("response", ""))
)
# Add assistant response to history
if state.get("response"):
state["messages"].append({
"role": "assistant",
"content": state["response"]
})
state = mark_finished(state)
return state
async def handle_error(state: AgentState) -> AgentState:
"""Handle errors in the workflow"""
logger.error(
"Workflow error",
conversation_id=state["conversation_id"],
error=state.get("error")
)
state = set_response(
state,
"抱歉,处理您的请求时遇到了问题。请稍后重试,或联系人工客服获取帮助。"
)
state = mark_finished(state)
return state
# ============ Routing Functions ============
def should_call_tools(state: AgentState) -> Literal["call_tools", "send_response", "back_to_agent"]:
"""Determine if tools need to be called"""
# If there are pending tool calls, execute them
if state.get("tool_calls"):
return "call_tools"
# If we have a response ready, send it
if state.get("response"):
return "send_response"
# If we're waiting for info, send the question
if state.get("state") == ConversationState.AWAITING_INFO.value:
return "send_response"
# Otherwise, something went wrong
return "send_response"
def after_tools(state: AgentState) -> str:
"""Route after tool execution
Returns the agent that should process the tool results.
"""
current_agent = state.get("current_agent")
# Route back to the agent that made the tool call
agent_mapping = {
"customer_service": "customer_service_agent",
"order": "order_agent",
"aftersale": "aftersale_agent",
"product": "product_agent"
}
return agent_mapping.get(current_agent, "customer_service_agent")
def check_completion(state: AgentState) -> Literal["continue", "end", "error"]:
"""Check if workflow should continue or end"""
# Check for errors
if state.get("error"):
return "error"
# Check if finished
if state.get("finished"):
return "end"
# Check step limit
if state.get("step_count", 0) >= state.get("max_steps", 10):
logger.warning("Max steps reached", conversation_id=state["conversation_id"])
return "end"
return "continue"
# ============ Graph Construction ============
def create_agent_graph() -> StateGraph:
"""Create the main agent workflow graph
Returns:
Compiled LangGraph workflow
"""
# Create graph with AgentState
graph = StateGraph(AgentState)
# Add nodes
graph.add_node("receive", receive_message)
graph.add_node("classify", classify_intent)
graph.add_node("customer_service_agent", customer_service_agent)
graph.add_node("order_agent", order_agent)
graph.add_node("aftersale_agent", aftersale_agent)
graph.add_node("product_agent", product_agent)
graph.add_node("call_tools", call_mcp_tools)
graph.add_node("human_handoff", human_handoff)
graph.add_node("send_response", send_response)
graph.add_node("handle_error", handle_error)
# Set entry point
graph.set_entry_point("receive")
# Add edges
graph.add_edge("receive", "classify")
# Conditional routing based on intent
graph.add_conditional_edges(
"classify",
route_by_intent,
{
"customer_service_agent": "customer_service_agent",
"order_agent": "order_agent",
"aftersale_agent": "aftersale_agent",
"product_agent": "product_agent",
"human_handoff": "human_handoff"
}
)
# After each agent, check if tools need to be called
for agent_node in ["customer_service_agent", "order_agent", "aftersale_agent", "product_agent"]:
graph.add_conditional_edges(
agent_node,
should_call_tools,
{
"call_tools": "call_tools",
"send_response": "send_response",
"back_to_agent": agent_node
}
)
# After tool execution, route back to appropriate agent
graph.add_conditional_edges(
"call_tools",
after_tools,
{
"customer_service_agent": "customer_service_agent",
"order_agent": "order_agent",
"aftersale_agent": "aftersale_agent",
"product_agent": "product_agent"
}
)
# Human handoff leads to send response
graph.add_edge("human_handoff", "send_response")
# Error handling
graph.add_edge("handle_error", END)
# Final node
graph.add_edge("send_response", END)
return graph.compile()
# Global compiled graph
_compiled_graph = None
def get_agent_graph():
"""Get or create the compiled agent graph"""
global _compiled_graph
if _compiled_graph is None:
_compiled_graph = create_agent_graph()
return _compiled_graph
async def process_message(
conversation_id: str,
user_id: str,
account_id: str,
message: str,
history: list[dict] = None,
context: dict = None
) -> AgentState:
"""Process a user message through the agent workflow
Args:
conversation_id: Chatwoot conversation ID
user_id: User identifier
account_id: B2B account identifier
message: User's message
history: Previous conversation history
context: Existing conversation context
Returns:
Final agent state with response
"""
from .state import create_initial_state
# Create initial state
initial_state = create_initial_state(
conversation_id=conversation_id,
user_id=user_id,
account_id=account_id,
current_message=message,
messages=history,
context=context
)
# Get compiled graph
graph = get_agent_graph()
# Run the workflow
logger.info(
"Starting workflow",
conversation_id=conversation_id,
message=message[:100]
)
try:
final_state = await graph.ainvoke(initial_state)
logger.info(
"Workflow completed",
conversation_id=conversation_id,
intent=final_state.get("intent"),
steps=final_state.get("step_count")
)
return final_state
except Exception as e:
logger.error("Workflow failed", error=str(e))
initial_state["error"] = str(e)
initial_state["response"] = "抱歉,处理您的请求时遇到了问题。请稍后重试。"
initial_state["finished"] = True
return initial_state