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

195
agent/core/llm.py Normal file
View File

@@ -0,0 +1,195 @@
"""
ZhipuAI LLM Client for B2B Shopping AI Assistant
"""
from typing import Any, AsyncGenerator, Optional
from dataclasses import dataclass
from zhipuai import ZhipuAI
from config import settings
from utils.logger import get_logger
logger = get_logger(__name__)
@dataclass
class Message:
"""Chat message structure"""
role: str # "system", "user", "assistant"
content: str
@dataclass
class LLMResponse:
"""LLM response structure"""
content: str
finish_reason: str
usage: dict[str, int]
class ZhipuLLMClient:
"""ZhipuAI LLM Client wrapper"""
def __init__(
self,
api_key: Optional[str] = None,
model: Optional[str] = None
):
"""Initialize ZhipuAI client
Args:
api_key: ZhipuAI API key, defaults to settings
model: Model name, defaults to settings
"""
self.api_key = api_key or settings.zhipu_api_key
self.model = model or settings.zhipu_model
self._client = ZhipuAI(api_key=self.api_key)
logger.info("ZhipuAI client initialized", model=self.model)
async def chat(
self,
messages: list[Message],
temperature: float = 0.7,
max_tokens: int = 2048,
top_p: float = 0.9,
**kwargs: Any
) -> LLMResponse:
"""Send chat completion request
Args:
messages: List of chat messages
temperature: Sampling temperature
max_tokens: Maximum tokens to generate
top_p: Top-p sampling parameter
**kwargs: Additional parameters
Returns:
LLM response with content and metadata
"""
formatted_messages = [
{"role": msg.role, "content": msg.content}
for msg in messages
]
logger.debug(
"Sending chat request",
model=self.model,
message_count=len(messages),
temperature=temperature
)
try:
response = self._client.chat.completions.create(
model=self.model,
messages=formatted_messages,
temperature=temperature,
max_tokens=max_tokens,
top_p=top_p,
**kwargs
)
choice = response.choices[0]
result = LLMResponse(
content=choice.message.content,
finish_reason=choice.finish_reason,
usage={
"prompt_tokens": response.usage.prompt_tokens,
"completion_tokens": response.usage.completion_tokens,
"total_tokens": response.usage.total_tokens
}
)
logger.debug(
"Chat response received",
finish_reason=result.finish_reason,
total_tokens=result.usage["total_tokens"]
)
return result
except Exception as e:
logger.error("Chat request failed", error=str(e))
raise
async def chat_with_tools(
self,
messages: list[Message],
tools: list[dict[str, Any]],
temperature: float = 0.7,
**kwargs: Any
) -> tuple[LLMResponse, Optional[list[dict[str, Any]]]]:
"""Send chat completion request with tool calling
Args:
messages: List of chat messages
tools: List of tool definitions
temperature: Sampling temperature
**kwargs: Additional parameters
Returns:
Tuple of (LLM response, tool calls if any)
"""
formatted_messages = [
{"role": msg.role, "content": msg.content}
for msg in messages
]
logger.debug(
"Sending chat request with tools",
model=self.model,
tool_count=len(tools)
)
try:
response = self._client.chat.completions.create(
model=self.model,
messages=formatted_messages,
tools=tools,
temperature=temperature,
**kwargs
)
choice = response.choices[0]
result = LLMResponse(
content=choice.message.content or "",
finish_reason=choice.finish_reason,
usage={
"prompt_tokens": response.usage.prompt_tokens,
"completion_tokens": response.usage.completion_tokens,
"total_tokens": response.usage.total_tokens
}
)
# Extract tool calls if present
tool_calls = None
if hasattr(choice.message, 'tool_calls') and choice.message.tool_calls:
tool_calls = [
{
"id": tc.id,
"type": tc.type,
"function": {
"name": tc.function.name,
"arguments": tc.function.arguments
}
}
for tc in choice.message.tool_calls
]
logger.debug("Tool calls received", tool_count=len(tool_calls))
return result, tool_calls
except Exception as e:
logger.error("Chat with tools request failed", error=str(e))
raise
# Global LLM client instance
llm_client: Optional[ZhipuLLMClient] = None
def get_llm_client() -> ZhipuLLMClient:
"""Get or create global LLM client instance"""
global llm_client
if llm_client is None:
llm_client = ZhipuLLMClient()
return llm_client