Files
tw/core/websocket_outbound_arbiter_flow.py

131 lines
4.7 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.
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"