newtw66
This commit is contained in:
@@ -11,6 +11,13 @@ from db.chat_log_db import get_conversation, get_customer_orders
|
||||
logger = logging.getLogger("cs_agent")
|
||||
|
||||
|
||||
class TransferSuccessException(Exception):
|
||||
"""转接成功后抛出此异常,用于提前终止 AI 处理流程"""
|
||||
def __init__(self, transfer_cmd: str):
|
||||
self.transfer_cmd = transfer_cmd
|
||||
super().__init__(transfer_cmd)
|
||||
|
||||
|
||||
async def transfer_to_human_tool(ctx: RunContext[Any], reason: str = Field(description="转人工的原因")) -> str:
|
||||
"""
|
||||
【核心工具】执行转人工逻辑。
|
||||
@@ -22,8 +29,9 @@ async def transfer_to_human_tool(ctx: RunContext[Any], reason: str = Field(descr
|
||||
|
||||
if designer_name:
|
||||
magic_cmd = f"正在为您转接|[转移会话],{designer_name},无原因"
|
||||
logger.info(f"[Tool] 成功呼叫设计师: {designer_name}")
|
||||
return magic_cmd
|
||||
logger.info(f"[Tool] 成功呼叫设计师: {designer_name},立即触发转接")
|
||||
# 抛出异常以提前终止 AI 后续处理,节省等待时间
|
||||
raise TransferSuccessException(magic_cmd)
|
||||
else:
|
||||
hour = datetime.now().hour
|
||||
logger.warning(f"[Tool] 派单失败:设计师们不在位 (当前{hour}点)")
|
||||
|
||||
@@ -37,6 +37,18 @@ _OUTBOUND_BLOCK_MARKERS = (
|
||||
'[{"name":',
|
||||
)
|
||||
|
||||
# 历史记录格式检测模式(AI 转述历史时容易泄露)
|
||||
_HISTORY_LEAK_PATTERNS = [
|
||||
r'\[\d{4}-\d{2}-\d{2}[^\]]*\]\s*(客户|客服)[::]', # [2026-03-07 12:00:00] 客户:
|
||||
r'\[\d{2}:\d{2}:\d{2}\]\s*(客户|客服|我)[::]', # [12:00:00] 客户:
|
||||
r'(根据|查看|查询|翻看)(历史|聊天|对话)(记录|内容)', # 根据历史记录
|
||||
r'历史(记录|对话|消息)(显示|表明|中)', # 历史记录显示
|
||||
r'之前的(聊天|对话|记录)(中|里|显示)', # 之前的聊天中
|
||||
r'共\d+条(历史|对话)?消息', # 共30条历史消息
|
||||
r'订单号[::]\s*\d{10,}', # 订单号:xxxxxxxxxx
|
||||
r'(状态|金额|数量)[::].*(状态|金额|数量)[::]', # 状态:xxx 金额:xxx 连续出现
|
||||
]
|
||||
|
||||
class SystemOrchestrator:
|
||||
"""
|
||||
全系统总编排:具备转接冷却、防抖合并、多消息去重、以及精准日志。
|
||||
@@ -84,6 +96,11 @@ class SystemOrchestrator:
|
||||
if any(marker in cleaned for marker in _OUTBOUND_BLOCK_MARKERS):
|
||||
logger.warning("[Orchestrator] 拦截到内部内容外发,替换为安全兜底回复")
|
||||
return "我在帮你看记录,稍等哈"
|
||||
# 检查历史记录泄露模式
|
||||
for pattern in _HISTORY_LEAK_PATTERNS:
|
||||
if re.search(pattern, cleaned):
|
||||
logger.warning(f"[Orchestrator] 检测到历史记录泄露模式: {pattern[:30]}...")
|
||||
return "我在帮你看记录,稍等哈"
|
||||
return cleaned
|
||||
|
||||
async def on_raw_message_received(self, platform: str, raw_data: dict):
|
||||
@@ -243,11 +260,17 @@ class SystemOrchestrator:
|
||||
|
||||
async def _debounced_process(self, session_key: str, user_id: str, platform: str):
|
||||
try:
|
||||
# 记录开始时间(防抖前)
|
||||
process_start = time.time()
|
||||
|
||||
await asyncio.sleep(self._debounce_seconds)
|
||||
async with self._get_user_lock(session_key):
|
||||
messages = self._pending_messages.pop(session_key, [])
|
||||
if not messages: return
|
||||
|
||||
debounce_elapsed = time.time() - process_start
|
||||
logger.info(f"[计时] user={user_id} 防抖等待完成: {debounce_elapsed:.1f}s")
|
||||
|
||||
# A. 合并与元数据修复(去重:同一防抖窗口内完全相同的内容只保留一条)
|
||||
seen_contents = set()
|
||||
unique_parts = []
|
||||
@@ -278,9 +301,12 @@ class SystemOrchestrator:
|
||||
)
|
||||
|
||||
# B. 持久化
|
||||
db_start = time.time()
|
||||
db_content = combined_content
|
||||
if all_image_urls: db_content = f"【系统:已收到{len(all_image_urls)}张图】\n{combined_content}"
|
||||
await repo.save_chat(platform, user_id, db_content, "in", acc_id=acc_id, image_urls=all_image_urls)
|
||||
db_elapsed = time.time() - db_start
|
||||
logger.info(f"[计时] user={user_id} 消息入库: {db_elapsed:.2f}s")
|
||||
|
||||
# B2. 后台图片分析(不阻塞主流程,用于数据标定)
|
||||
if all_image_urls:
|
||||
@@ -302,9 +328,17 @@ class SystemOrchestrator:
|
||||
)
|
||||
else:
|
||||
# D. 正常流程:调用AI思考
|
||||
history_start = time.time()
|
||||
history = await repo.get_chat_history(user_id, limit=10, acc_id=acc_id)
|
||||
if history and history[-1].get('content') == db_content: history = history[:-1]
|
||||
history_elapsed = time.time() - history_start
|
||||
logger.info(f"[计时] user={user_id} 查询历史: {history_elapsed:.2f}s (共{len(history)}条)")
|
||||
|
||||
ai_start = time.time()
|
||||
std_res = await self.brain.think_and_reply(final_msg, history=history)
|
||||
ai_elapsed = time.time() - ai_start
|
||||
total_elapsed = time.time() - process_start
|
||||
logger.info(f"[计时] user={user_id} AI思考: {ai_elapsed:.1f}s | 总耗时: {total_elapsed:.1f}s")
|
||||
|
||||
# E. 发送并记录时间
|
||||
if std_res.should_reply:
|
||||
|
||||
@@ -2,15 +2,20 @@ import os
|
||||
import re
|
||||
import hashlib
|
||||
import logging
|
||||
import time
|
||||
from typing import List, Optional, Any, Dict
|
||||
from pydantic_ai import Agent, RunContext
|
||||
from pydantic_ai.models.openai import OpenAIChatModel
|
||||
from pydantic_ai.providers.openai import OpenAIProvider
|
||||
from core.schema import StandardMessage, StandardResponse
|
||||
from core.agent_tools import register_agent_tools
|
||||
from core.agent_tools import register_agent_tools, TransferSuccessException
|
||||
|
||||
logger = logging.getLogger("cs_agent")
|
||||
|
||||
# 日志详细程度:设置环境变量 AI_LOG_LEVEL=debug 可获得完整日志
|
||||
_LOG_FULL_PROMPT = os.getenv("AI_LOG_LEVEL", "").lower() == "debug"
|
||||
_LOG_CLIP_LIMIT = int(os.getenv("AI_LOG_CLIP", "2000")) # 日志截断长度
|
||||
|
||||
from core.skill_manager import skill_manager
|
||||
|
||||
|
||||
@@ -21,6 +26,18 @@ _INTERNAL_TOOL_MARKERS = (
|
||||
"【订单详情】",
|
||||
)
|
||||
|
||||
# 历史记录格式检测模式(AI 转述历史时容易泄露)
|
||||
_HISTORY_LEAK_PATTERNS = [
|
||||
r'\[\d{4}-\d{2}-\d{2}[^\]]*\]\s*(客户|客服)[::]', # [2026-03-07 12:00:00] 客户:
|
||||
r'\[\d{2}:\d{2}:\d{2}\]\s*(客户|客服|我)[::]', # [12:00:00] 客户:
|
||||
r'(根据|查看|查询|翻看)(历史|聊天|对话)(记录|内容)', # 根据历史记录
|
||||
r'历史(记录|对话|消息)(显示|表明|中)', # 历史记录显示
|
||||
r'之前的(聊天|对话|记录)(中|里|显示)', # 之前的聊天中
|
||||
r'共\d+条(历史|对话)?消息', # 共30条历史消息
|
||||
r'订单号[::]\s*\d{10,}', # 订单号:xxxxxxxxxx
|
||||
r'(状态|金额|数量)[::].*(状态|金额|数量)[::]', # 状态:xxx 金额:xxx 连续出现
|
||||
]
|
||||
|
||||
|
||||
def _clip(text: str, limit: int = 1200) -> str:
|
||||
if text is None:
|
||||
@@ -61,10 +78,17 @@ def _sanitize_reply_text(reply_text: str) -> str:
|
||||
text = re.sub(r'[\[\]]{2,}', '', text)
|
||||
text = text.strip()
|
||||
|
||||
# 检查固定标记
|
||||
if any(marker in text for marker in _INTERNAL_TOOL_MARKERS):
|
||||
logger.warning("[Brain] 拦截到工具原文泄露,降级为安全兜底回复")
|
||||
return "我在帮你看记录,稍等哈"
|
||||
|
||||
# 检查历史记录泄露模式(AI 转述历史内容)
|
||||
for pattern in _HISTORY_LEAK_PATTERNS:
|
||||
if re.search(pattern, text):
|
||||
logger.warning(f"[Brain] 检测到历史记录泄露模式: {pattern[:30]}...")
|
||||
return "我在帮你看记录,稍等哈"
|
||||
|
||||
return text.strip()
|
||||
|
||||
|
||||
@@ -187,12 +211,51 @@ class CustomerServiceBrain:
|
||||
recent_context = "【近期对话回顾】\n" + "\n".join(lines) + "\n----------------\n"
|
||||
|
||||
full_input = f"【当前客户ID:{msg.user_id}】\n{recent_context}现在的对话:{user_content}"
|
||||
logger.info(
|
||||
f"[PROMPT->AI] user={msg.user_id} acc={msg.acc_id} images={len(msg.image_urls)}\n"
|
||||
f"{_clip(full_input)}"
|
||||
)
|
||||
start_time = time.time()
|
||||
|
||||
# ===== 详细日志:发给 AI 的提示词 =====
|
||||
logger.info(f"[AI提示词] user={msg.user_id} acc={msg.acc_id} images={len(msg.image_urls)}\n{full_input}")
|
||||
if history:
|
||||
history_preview = "\n".join([f" {h.get('role','?')}: {str(h.get('content',''))[:50]}" for h in history[-4:]])
|
||||
logger.info(f"[AI历史上下文] 共{len(history)}条:\n{history_preview}")
|
||||
|
||||
result = await self.agent.run(full_input, message_history=history)
|
||||
# 尝试运行 AI,捕获转接成功异常以提前终止
|
||||
try:
|
||||
result = await self.agent.run(full_input, message_history=history)
|
||||
except TransferSuccessException as e:
|
||||
# 转接工具成功后立即返回,无需等待 AI 继续生成
|
||||
elapsed = time.time() - start_time
|
||||
logger.info(f"[Brain] 转接成功(提前终止,耗时{elapsed:.1f}s): {e.transfer_cmd[:60]}")
|
||||
return StandardResponse(
|
||||
reply_content=e.transfer_cmd,
|
||||
need_transfer=True,
|
||||
metadata={"acc_id": msg.acc_id, "acc_type": msg.acc_type}
|
||||
)
|
||||
|
||||
elapsed = time.time() - start_time
|
||||
logger.info(f"[Brain] AI处理完成,总耗时{elapsed:.1f}s")
|
||||
|
||||
# ===== 详细日志:AI 的思考过程和工具调用 =====
|
||||
try:
|
||||
all_msgs = result.all_messages()
|
||||
for idx, m in enumerate(all_msgs):
|
||||
msg_kind = getattr(m, 'kind', type(m).__name__)
|
||||
if hasattr(m, 'parts'):
|
||||
for part in m.parts:
|
||||
part_kind = getattr(part, 'part_kind', '')
|
||||
if part_kind == 'tool-call':
|
||||
tool_name = getattr(part, 'tool_name', '?')
|
||||
tool_args = getattr(part, 'args', {})
|
||||
logger.info(f"[AI思考] 步骤{idx+1} 调用工具: {tool_name}({tool_args})")
|
||||
elif part_kind == 'tool-return':
|
||||
content = str(getattr(part, 'content', ''))[:200]
|
||||
logger.info(f"[AI思考] 步骤{idx+1} 工具返回: {content}")
|
||||
elif part_kind == 'text':
|
||||
content = str(getattr(part, 'content', ''))[:150]
|
||||
if content.strip():
|
||||
logger.info(f"[AI思考] 步骤{idx+1} 文本输出: {content}")
|
||||
except Exception as log_err:
|
||||
logger.debug(f"[AI思考日志] 解析失败: {log_err}")
|
||||
|
||||
# --- 转接指令:直接从工具返回截获,不经过 AI 二次加工 ---
|
||||
transfer_cmd = ""
|
||||
|
||||
Reference in New Issue
Block a user