feat: queue pending transfers until designers are available
This commit is contained in:
@@ -10,6 +10,13 @@ from core.adapters.qianniu_adapter import QianniuAdapter
|
||||
from core.pydantic_ai_agent_v2 import CustomerServiceBrain
|
||||
from core.events.event_bus import bus
|
||||
from core.repository import repo
|
||||
from db.pending_transfer_db import (
|
||||
enqueue_pending_transfer,
|
||||
claim_due_pending_transfers,
|
||||
complete_pending_transfer,
|
||||
retry_pending_transfer,
|
||||
)
|
||||
from services.dispatch_service import dispatch_service
|
||||
|
||||
logger = logging.getLogger("cs_agent")
|
||||
|
||||
@@ -17,6 +24,8 @@ logger = logging.getLogger("cs_agent")
|
||||
MSG_DEDUP_CAPACITY = 200 # 消息 ID 去重缓存容量
|
||||
TRANSFER_COOLDOWN_SEC = 120 # 转接冷却时间(秒)—— 转接后2分钟内不再调用AI
|
||||
DEBOUNCE_SECONDS = 2.0 # 消息防抖延迟(秒)
|
||||
PENDING_TRANSFER_POLL_SECONDS = 30
|
||||
PENDING_TRANSFER_RETRY_SECONDS = 60
|
||||
|
||||
# 转接后安抚话术池(轮换使用,避免复读)
|
||||
_TRANSFER_CALM_REPLIES = [
|
||||
@@ -70,6 +79,7 @@ class SystemOrchestrator:
|
||||
self._debounce_tasks: Dict[str, asyncio.Task] = {}
|
||||
self._pending_messages: Dict[str, List[StandardMessage]] = {}
|
||||
self._user_locks: Dict[str, asyncio.Lock] = {}
|
||||
self._pending_transfer_task: Optional[asyncio.Task] = None
|
||||
|
||||
bus.subscribe("MESSAGE_OUTBOUND", self.handle_outbound_event)
|
||||
|
||||
@@ -86,6 +96,17 @@ class SystemOrchestrator:
|
||||
self._user_locks[user_id] = asyncio.Lock()
|
||||
return self._user_locks[user_id]
|
||||
|
||||
def _should_run_pending_transfer_worker(self) -> bool:
|
||||
worker_id = getattr(self.ws_client, "worker_id", -1) if self.ws_client else -1
|
||||
return worker_id in (-1, 0)
|
||||
|
||||
def _ensure_background_tasks(self):
|
||||
if not self._should_run_pending_transfer_worker():
|
||||
return
|
||||
if self._pending_transfer_task is None or self._pending_transfer_task.done():
|
||||
self._pending_transfer_task = asyncio.create_task(self._process_pending_transfers_loop())
|
||||
logger.info("[Orchestrator] 待转接轮询任务已启动")
|
||||
|
||||
@staticmethod
|
||||
def _sanitize_outbound_text(text: str) -> str:
|
||||
if not text:
|
||||
@@ -107,6 +128,7 @@ class SystemOrchestrator:
|
||||
"""链路入口"""
|
||||
try:
|
||||
if platform != "qianniu": return
|
||||
self._ensure_background_tasks()
|
||||
|
||||
std_msg, direction = await self.qianniu_adapter.translate_inbound(raw_data)
|
||||
|
||||
@@ -343,7 +365,9 @@ class SystemOrchestrator:
|
||||
# E. 发送并记录时间
|
||||
if std_res.should_reply:
|
||||
std_res.reply_content = self._sanitize_outbound_text(std_res.reply_content)
|
||||
std_res.metadata = {"acc_id": acc_id, "acc_type": acc_type}
|
||||
meta = dict(std_res.metadata or {})
|
||||
meta.update({"acc_id": acc_id, "acc_type": acc_type})
|
||||
std_res.metadata = meta
|
||||
|
||||
# 转接场景:先发一句安抚话,再发转接指令
|
||||
if "[转移会话]" in std_res.reply_content:
|
||||
@@ -358,6 +382,21 @@ class SystemOrchestrator:
|
||||
await self.qianniu_adapter.translate_outbound(std_res, user_id)
|
||||
await repo.save_chat(platform, user_id, std_res.reply_content, "out", acc_id=acc_id)
|
||||
|
||||
if std_res.metadata.get("pending_transfer"):
|
||||
reason = str(std_res.metadata.get("pending_transfer_reason") or "").strip()
|
||||
if reason:
|
||||
pending_id = await asyncio.to_thread(
|
||||
enqueue_pending_transfer,
|
||||
customer_id=user_id,
|
||||
acc_id=acc_id,
|
||||
acc_type=acc_type,
|
||||
platform=platform,
|
||||
reason=reason,
|
||||
)
|
||||
logger.info(
|
||||
f"[Orchestrator] 已加入待转接池: pending_id={pending_id} user={user_id} acc={acc_id}"
|
||||
)
|
||||
|
||||
if "[转移会话]" in std_res.reply_content:
|
||||
self._last_transfer_time[session_key] = time.time()
|
||||
|
||||
@@ -370,6 +409,71 @@ class SystemOrchestrator:
|
||||
response.reply_content = self._sanitize_outbound_text(response.reply_content)
|
||||
await self.qianniu_adapter.translate_outbound(response, user_id)
|
||||
|
||||
async def _process_pending_transfers_loop(self):
|
||||
while True:
|
||||
try:
|
||||
if not self.ws_client or not getattr(self.ws_client, "websocket", None):
|
||||
await asyncio.sleep(PENDING_TRANSFER_POLL_SECONDS)
|
||||
continue
|
||||
|
||||
rows = await asyncio.to_thread(claim_due_pending_transfers, 10)
|
||||
if not rows:
|
||||
await asyncio.sleep(PENDING_TRANSFER_POLL_SECONDS)
|
||||
continue
|
||||
|
||||
for row in rows:
|
||||
row_id = int(row["id"])
|
||||
customer_id = str(row.get("customer_id") or "")
|
||||
acc_id = str(row.get("acc_id") or "")
|
||||
acc_type = str(row.get("acc_type") or "AliWorkbench")
|
||||
reason = str(row.get("reason") or "").strip()
|
||||
|
||||
try:
|
||||
designer_name = await dispatch_service.assign_designer(user_id=customer_id)
|
||||
if not designer_name:
|
||||
await asyncio.to_thread(
|
||||
retry_pending_transfer,
|
||||
row_id,
|
||||
PENDING_TRANSFER_RETRY_SECONDS,
|
||||
"designer_unavailable",
|
||||
)
|
||||
continue
|
||||
|
||||
notify = StandardResponse(
|
||||
reply_content="设计师上线了,我给您转过去哈",
|
||||
metadata={"acc_id": acc_id, "acc_type": acc_type},
|
||||
)
|
||||
transfer = StandardResponse(
|
||||
reply_content=f"正在为您转接|[转移会话],{designer_name},无原因",
|
||||
need_transfer=True,
|
||||
metadata={"acc_id": acc_id, "acc_type": acc_type},
|
||||
)
|
||||
|
||||
await self.qianniu_adapter.translate_outbound(notify, customer_id)
|
||||
await repo.save_chat("qianniu", customer_id, notify.reply_content, "out", acc_id=acc_id)
|
||||
await asyncio.sleep(0.5)
|
||||
await self.qianniu_adapter.translate_outbound(transfer, customer_id)
|
||||
await repo.save_chat("qianniu", customer_id, transfer.reply_content, "out", acc_id=acc_id)
|
||||
|
||||
self._last_transfer_time[f"{customer_id}@{acc_id}"] = time.time()
|
||||
await asyncio.to_thread(complete_pending_transfer, row_id)
|
||||
logger.info(
|
||||
f"[Orchestrator] 待转接自动完成: pending_id={row_id} user={customer_id} designer={designer_name} reason={reason}"
|
||||
)
|
||||
except Exception as e:
|
||||
logger.warning(f"[Orchestrator] 待转接处理失败 pending_id={row_id}: {e}")
|
||||
await asyncio.to_thread(
|
||||
retry_pending_transfer,
|
||||
row_id,
|
||||
PENDING_TRANSFER_RETRY_SECONDS,
|
||||
str(e),
|
||||
)
|
||||
except asyncio.CancelledError:
|
||||
break
|
||||
except Exception as e:
|
||||
logger.warning(f"[Orchestrator] 待转接轮询异常: {e}")
|
||||
await asyncio.sleep(PENDING_TRANSFER_RETRY_SECONDS)
|
||||
|
||||
# 全局单例
|
||||
orchestrator: Optional[SystemOrchestrator] = None
|
||||
def init_orchestrator(ws_client):
|
||||
|
||||
@@ -3,6 +3,7 @@ import re
|
||||
import hashlib
|
||||
import logging
|
||||
import time
|
||||
import json
|
||||
from typing import List, Optional, Any, Dict
|
||||
from pydantic_ai import Agent, RunContext
|
||||
from pydantic_ai.models.openai import OpenAIChatModel
|
||||
@@ -236,6 +237,8 @@ class CustomerServiceBrain:
|
||||
logger.info(f"[Brain] AI处理完成,总耗时{elapsed:.1f}s")
|
||||
|
||||
# ===== 详细日志:AI 的思考过程和工具调用 =====
|
||||
pending_transfer_reason = ""
|
||||
pending_transfer_error = ""
|
||||
try:
|
||||
all_msgs = result.all_messages()
|
||||
for idx, m in enumerate(all_msgs):
|
||||
@@ -247,9 +250,20 @@ class CustomerServiceBrain:
|
||||
tool_name = getattr(part, 'tool_name', '?')
|
||||
tool_args = getattr(part, 'args', {})
|
||||
logger.info(f"[AI思考] 步骤{idx+1} 调用工具: {tool_name}({tool_args})")
|
||||
if tool_name == "transfer_to_human_tool":
|
||||
if isinstance(tool_args, str):
|
||||
try:
|
||||
tool_args = json.loads(tool_args)
|
||||
except Exception:
|
||||
tool_args = {"reason": tool_args}
|
||||
if isinstance(tool_args, dict):
|
||||
pending_transfer_reason = str(tool_args.get("reason") or "").strip()
|
||||
elif part_kind == 'tool-return':
|
||||
content = str(getattr(part, 'content', ''))[:200]
|
||||
logger.info(f"[AI思考] 步骤{idx+1} 工具返回: {content}")
|
||||
full_content = str(getattr(part, 'content', ''))
|
||||
if full_content.startswith("ERROR_DESIGNER_"):
|
||||
pending_transfer_error = full_content
|
||||
elif part_kind == 'text':
|
||||
content = str(getattr(part, 'content', ''))[:150]
|
||||
if content.strip():
|
||||
@@ -298,7 +312,13 @@ class CustomerServiceBrain:
|
||||
return StandardResponse(
|
||||
reply_content=reply_text,
|
||||
need_transfer=need_transfer,
|
||||
metadata={"acc_id": msg.acc_id, "acc_type": msg.acc_type}
|
||||
metadata={
|
||||
"acc_id": msg.acc_id,
|
||||
"acc_type": msg.acc_type,
|
||||
"pending_transfer": bool(pending_transfer_error and pending_transfer_reason),
|
||||
"pending_transfer_reason": pending_transfer_reason,
|
||||
"pending_transfer_error": pending_transfer_error,
|
||||
}
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
|
||||
Reference in New Issue
Block a user