refactor: split websocket flows and add brain action decision pipeline
This commit is contained in:
130
core/websocket_outbound_arbiter_flow.py
Normal file
130
core/websocket_outbound_arbiter_flow.py
Normal file
@@ -0,0 +1,130 @@
|
||||
import os
|
||||
import re
|
||||
import time
|
||||
|
||||
|
||||
def normalize_reply_semantic_key(text: str) -> str:
|
||||
s = (text or "").strip().lower()
|
||||
if not s:
|
||||
return ""
|
||||
for w in ("哈", "呀", "哦", "呢", "啦", "咯", "亲"):
|
||||
s = s.replace(w, "")
|
||||
s = re.sub(r"[,。!?、,.!?::;\s~\-—_]+", "", s)
|
||||
return s[:200]
|
||||
|
||||
|
||||
def classify_outbound_reply(text: str) -> str:
|
||||
s = (text or "").strip()
|
||||
if not s:
|
||||
return "empty"
|
||||
if any(k in s for k in ("报价", "总价", "多少钱", "多少", "马上给你报价", "先给你报")):
|
||||
return "quote"
|
||||
if any(k in s for k in ("继续发图", "发完", "发图", "把图发", "先看图")):
|
||||
return "collect"
|
||||
if any(k in s for k in ("在吗", "你好", "在的", "在呢")):
|
||||
return "greeting"
|
||||
if any(k in s for k in ("转人工", "转接", "转给")):
|
||||
return "transfer"
|
||||
if any(k in s for k in ("稍等", "我先看", "看一下", "看下")):
|
||||
return "ack"
|
||||
return "general"
|
||||
|
||||
|
||||
def template_family(reply: str) -> str:
|
||||
s = (reply or "").strip()
|
||||
if not s:
|
||||
return ""
|
||||
if "需求我记上了" in s and "继续发图" in s:
|
||||
return "collect_remind"
|
||||
if ("这批图过一遍" in s or "收齐了" in s or "收好了" in s) and ("总价" in s or "报价" in s):
|
||||
return "quote_defer"
|
||||
if "图片收到了" in s and "继续发" in s:
|
||||
return "collect_ack"
|
||||
if "好嘞,你稍等下,我这边看一下" in s:
|
||||
return "fallback_ack"
|
||||
return ""
|
||||
|
||||
|
||||
def outbound_arbiter(client, original_msg: dict, reply_content: str, trace_id: str) -> tuple[bool, str]:
|
||||
"""
|
||||
统一出站裁决层:
|
||||
1) 语义去重(相同语义短窗口不重复);
|
||||
2) 同类回复节流(同类话术短窗口不重复)。
|
||||
"""
|
||||
key = f"{original_msg.get('acc_id', '')}:{original_msg.get('from_id', '')}"
|
||||
now_mono = time.monotonic()
|
||||
sem_key = normalize_reply_semantic_key(reply_content)
|
||||
reply_class = classify_outbound_reply(reply_content)
|
||||
try:
|
||||
sem_window = max(30, int(os.getenv("AI_OUTBOUND_SEMANTIC_DEDUPE_SECONDS", "180")))
|
||||
except Exception:
|
||||
sem_window = 180
|
||||
try:
|
||||
class_window = max(20, int(os.getenv("AI_OUTBOUND_CLASS_DEDUPE_SECONDS", "90")))
|
||||
except Exception:
|
||||
class_window = 90
|
||||
try:
|
||||
template_window = max(120, int(os.getenv("AI_OUTBOUND_TEMPLATE_FATIGUE_SECONDS", "600")))
|
||||
except Exception:
|
||||
template_window = 600
|
||||
|
||||
sem_bucket = client._outbound_semantic_seen.setdefault(key, {})
|
||||
cls_bucket = client._outbound_class_seen.setdefault(key, {})
|
||||
tpl_bucket = client._outbound_template_seen.setdefault(key, {})
|
||||
client._prune_seen(sem_bucket, now_mono, ttl_sec=max(sem_window * 2, 240))
|
||||
client._prune_seen(cls_bucket, now_mono, ttl_sec=max(class_window * 2, 180))
|
||||
client._prune_seen(tpl_bucket, now_mono, ttl_sec=max(template_window * 2, 1200))
|
||||
|
||||
if sem_key and (now_mono - sem_bucket.get(sem_key, 0.0)) < sem_window:
|
||||
client._activity_log(
|
||||
"outbound_arbiter_block",
|
||||
trace_id=trace_id,
|
||||
acc_id=original_msg.get("acc_id", ""),
|
||||
customer_id=original_msg.get("from_id", ""),
|
||||
reason="semantic_duplicate",
|
||||
semantic_key=sem_key[:80],
|
||||
reply_class=reply_class,
|
||||
msg=reply_content,
|
||||
)
|
||||
return False, "semantic_duplicate"
|
||||
|
||||
family = template_family(reply_content)
|
||||
if family and (now_mono - tpl_bucket.get(family, 0.0)) < template_window:
|
||||
client._activity_log(
|
||||
"outbound_arbiter_block",
|
||||
trace_id=trace_id,
|
||||
acc_id=original_msg.get("acc_id", ""),
|
||||
customer_id=original_msg.get("from_id", ""),
|
||||
reason="template_fatigue",
|
||||
template_family=family,
|
||||
msg=reply_content,
|
||||
)
|
||||
return False, "template_fatigue"
|
||||
|
||||
if reply_class in {"quote", "collect", "ack"} and (now_mono - cls_bucket.get(reply_class, 0.0)) < class_window:
|
||||
client._activity_log(
|
||||
"outbound_arbiter_block",
|
||||
trace_id=trace_id,
|
||||
acc_id=original_msg.get("acc_id", ""),
|
||||
customer_id=original_msg.get("from_id", ""),
|
||||
reason="class_duplicate",
|
||||
reply_class=reply_class,
|
||||
msg=reply_content,
|
||||
)
|
||||
return False, "class_duplicate"
|
||||
|
||||
if sem_key:
|
||||
sem_bucket[sem_key] = now_mono
|
||||
cls_bucket[reply_class] = now_mono
|
||||
if family:
|
||||
tpl_bucket[family] = now_mono
|
||||
client._activity_log(
|
||||
"outbound_arbiter_pass",
|
||||
trace_id=trace_id,
|
||||
acc_id=original_msg.get("acc_id", ""),
|
||||
customer_id=original_msg.get("from_id", ""),
|
||||
reply_class=reply_class,
|
||||
template_family=family,
|
||||
semantic_key=sem_key[:80] if sem_key else "",
|
||||
)
|
||||
return True, "pass"
|
||||
Reference in New Issue
Block a user