Files
tw/core/websocket_brain_flow.py

312 lines
12 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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": "",
},
)
)