refactor: split websocket flows and add brain action decision pipeline
This commit is contained in:
311
core/websocket_brain_flow.py
Normal file
311
core/websocket_brain_flow.py
Normal file
@@ -0,0 +1,311 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import json
|
||||
import logging
|
||||
import re
|
||||
from dataclasses import dataclass
|
||||
from typing import Any
|
||||
|
||||
logger = logging.getLogger("cs_agent")
|
||||
|
||||
|
||||
@dataclass
|
||||
class BrainDecision:
|
||||
action: str # reply | quote | transfer | noop
|
||||
source: str
|
||||
reply: str = ""
|
||||
transfer_msg: str = ""
|
||||
should_reply: bool = False
|
||||
need_transfer: bool = False
|
||||
payload: dict[str, Any] | None = None
|
||||
|
||||
|
||||
def _extract_json_obj(text: str) -> dict[str, Any] | None:
|
||||
if not text:
|
||||
return None
|
||||
m = re.search(r"\{[\s\S]*\}", text)
|
||||
if not m:
|
||||
return None
|
||||
try:
|
||||
return json.loads(m.group(0))
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
|
||||
async def _ai_policy_brain_decide(client, data: dict, *, msg_text: str, shop_type: str) -> BrainDecision | None:
|
||||
if not client.enable_agent or not client.agent or not client.AgentDeps:
|
||||
return None
|
||||
|
||||
acc_id = str(data.get("acc_id", "") or "")
|
||||
customer_id = str(data.get("from_id", "") or "")
|
||||
current_urls = client._extract_image_urls(msg_text)
|
||||
recent_urls = client._collect_recent_image_urls(customer_id, acc_id, max_count=6)
|
||||
key = client._customer_key(data)
|
||||
pending_urls = client._pending_images.get(key) or []
|
||||
|
||||
try:
|
||||
order_status = client._detect_order_status(msg_text)
|
||||
has_image_url = client._msg_has_image_url(msg_text)
|
||||
refers_images = client._msg_refers_images(msg_text)
|
||||
is_price = client._msg_is_price_inquiry(msg_text)
|
||||
is_req = client._msg_is_requirement(msg_text)
|
||||
ext_contact = client._msg_requests_external_contact(msg_text)
|
||||
except Exception:
|
||||
order_status, has_image_url, refers_images, is_price, is_req, ext_contact = "", False, False, False, False, False
|
||||
|
||||
deps = client.AgentDeps(
|
||||
msg_id=str(data.get("msg_id", "") or "brain_policy"),
|
||||
acc_id=acc_id,
|
||||
from_id=customer_id,
|
||||
platform=str(data.get("acc_type", "") or "AliWorkbench"),
|
||||
)
|
||||
|
||||
prompt = (
|
||||
"你是淘宝客服系统的主决策Brain,只做决策,不要解释。\n"
|
||||
"你必须根据历史规则和当前上下文,输出唯一动作。\n"
|
||||
"可选动作 action: reply / quote / transfer / noop。\n"
|
||||
"历史规则(完整继承):\n"
|
||||
"1) 客户发图/补图:先自然承接,再根据上下文决定继续收集或报价;\n"
|
||||
"2) 客户询价且有可用图片(当前或最近)时,优先 action=quote;\n"
|
||||
"3) 若有 pending 图片且客户催报价/补充需求,优先 quote_mode=flush_pending;\n"
|
||||
"4) 仅打招呼/短无意义文本:可 action=reply 简短承接,不要机械模板;\n"
|
||||
"5) 索要外部联系方式(微信/QQ/手机号)时,不外呼,站内引导;\n"
|
||||
"6) 订单已付款:可回执安排处理;未付款/待付款:提醒完成付款;\n"
|
||||
"7) 地图/政治/高风险内容:谨慎,必要时 transfer 或拒绝性 reply;\n"
|
||||
"8) 尺寸超限/不可做场景:给明确边界,不要胡乱承诺;\n"
|
||||
"9) 客户没发图却问价:先承接,再引导发图;\n"
|
||||
"10) 避免重复外发,避免同一句话反复说。\n"
|
||||
"\n"
|
||||
"quote_mode 可选: flush_pending / analyze_current_or_recent / collect_only\n"
|
||||
"只输出 JSON:\n"
|
||||
'{"action":"reply|quote|transfer|noop","reply":"","transfer_msg":"","quote_mode":"","reason":""}\n\n'
|
||||
f"店铺类型: {shop_type}\n"
|
||||
f"legacy_fast_quote_enabled: {str(bool(client._legacy_fast_quote_enabled)).lower()}\n"
|
||||
f"客户原话: {msg_text}\n"
|
||||
f"has_image_url: {has_image_url}\n"
|
||||
f"current_image_urls_count: {len(current_urls)}\n"
|
||||
f"recent_image_urls_count: {len(recent_urls)}\n"
|
||||
f"pending_image_urls_count: {len(pending_urls)}\n"
|
||||
f"refers_images: {refers_images}\n"
|
||||
f"is_price_inquiry: {is_price}\n"
|
||||
f"is_requirement: {is_req}\n"
|
||||
f"requests_external_contact: {ext_contact}\n"
|
||||
f"order_status: {order_status or 'none'}\n"
|
||||
)
|
||||
|
||||
try:
|
||||
result = await client.agent.agent_natural_reply.run(prompt, deps=deps, message_history=[])
|
||||
raw = str(getattr(result, "output", "") or "").strip()
|
||||
obj = _extract_json_obj(raw)
|
||||
if not obj:
|
||||
client._activity_log(
|
||||
"brain_policy_parse_error",
|
||||
acc_id=acc_id,
|
||||
customer_id=customer_id,
|
||||
raw=raw[:300],
|
||||
)
|
||||
return None
|
||||
|
||||
action = str(obj.get("action", "") or "").strip().lower()
|
||||
reply = str(obj.get("reply", "") or "").strip()
|
||||
transfer_msg = str(obj.get("transfer_msg", "") or "").strip()
|
||||
quote_mode = str(obj.get("quote_mode", "") or "").strip().lower()
|
||||
reason = str(obj.get("reason", "") or "").strip()
|
||||
|
||||
payload: dict[str, Any] | None = None
|
||||
if action == "quote":
|
||||
mode = quote_mode or "analyze_current_or_recent"
|
||||
if mode == "flush_pending":
|
||||
payload = {"mode": "flush_pending", "key": key, "pre_reply": reply}
|
||||
elif mode == "collect_only":
|
||||
payload = {"mode": "collect_only", "pre_reply": reply}
|
||||
else:
|
||||
urls = current_urls or recent_urls
|
||||
payload = {"mode": "analyze_urls", "urls": urls, "pre_reply": reply}
|
||||
|
||||
decision = BrainDecision(
|
||||
action=action if action in {"reply", "quote", "transfer", "noop"} else "noop",
|
||||
source="brain_ai_policy",
|
||||
reply=reply,
|
||||
transfer_msg=transfer_msg,
|
||||
should_reply=bool(reply),
|
||||
need_transfer=(action == "transfer"),
|
||||
payload=payload,
|
||||
)
|
||||
client._activity_log(
|
||||
"brain_policy_raw",
|
||||
acc_id=acc_id,
|
||||
customer_id=customer_id,
|
||||
action=decision.action,
|
||||
quote_mode=quote_mode,
|
||||
reason=reason,
|
||||
)
|
||||
return decision
|
||||
except Exception as e:
|
||||
client._activity_log(
|
||||
"brain_policy_error",
|
||||
acc_id=acc_id,
|
||||
customer_id=customer_id,
|
||||
error=str(e),
|
||||
)
|
||||
return None
|
||||
|
||||
|
||||
async def decide_brain_action(client, data: dict, customer_msg, *, trace_id: str, msg_text: str, shop_type: str) -> BrainDecision:
|
||||
"""统一主决策层:优先由 Brain AI 决策;失败时回退 Agent 默认决策。"""
|
||||
ai_decision = await _ai_policy_brain_decide(client, data, msg_text=msg_text, shop_type=shop_type)
|
||||
if ai_decision is not None:
|
||||
return ai_decision
|
||||
|
||||
# 回退:保持可用性
|
||||
logger.info("Agent 正在处理消息...")
|
||||
client._activity_log(
|
||||
"agent_process_start",
|
||||
trace_id=trace_id,
|
||||
acc_id=data.get("acc_id", ""),
|
||||
customer_id=data.get("from_id", ""),
|
||||
msg=msg_text,
|
||||
)
|
||||
response = await client.agent.process_message(customer_msg)
|
||||
client._activity_log(
|
||||
"agent_process_done",
|
||||
trace_id=trace_id,
|
||||
acc_id=data.get("acc_id", ""),
|
||||
customer_id=data.get("from_id", ""),
|
||||
result="ok",
|
||||
should_reply=bool(response.should_reply),
|
||||
need_transfer=bool(response.need_transfer),
|
||||
)
|
||||
if response.need_transfer:
|
||||
return BrainDecision(
|
||||
action="transfer",
|
||||
source="fallback_agent",
|
||||
reply=response.reply or "",
|
||||
transfer_msg=response.transfer_msg or "",
|
||||
should_reply=bool(response.should_reply),
|
||||
need_transfer=True,
|
||||
)
|
||||
if response.should_reply and response.reply:
|
||||
return BrainDecision(
|
||||
action="reply",
|
||||
source="fallback_agent",
|
||||
reply=response.reply,
|
||||
should_reply=True,
|
||||
need_transfer=False,
|
||||
)
|
||||
return BrainDecision(action="noop", source="fallback_agent", should_reply=False, need_transfer=False)
|
||||
|
||||
|
||||
async def execute_brain_action(client, data: dict, *, decision: BrainDecision, trace_id: str, msg_text: str):
|
||||
"""统一执行层:只执行标准动作。"""
|
||||
customer_id = data.get("from_id", "")
|
||||
|
||||
if customer_id:
|
||||
client._touch_customer_last_contact(customer_id)
|
||||
|
||||
if decision.action == "transfer":
|
||||
logger.info("Agent 决定转接人工")
|
||||
client._activity_log(
|
||||
"agent_transfer",
|
||||
trace_id=trace_id,
|
||||
acc_id=data.get("acc_id", ""),
|
||||
customer_id=data.get("from_id", ""),
|
||||
transfer_msg=decision.transfer_msg,
|
||||
)
|
||||
client._fire_and_forget(
|
||||
client._post_tianwang_callback(
|
||||
"message_processed",
|
||||
data,
|
||||
extra={
|
||||
"should_reply": bool(decision.should_reply),
|
||||
"need_transfer": True,
|
||||
"agent_reply": decision.reply or "",
|
||||
"transfer_msg": decision.transfer_msg or "",
|
||||
},
|
||||
)
|
||||
)
|
||||
await client.transfer_to_human(data, decision.transfer_msg)
|
||||
client._push_chat_to_wechat_safe(
|
||||
data=data,
|
||||
customer_msg=msg_text,
|
||||
reply_msg=decision.transfer_msg or "转接",
|
||||
tag="转人工",
|
||||
)
|
||||
return
|
||||
|
||||
if decision.action == "reply":
|
||||
text = (decision.reply or "").strip()
|
||||
if not text:
|
||||
return
|
||||
await asyncio.sleep(0.6)
|
||||
client._activity_log(
|
||||
"agent_reply",
|
||||
trace_id=trace_id,
|
||||
acc_id=data.get("acc_id", ""),
|
||||
customer_id=data.get("from_id", ""),
|
||||
reply=text,
|
||||
)
|
||||
await client.send_reply(data, text)
|
||||
await client._maybe_schedule_auto_quote(data)
|
||||
client._fire_and_forget(
|
||||
client._post_tianwang_callback(
|
||||
"message_processed",
|
||||
data,
|
||||
extra={
|
||||
"should_reply": True,
|
||||
"need_transfer": False,
|
||||
"agent_reply": text,
|
||||
},
|
||||
)
|
||||
)
|
||||
client._push_chat_to_wechat_safe(
|
||||
data=data,
|
||||
customer_msg=msg_text,
|
||||
reply_msg=text,
|
||||
tag="正常AI回复",
|
||||
)
|
||||
return
|
||||
|
||||
if decision.action == "quote":
|
||||
payload = decision.payload or {}
|
||||
pre_reply = str(payload.get("pre_reply", "") or "").strip()
|
||||
if pre_reply:
|
||||
await client.send_reply(data, pre_reply)
|
||||
mode = str(payload.get("mode", "") or "")
|
||||
if mode == "flush_pending":
|
||||
key = str(payload.get("key", "") or "")
|
||||
if key:
|
||||
await client._flush_pending_images(key, data)
|
||||
elif mode == "analyze_urls":
|
||||
urls = payload.get("urls") or []
|
||||
if isinstance(urls, list) and urls:
|
||||
if len(urls) == 1:
|
||||
asyncio.create_task(client._analyze_single_and_reply(data, urls[0]))
|
||||
else:
|
||||
asyncio.create_task(client._analyze_multi_and_reply(data, urls))
|
||||
else:
|
||||
await client.send_reply(data, "你把要处理的图再发我一下,我马上给你看。")
|
||||
else:
|
||||
if not pre_reply:
|
||||
await client.send_reply(data, "收到,我先看一下哈,稍等哈。")
|
||||
return
|
||||
|
||||
# noop
|
||||
client._activity_log(
|
||||
"agent_no_reply",
|
||||
trace_id=trace_id,
|
||||
acc_id=data.get("acc_id", ""),
|
||||
customer_id=data.get("from_id", ""),
|
||||
)
|
||||
client._fire_and_forget(
|
||||
client._post_tianwang_callback(
|
||||
"message_processed",
|
||||
data,
|
||||
extra={
|
||||
"should_reply": False,
|
||||
"need_transfer": False,
|
||||
"agent_reply": "",
|
||||
},
|
||||
)
|
||||
)
|
||||
Reference in New Issue
Block a user