feat: 添加 AI Agent 对话测试工具 + 代码优化
主要变更: - 新增 tests/test_ai_chat.py: AI Agent 对话测试工具 - 优化 core/pydantic_ai_agent.py 和 db/chat_log_db.py - 清理归档文件,更新文档 Made-with: Cursor
This commit is contained in:
@@ -19,6 +19,8 @@ from dotenv import load_dotenv
|
||||
load_dotenv()
|
||||
|
||||
from services.service_tuhui_upload import upload_to_tuhui
|
||||
from core.workflow_router import get_workflow_router
|
||||
from core.workflow_router import get_workflow_router
|
||||
|
||||
# ========== 企业微信通知 ==========
|
||||
_WECHAT_WEBHOOK = "https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=cc88bdef-a13f-4d7e-bdb6-ee51b68b8205"
|
||||
@@ -200,9 +202,9 @@ class CustomerServiceAgent:
|
||||
system_prompt=self._get_similar_prompt()
|
||||
)
|
||||
# 工作流程路由器
|
||||
self.workflow_router = get_workflow_router()
|
||||
self.workflow_router = get_workflow_router()
|
||||
|
||||
self.agent_order = Agent(
|
||||
self.agent_order = Agent(
|
||||
model=model,
|
||||
deps_type=AgentDeps,
|
||||
system_prompt=self._get_order_prompt()
|
||||
@@ -260,7 +262,6 @@ self.agent_order = Agent(
|
||||
# 在 workflow 里创建待处理任务(付款后自动触发 Gemini)
|
||||
try:
|
||||
from core.workflow import workflow
|
||||
from core.workflow_router import get_workflow_router
|
||||
await workflow.image_analysis_result(
|
||||
customer_id=ctx.deps.from_id,
|
||||
image_url=image_url,
|
||||
@@ -455,21 +456,20 @@ from core.workflow_router import get_workflow_router
|
||||
|
||||
@self.agent.tool
|
||||
async def process_image_gemini(ctx: RunContext[AgentDeps], customer_id: str = "") -> str:
|
||||
"""
|
||||
触发 Gemini 作图处理。客户付款后或说「安排一下」「处理一下」时调用。
|
||||
会从客户档案读取上次发图的 URL 和处理参数(提示词、比例、透视),启动 Gemini 流程。
|
||||
处理完成后会自动发图给客户。
|
||||
"""
|
||||
try:
|
||||
from config.config import IMAGE_MODULE_ENABLED
|
||||
if not IMAGE_MODULE_ENABLED:
|
||||
return "现在处理模块暂时暂停,先不自动作图"
|
||||
except Exception:
|
||||
return "现在处理模块暂时暂停,先不自动作图"
|
||||
"""
|
||||
触发 Gemini 作图处理。客户付款后或说「安排一下」「处理一下」时调用。
|
||||
会从客户档案读取上次发图的 URL 和处理参数(提示词、比例、透视),启动 Gemini 流程。
|
||||
处理完成后会自动发图给客户。
|
||||
"""
|
||||
cid = customer_id or ctx.deps.from_id
|
||||
try:
|
||||
from core.workflow import workflow
|
||||
from core.workflow_router import get_workflow_router
|
||||
ok = await workflow.trigger_processing_on_payment(
|
||||
customer_id=cid,
|
||||
acc_id=ctx.deps.acc_id,
|
||||
@@ -518,26 +518,8 @@ from core.workflow_router import get_workflow_router
|
||||
|
||||
@self.agent_processing.tool
|
||||
async def process_image_gemini_run(ctx: RunContext[AgentDeps], customer_id: str = "") -> str:
|
||||
try:
|
||||
from config.config import IMAGE_MODULE_ENABLED
|
||||
if not IMAGE_MODULE_ENABLED:
|
||||
return "现在处理模块暂时暂停"
|
||||
except Exception:
|
||||
return "现在处理模块暂时暂停"
|
||||
cid = customer_id or ctx.deps.from_id
|
||||
try:
|
||||
from core.workflow import workflow
|
||||
from core.workflow_router import get_workflow_router
|
||||
ok = await workflow.trigger_processing_on_payment(
|
||||
customer_id=cid,
|
||||
acc_id=ctx.deps.acc_id,
|
||||
acc_type=ctx.deps.platform,
|
||||
)
|
||||
if ok:
|
||||
return "已安排"
|
||||
return "暂无待处理图片"
|
||||
except Exception as e:
|
||||
return f"触发作图失败: {e}"
|
||||
"""触发 Gemini 作图处理(processing agent 专用入口)。"""
|
||||
return await process_image_gemini(ctx, customer_id)
|
||||
|
||||
@self.agent_similar.tool
|
||||
async def recommend_similar(ctx: RunContext[AgentDeps], hint: str = "") -> str:
|
||||
@@ -986,7 +968,7 @@ from core.workflow_router import get_workflow_router
|
||||
- 输出不超过1句话"""
|
||||
|
||||
def _get_customer_profile_context(self, customer_id: str) -> str:
|
||||
"""从数据库读取客户画像,注入给 AI。含个性化语气、报价策略、主动预测。"""
|
||||
"""从数据库读取客户画像,注入给 AI。含个性化语气、报价策略、主动预测、近期对话。"""
|
||||
try:
|
||||
from db.customer_db import db
|
||||
profile = db.get_customer(customer_id)
|
||||
@@ -994,41 +976,95 @@ from core.workflow_router import get_workflow_router
|
||||
if profile.blacklist:
|
||||
return f"【⚠️黑名单客户】原因:{profile.blacklist_reason or '已标记'},请转接人工处理,不要自动回复"
|
||||
|
||||
parts = []
|
||||
lines = []
|
||||
lines.append("=== 客户档案 ===")
|
||||
|
||||
# 基础信息
|
||||
basic_info = []
|
||||
basic_info.append(f"客户ID: {customer_id}")
|
||||
basic_info.append(f"姓名: {profile.name or '未知'}")
|
||||
if profile.email:
|
||||
parts.append(f"邮箱:{profile.email}")
|
||||
basic_info.append(f"邮箱: {profile.email}")
|
||||
if profile.phone:
|
||||
parts.append(f"电话:{profile.phone}")
|
||||
basic_info.append(f"电话: {profile.phone}")
|
||||
if profile.wechat:
|
||||
parts.append(f"微信:{profile.wechat}")
|
||||
if profile.personality:
|
||||
parts.append(f"性格:{'/'.join(profile.personality)}")
|
||||
basic_info.append(f"微信: {profile.wechat}")
|
||||
lines.append(" | ".join(basic_info))
|
||||
|
||||
# 消费分析
|
||||
consume_info = []
|
||||
consume_info.append(f"客户等级: {profile.customer_level}级")
|
||||
if profile.vip:
|
||||
consume_info.append("VIP客户")
|
||||
consume_info.append(f"总订单: {profile.total_orders}单")
|
||||
consume_info.append(f"总消费: {profile.total_spent}元")
|
||||
if profile.total_orders > 0:
|
||||
parts.append(f"历史下单:{profile.total_orders}次")
|
||||
consume_info.append(f"客单价: {profile.total_spent // profile.total_orders}元")
|
||||
lines.append("--- 消费分析 ---")
|
||||
lines.append(" | ".join(consume_info))
|
||||
|
||||
# 报价历史
|
||||
price_info = []
|
||||
if profile.vip_custom_price:
|
||||
parts.append(f"VIP专属价:{profile.vip_custom_price}元(直接用这个价报)")
|
||||
elif profile.last_price:
|
||||
parts.append(f"上次报价:{profile.last_price}元")
|
||||
price_info.append(f"VIP专属价: {profile.vip_custom_price}元(直接报这个价)")
|
||||
if profile.last_price:
|
||||
price_info.append(f"上次报价: {profile.last_price}元")
|
||||
if profile.lowest_price_accepted:
|
||||
parts.append(f"最低接受价:{profile.lowest_price_accepted}元")
|
||||
price_info.append(f"历史最低成交: {profile.lowest_price_accepted}元")
|
||||
if profile.discount_given_count:
|
||||
parts.append(f"历史让价:{profile.discount_given_count}次")
|
||||
if getattr(profile, "last_quote_no_convert", False):
|
||||
parts.append("【报价策略】上次报价未成交,本次可适当降低5-10元促成交")
|
||||
price_info.append(f"历史让价: {profile.discount_given_count}次")
|
||||
if profile.price_sensitivity:
|
||||
parts.append(f"价格敏感:{profile.price_sensitivity}")
|
||||
price_info.append(f"价格敏感度: {profile.price_sensitivity}")
|
||||
if getattr(profile, "last_quote_no_convert", False):
|
||||
price_info.append("【策略】上次报价未成交,本次可降5-10元")
|
||||
if price_info:
|
||||
lines.append("--- 报价历史 ---")
|
||||
lines.append(" | ".join(price_info))
|
||||
|
||||
# 性格与决策
|
||||
personality_info = []
|
||||
if profile.personality:
|
||||
personality_info.append(f"性格: {'/'.join(profile.personality)}")
|
||||
if profile.decision_speed:
|
||||
parts.append(f"决策速度:{profile.decision_speed}")
|
||||
if profile.last_image_url:
|
||||
parts.append(f"上次发图:{profile.last_image_url}")
|
||||
if profile.processing_status:
|
||||
parts.append(f"当前任务:{profile.processing_status}")
|
||||
personality_info.append(f"决策速度: {profile.decision_speed}")
|
||||
if profile.communication_prefer:
|
||||
personality_info.append(f"沟通偏好: {profile.communication_prefer}")
|
||||
if personality_info:
|
||||
lines.append("--- 性格特征 ---")
|
||||
lines.append(" | ".join(personality_info))
|
||||
|
||||
# 图片习惯
|
||||
image_info = []
|
||||
image_info.append(f"累计发图: {profile.total_images_sent}张")
|
||||
if profile.complexity_history:
|
||||
avg_complexity = self._calc_avg_complexity(profile.complexity_history)
|
||||
image_info.append(f"平均复杂度: {avg_complexity}")
|
||||
if profile.image_type_history:
|
||||
from collections import Counter
|
||||
top_types = Counter(profile.image_type_history).most_common(3)
|
||||
types_str = "、".join(f"{t}({c}次)" for t, c in top_types)
|
||||
image_info.append(f"常见类型: {types_str}")
|
||||
if profile.preferred_format:
|
||||
parts.append(f"格式偏好:{profile.preferred_format}")
|
||||
if profile.bulk_potential == "有":
|
||||
parts.append("批量潜力:有(可主动推打包价)")
|
||||
if profile.upsell_opportunity:
|
||||
parts.append(f"加购机会:{'/'.join(profile.upsell_opportunity)}")
|
||||
image_info.append(f"格式偏好: {profile.preferred_format}")
|
||||
if profile.preferred_size:
|
||||
image_info.append(f"尺寸要求: {profile.preferred_size}")
|
||||
if profile.last_image_url:
|
||||
image_info.append(f"最近发图: {profile.last_image_url[:60]}...")
|
||||
lines.append("--- 图片习惯 ---")
|
||||
lines.append(" | ".join(image_info))
|
||||
|
||||
# 当前任务状态
|
||||
if profile.processing_status:
|
||||
task_info = []
|
||||
task_info.append(f"状态: {profile.processing_status}")
|
||||
if profile.processing_image_url:
|
||||
task_info.append(f"处理中: {profile.processing_image_url[:40]}...")
|
||||
if profile.expected_done_at:
|
||||
task_info.append(f"预计完成: {profile.expected_done_at}")
|
||||
lines.append("--- 当前任务 ---")
|
||||
lines.append(" | ".join(task_info))
|
||||
|
||||
# 上次对话摘要
|
||||
if profile.last_conversation_summary:
|
||||
time_str = ""
|
||||
if profile.last_conversation_time:
|
||||
@@ -1042,38 +1078,54 @@ from core.workflow_router import get_workflow_router
|
||||
time_str = f"({h}小时前)" if h > 0 else "(刚刚)"
|
||||
except Exception:
|
||||
pass
|
||||
parts.append(f"上次对话{time_str}:{profile.last_conversation_summary}")
|
||||
|
||||
# 个性化:语气与报价策略
|
||||
lines.append(f"--- 上次对话 {time_str} ---")
|
||||
lines.append(profile.last_conversation_summary)
|
||||
|
||||
# 个性化回复策略
|
||||
hints = []
|
||||
if profile.personality:
|
||||
if "爽快" in profile.personality:
|
||||
hints.append("回复简洁直接,少废话")
|
||||
hints.append("回复简洁直接,不废话,快速报价")
|
||||
if "砍价" in profile.personality or "砍价狂" in profile.personality:
|
||||
hints.append("报价时强调性价比,只让价一次")
|
||||
hints.append("报价时强调性价比,只让价一次,第二次引导去 xinhui.cloud")
|
||||
if "纠结" in profile.personality or "墨迹" in profile.personality:
|
||||
hints.append("耐心一点,可多给一点说明")
|
||||
hints.append("多给一点说明,耐心回答")
|
||||
if profile.price_sensitivity == "高":
|
||||
hints.append("报价时顺带提「满意再拍」降低顾虑")
|
||||
if profile.decision_speed == "快":
|
||||
hints.append("重点推快速成交,少铺垫")
|
||||
hints.append("直接报价推成交,少铺垫")
|
||||
if profile.total_orders > 0 and profile.decision_speed == "快":
|
||||
hints.append("老客爽快,直接报价成交")
|
||||
if hints:
|
||||
parts.append(f"【回复风格】{'; '.join(hints)}")
|
||||
|
||||
# 主动预测:批量/加急
|
||||
lines.append("--- 回复策略 ---")
|
||||
lines.append(";".join(hints))
|
||||
|
||||
# 主动推荐
|
||||
proactive = []
|
||||
if profile.bulk_potential == "有" or (profile.total_images_sent or 0) >= 2:
|
||||
proactive.append("可主动问「要做多张吗,多张有优惠」")
|
||||
if profile.total_orders > 0 and profile.decision_speed == "快":
|
||||
proactive.append("老客爽快,直接推成交")
|
||||
proactive.append("可问「要做多张吗,多张有优惠」")
|
||||
if profile.upsell_opportunity:
|
||||
proactive.append(f"加购机会: {'、'.join(profile.upsell_opportunity)}")
|
||||
if proactive:
|
||||
parts.append(f"【主动推荐】{'; '.join(proactive)}")
|
||||
|
||||
if parts:
|
||||
return "【该客户历史信息】" + " | ".join(parts)
|
||||
lines.append("--- 主动推荐 ---")
|
||||
lines.append(";".join(proactive))
|
||||
|
||||
return "\n".join(lines)
|
||||
except Exception as e:
|
||||
print(f"[Agent] 获取客户画像失败: {e}")
|
||||
return ""
|
||||
|
||||
def _calc_avg_complexity(self, complexity_history: list) -> str:
|
||||
"""计算平均复杂度"""
|
||||
if not complexity_history:
|
||||
return "未知"
|
||||
level_map = {"simple": 1, "normal": 2, "complex": 3, "hard": 4}
|
||||
label_map = {1: "简单", 2: "一般", 3: "复杂", 4: "很复杂"}
|
||||
try:
|
||||
avg = sum(level_map.get(c, 2) for c in complexity_history) / len(complexity_history)
|
||||
return label_map.get(round(avg), "一般")
|
||||
except Exception:
|
||||
pass
|
||||
return ""
|
||||
return "一般"
|
||||
|
||||
def _get_refusal_context_hint(self, customer_id: str, current_msg: str, profile_context: str) -> str:
|
||||
"""
|
||||
@@ -1203,7 +1255,6 @@ from core.workflow_router import get_workflow_router
|
||||
# 已付款:触发 Gemini 作图
|
||||
try:
|
||||
from core.workflow import workflow
|
||||
from core.workflow_router import get_workflow_router
|
||||
asyncio.create_task(workflow.trigger_processing_on_payment(
|
||||
customer_id=message.from_id,
|
||||
acc_id=message.acc_id,
|
||||
@@ -1816,6 +1867,51 @@ from core.workflow_router import get_workflow_router
|
||||
|
||||
return prompt
|
||||
|
||||
async def _handle_image_workflow(self, message: str, data: dict, image_urls: list) -> bool:
|
||||
"""处理图片工作流(根据客户说的话判断执行哪种工作流)"""
|
||||
if not image_urls:
|
||||
return False
|
||||
|
||||
workflow_type, confidence = self.workflow_router.detect_workflow(message)
|
||||
|
||||
customer_id = data.get('from_id')
|
||||
acc_id = data.get('acc_id', '')
|
||||
acc_type = data.get('acc_type', 'AliWorkbench')
|
||||
image_url = image_urls[0]
|
||||
|
||||
print(f"[Agent] 检测到工作流类型:{workflow_type} (置信度:{confidence})")
|
||||
|
||||
if workflow_type == "find_image":
|
||||
print(f"[Agent] 执行查找图片工作流 | 客户:{customer_id}")
|
||||
from core.workflow import workflow
|
||||
return await workflow.find_image_workflow(
|
||||
customer_id=customer_id,
|
||||
image_url=image_url,
|
||||
acc_id=acc_id,
|
||||
acc_type=acc_type
|
||||
)
|
||||
elif workflow_type == "process_image":
|
||||
print(f"[Agent] 执行处理图片工作流 | 客户:{customer_id}")
|
||||
from core.workflow import workflow
|
||||
return await workflow.process_image_workflow(
|
||||
customer_id=customer_id,
|
||||
image_url=image_url,
|
||||
acc_id=acc_id,
|
||||
acc_type=acc_type
|
||||
)
|
||||
elif workflow_type == "transfer_human":
|
||||
print(f"[Agent] 执行转人工派单工作流 | 客户:{customer_id}")
|
||||
from core.workflow import workflow
|
||||
return await workflow.transfer_to_designer_workflow(
|
||||
customer_id=customer_id,
|
||||
image_url=image_url,
|
||||
acc_id=acc_id,
|
||||
acc_type=acc_type,
|
||||
reason="客户主动要求转人工"
|
||||
)
|
||||
|
||||
return False
|
||||
|
||||
|
||||
async def test_agent():
|
||||
"""测试 Agent"""
|
||||
@@ -1842,65 +1938,3 @@ async def test_agent():
|
||||
if __name__ == "__main__":
|
||||
import asyncio
|
||||
asyncio.run(test_agent())
|
||||
|
||||
async def _handle_image_workflow(self, message: str, data: dict, image_urls: list) -> bool:
|
||||
"""
|
||||
处理图片工作流(根据客户说的话判断执行哪种工作流)
|
||||
|
||||
Args:
|
||||
message: 客户消息
|
||||
data: 消息数据
|
||||
image_urls: 图片 URL 列表
|
||||
|
||||
Returns:
|
||||
bool: 是否已处理
|
||||
"""
|
||||
if not image_urls:
|
||||
return False
|
||||
|
||||
# 检测工作流类型
|
||||
workflow_type, confidence = self.workflow_router.detect_workflow(message)
|
||||
|
||||
customer_id = data.get('from_id')
|
||||
acc_id = data.get('acc_id', '')
|
||||
acc_type = data.get('acc_type', 'AliWorkbench')
|
||||
image_url = image_urls[0] # 取第一张图
|
||||
|
||||
print(f"[Agent] 检测到工作流类型:{workflow_type} (置信度:{confidence})")
|
||||
|
||||
if workflow_type == "find_image":
|
||||
# 工作流 1:查找图片
|
||||
print(f"[Agent] 执行查找图片工作流 | 客户:{customer_id}")
|
||||
success = await workflow.find_image_workflow(
|
||||
customer_id=customer_id,
|
||||
image_url=image_url,
|
||||
acc_id=acc_id,
|
||||
acc_type=acc_type
|
||||
)
|
||||
return success
|
||||
|
||||
elif workflow_type == "process_image":
|
||||
# 工作流 2:处理图片
|
||||
print(f"[Agent] 执行处理图片工作流 | 客户:{customer_id}")
|
||||
success = await workflow.process_image_workflow(
|
||||
customer_id=customer_id,
|
||||
image_url=image_url,
|
||||
acc_id=acc_id,
|
||||
acc_type=acc_type
|
||||
)
|
||||
return success
|
||||
|
||||
elif workflow_type == "transfer_human":
|
||||
# 工作流 3:转人工派单
|
||||
print(f"[Agent] 执行转人工派单工作流 | 客户:{customer_id}")
|
||||
success = await workflow.transfer_to_designer_workflow(
|
||||
customer_id=customer_id,
|
||||
image_url=image_url,
|
||||
acc_id=acc_id,
|
||||
acc_type=acc_type,
|
||||
reason="客户主动要求转人工"
|
||||
)
|
||||
return success
|
||||
|
||||
# 未知工作流,不处理
|
||||
return False
|
||||
|
||||
Reference in New Issue
Block a user