feat: 添加图片搜索功能和 Qwen 模型支持
图片搜索功能(以图搜图): - Chatwoot webhook 检测图片搜索消息 (content_type="search_image") - 从 content_attributes.url 提取图片 URL - 调用 Mall API 图片搜索接口 (/mall/api/spu?searchImageUrl=...) - 支持嵌套和顶层 URL 位置提取 - Product Agent 添加 fast path 直接调用图片搜索工具 - 防止无限循环(使用后清除 context.image_search_url) Qwen 模型支持: - 添加 LLM provider 选择(zhipu/qwen) - 实现 QwenLLMClient 类(基于 DashScope SDK) - 添加 dashscope>=1.14.0 依赖 - 修复 API key 设置(直接设置 dashscope.api_key) - 更新 .env.example 和 docker-compose.yml 配置 其他优化: - 重构 Chatwoot 集成代码(删除冗余) - 优化 Product Agent prompt - 增强 Customer Service Agent 多语言支持 Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -6,6 +6,7 @@ import hashlib
|
||||
from typing import Any, Optional
|
||||
|
||||
from fastapi import APIRouter, Request, HTTPException, BackgroundTasks
|
||||
from fastapi.responses import JSONResponse
|
||||
from pydantic import BaseModel
|
||||
|
||||
from config import settings
|
||||
@@ -75,6 +76,7 @@ class ChatwootWebhookPayload(BaseModel):
|
||||
message_type: Optional[str] = None
|
||||
content_type: Optional[str] = None
|
||||
private: Optional[bool] = False
|
||||
content_attributes: Optional[dict] = None # 图片搜索 URL 等额外属性
|
||||
conversation: Optional[WebhookConversation] = None
|
||||
sender: Optional[WebhookSender] = None
|
||||
contact: Optional[WebhookContact] = None
|
||||
@@ -132,7 +134,15 @@ async def handle_incoming_message(payload: ChatwootWebhookPayload, cookie_token:
|
||||
conversation_id = str(conversation.id)
|
||||
content = payload.content
|
||||
|
||||
if not content:
|
||||
# 检查是否是图片搜索消息(在过滤空 content 之前)
|
||||
# 图片搜索消息的 content 通常是空的,但需要处理
|
||||
is_image_search = (
|
||||
payload.content_type == "search_image" or
|
||||
payload.content_type == 17
|
||||
)
|
||||
|
||||
# 只有非图片搜索消息才检查 content 是否为空
|
||||
if not content and not is_image_search:
|
||||
logger.debug("Empty message content, skipping")
|
||||
return
|
||||
|
||||
@@ -282,19 +292,7 @@ async def handle_incoming_message(payload: ChatwootWebhookPayload, cookie_token:
|
||||
channel=conversation.channel if conversation else None
|
||||
)
|
||||
|
||||
# 识别消息渠道(邮件、网站等)
|
||||
message_channel = conversation.channel if conversation else "Channel"
|
||||
is_email = message_channel == "Email"
|
||||
|
||||
# 邮件渠道特殊处理
|
||||
if is_email:
|
||||
logger.info(
|
||||
"Email channel detected",
|
||||
conversation_id=conversation_id,
|
||||
sender_email=contact.email if contact else None
|
||||
)
|
||||
|
||||
# Load conversation context from cache
|
||||
# Load conversation context from cache (需要在图片搜索检测之前)
|
||||
cache = get_cache_manager()
|
||||
await cache.connect()
|
||||
|
||||
@@ -307,10 +305,125 @@ async def handle_incoming_message(payload: ChatwootWebhookPayload, cookie_token:
|
||||
if mall_token:
|
||||
context["mall_token"] = mall_token
|
||||
|
||||
# 图片 URL 可能在两个位置:
|
||||
# 1. payload.content_attributes.url (顶层)
|
||||
# 2. payload.conversation.messages[0].content_attributes.url (嵌套)
|
||||
image_url = None
|
||||
|
||||
# 首先尝试从顶层获取
|
||||
if payload.content_attributes:
|
||||
image_url = payload.content_attributes.get("url")
|
||||
|
||||
# 如果顶层没有,尝试从 conversation.messages[0] 获取
|
||||
if not image_url and conversation and hasattr(conversation, 'model_dump'):
|
||||
conv_dict = conversation.model_dump()
|
||||
messages = conv_dict.get('messages', [])
|
||||
if messages and len(messages) > 0:
|
||||
first_message = messages[0]
|
||||
msg_content_attrs = first_message.get('content_attributes', {})
|
||||
image_url = msg_content_attrs.get('url')
|
||||
|
||||
if is_image_search and image_url:
|
||||
logger.info(
|
||||
"Image search detected",
|
||||
conversation_id=conversation_id,
|
||||
image_url=image_url[:100] + "..." if len(image_url) > 100 else image_url
|
||||
)
|
||||
|
||||
# 创建 Chatwoot client
|
||||
from integrations.chatwoot import ChatwootClient
|
||||
chatwoot = ChatwootClient(account_id=int(account_id))
|
||||
|
||||
# 开启 typing status
|
||||
try:
|
||||
await chatwoot.toggle_typing_status(
|
||||
conversation_id=conversation.id,
|
||||
typing_status="on"
|
||||
)
|
||||
except Exception as e:
|
||||
logger.warning(
|
||||
"Failed to enable typing status for image search",
|
||||
conversation_id=conversation_id,
|
||||
error=str(e)
|
||||
)
|
||||
|
||||
# 直接调用图片搜索工具
|
||||
# 修改 content 为图片搜索指令
|
||||
search_content = f"[图片搜索] 请根据这张图片搜索相似的商品"
|
||||
|
||||
# 使用特殊标记让 Product Agent 知道这是图片搜索
|
||||
context["image_search_url"] = image_url
|
||||
|
||||
final_state = await process_message(
|
||||
conversation_id=conversation_id,
|
||||
user_id=user_id,
|
||||
account_id=account_id,
|
||||
message=search_content,
|
||||
history=history,
|
||||
context=context,
|
||||
user_token=user_token,
|
||||
mall_token=mall_token
|
||||
)
|
||||
|
||||
# 获取响应并发送
|
||||
response = final_state.get("response", "")
|
||||
|
||||
if response:
|
||||
await chatwoot.send_message(
|
||||
conversation_id=conversation.id,
|
||||
content=response
|
||||
)
|
||||
|
||||
logger.info(
|
||||
"Image search response sent",
|
||||
conversation_id=conversation_id,
|
||||
response_length=len(response)
|
||||
)
|
||||
|
||||
# 关闭 typing status
|
||||
await chatwoot.toggle_typing_status(
|
||||
conversation_id=conversation.id,
|
||||
typing_status="off"
|
||||
)
|
||||
|
||||
await chatwoot.close()
|
||||
|
||||
# Update cache
|
||||
await cache.add_message(conversation_id, "user", search_content)
|
||||
await cache.add_message(conversation_id, "assistant", response)
|
||||
|
||||
# Save context
|
||||
new_context = final_state.get("context", {})
|
||||
new_context["last_intent"] = final_state.get("intent")
|
||||
await cache.set_context(conversation_id, new_context)
|
||||
|
||||
logger.info(
|
||||
"Image search message processed successfully",
|
||||
conversation_id=conversation_id,
|
||||
intent=final_state.get("intent")
|
||||
)
|
||||
|
||||
return JSONResponse(
|
||||
content={"status": "success"},
|
||||
status_code=200
|
||||
)
|
||||
|
||||
# 识别消息渠道(邮件、网站等)
|
||||
message_channel = conversation.channel if conversation else "Channel"
|
||||
is_email = message_channel == "Email"
|
||||
|
||||
# 添加渠道信息到 context(让 Agent 知道是邮件还是网站)
|
||||
context["channel"] = message_channel
|
||||
context["is_email"] = is_email
|
||||
|
||||
# 邮件渠道特殊处理
|
||||
if is_email:
|
||||
logger.info(
|
||||
"Email channel detected",
|
||||
conversation_id=conversation_id,
|
||||
sender_email=contact.email if contact else None
|
||||
)
|
||||
|
||||
# 创建 Chatwoot client(提前创建以便开启 typing status)
|
||||
from integrations.chatwoot import ChatwootClient
|
||||
chatwoot = ChatwootClient(account_id=int(account_id))
|
||||
|
||||
Reference in New Issue
Block a user