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:
@@ -322,16 +322,51 @@ async def customer_service_agent(state: AgentState) -> AgentState:
|
||||
return state
|
||||
|
||||
except json.JSONDecodeError as e:
|
||||
# JSON parsing failed
|
||||
# JSON parsing failed - try alternative format: "tool_name\n{args}"
|
||||
logger.error(
|
||||
"Failed to parse LLM response as JSON",
|
||||
error=str(e),
|
||||
raw_content=content[:500],
|
||||
conversation_id=state["conversation_id"]
|
||||
)
|
||||
# Don't use raw content as response - use fallback instead
|
||||
state = set_response(state, "抱歉,我无法理解您的请求。请尝试重新表述或联系人工客服。")
|
||||
return state
|
||||
|
||||
# Handle non-JSON format: "tool_name\n{args}"
|
||||
if '\n' in content and not content.startswith('{'):
|
||||
lines = content.split('\n', 1)
|
||||
tool_name = lines[0].strip()
|
||||
args_json = lines[1].strip() if len(lines) > 1 else '{}'
|
||||
|
||||
try:
|
||||
arguments = json.loads(args_json) if args_json else {}
|
||||
logger.info(
|
||||
"Customer service agent calling tool (alternative format)",
|
||||
tool_name=tool_name,
|
||||
arguments=arguments,
|
||||
conversation_id=state["conversation_id"]
|
||||
)
|
||||
|
||||
state = add_tool_call(
|
||||
state,
|
||||
tool_name=tool_name,
|
||||
arguments=arguments,
|
||||
server="strapi"
|
||||
)
|
||||
state["state"] = ConversationState.TOOL_CALLING.value
|
||||
return state
|
||||
except json.JSONDecodeError:
|
||||
# Args parsing also failed
|
||||
logger.warning(
|
||||
"Failed to parse tool arguments",
|
||||
tool_name=tool_name,
|
||||
args_json=args_json[:200],
|
||||
conversation_id=state["conversation_id"]
|
||||
)
|
||||
state = set_response(state, "抱歉,我无法理解您的请求。请尝试重新表述或联系人工客服。")
|
||||
return state
|
||||
else:
|
||||
# Not a recognized format
|
||||
state = set_response(state, "抱歉,我无法理解您的请求。请尝试重新表述或联系人工客服。")
|
||||
return state
|
||||
|
||||
except Exception as e:
|
||||
logger.error("Customer service agent failed", error=str(e), exc_info=True)
|
||||
@@ -342,11 +377,59 @@ async def customer_service_agent(state: AgentState) -> AgentState:
|
||||
async def _generate_response_from_results(state: AgentState) -> AgentState:
|
||||
"""Generate response based on tool results"""
|
||||
|
||||
# Build context from tool results
|
||||
# Build context from tool results - extract only essential info to reduce prompt size
|
||||
tool_context = []
|
||||
for result in state["tool_results"]:
|
||||
if result["success"]:
|
||||
tool_context.append(f"Tool {result['tool_name']} returned:\n{json.dumps(result['data'], ensure_ascii=False, indent=2)}")
|
||||
tool_name = result['tool_name']
|
||||
data = result['data']
|
||||
|
||||
# Extract only essential information based on tool type
|
||||
if tool_name == "get_company_info":
|
||||
# Extract key contact info only
|
||||
contact = data.get('contact', {})
|
||||
emails = contact.get('email', [])
|
||||
if isinstance(emails, list) and emails:
|
||||
email_str = ", ".join(emails[:3]) # Max 3 emails
|
||||
else:
|
||||
email_str = str(emails) if emails else "N/A"
|
||||
|
||||
phones = contact.get('phone', [])
|
||||
if isinstance(phones, list) and phones:
|
||||
phone_str = ", ".join(phones[:2]) # Max 2 phones
|
||||
else:
|
||||
phone_str = str(phones) if phones else "N/A"
|
||||
|
||||
address = contact.get('address', {})
|
||||
address_str = f"{address.get('city', '')}, {address.get('country', '')}".strip(', ')
|
||||
|
||||
summary = f"Contact Information: Emails: {email_str} | Phones: {phone_str} | Address: {address_str} | Working hours: {contact.get('working_hours', 'N/A')}"
|
||||
tool_context.append(summary)
|
||||
|
||||
elif tool_name == "query_faq" or tool_name == "search_knowledge_base":
|
||||
# Extract FAQ items summary
|
||||
faqs = data.get('faqs', []) if isinstance(data, dict) else []
|
||||
if faqs:
|
||||
faq_summaries = [f"- Q: {faq.get('question', '')[:50]}... A: {faq.get('answer', '')[:50]}..." for faq in faqs[:3]]
|
||||
summary = f"Found {len(faqs)} FAQ items:\n" + "\n".join(faq_summaries)
|
||||
tool_context.append(summary)
|
||||
else:
|
||||
tool_context.append("No FAQ items found")
|
||||
|
||||
elif tool_name == "get_categories":
|
||||
# Extract category names only
|
||||
categories = data.get('categories', []) if isinstance(data, dict) else []
|
||||
category_names = [cat.get('name', '') for cat in categories[:5] if cat.get('name')]
|
||||
summary = f"Available categories: {', '.join(category_names)}"
|
||||
if len(categories) > 5:
|
||||
summary += f" (and {len(categories) - 5} more)"
|
||||
tool_context.append(summary)
|
||||
|
||||
else:
|
||||
# For other tools, include concise summary (limit to 200 chars)
|
||||
data_str = json.dumps(data, ensure_ascii=False)[:200]
|
||||
tool_context.append(f"Tool {tool_name} returned: {data_str}...")
|
||||
|
||||
else:
|
||||
tool_context.append(f"Tool {result['tool_name']} failed: {result['error']}")
|
||||
|
||||
@@ -357,7 +440,8 @@ User question: {state["current_message"]}
|
||||
Tool returned information:
|
||||
{chr(10).join(tool_context)}
|
||||
|
||||
Please generate a friendly and professional response. If the tool did not return useful information, honestly inform the user and suggest other ways to get help.
|
||||
Please generate a friendly and professional response in Chinese. Keep it concise but informative.
|
||||
If the tool did not return useful information, honestly inform the user and suggest other ways to get help.
|
||||
Return only the response content, do not return JSON."""
|
||||
|
||||
messages = [
|
||||
@@ -367,11 +451,12 @@ Return only the response content, do not return JSON."""
|
||||
|
||||
try:
|
||||
llm = get_llm_client()
|
||||
response = await llm.chat(messages, temperature=0.7)
|
||||
# Lower temperature for faster response
|
||||
response = await llm.chat(messages, temperature=0.3)
|
||||
state = set_response(state, response.content)
|
||||
return state
|
||||
|
||||
except Exception as e:
|
||||
logger.error("Response generation failed", error=str(e))
|
||||
state = set_response(state, "Sorry, there was a problem processing your request. Please try again later or contact customer support.")
|
||||
state = set_response(state, "抱歉,处理您的请求时出现问题。请稍后重试或联系人工客服。")
|
||||
return state
|
||||
|
||||
Reference in New Issue
Block a user