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:
2026-02-28 16:19:35 +08:00
parent a6c42d505a
commit c39840fe15
49 changed files with 2453 additions and 8556 deletions

View File

@@ -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