feat: 多语言输出墙(€/£/西葡俄阿价格词+时间承诺+ES话术池)+话术建材化+防误杀
Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
@@ -9,7 +9,8 @@
|
||||
- 公里/km 排除必须直接挂在 万块? 之后: 挂在末尾裸 万 备选上会被
|
||||
万块? 的空块匹配绕过("3万公里"仍命中,实测)。
|
||||
- 预算复述(裸 budget+数字/中文数字万)、金融裸数(首付/月供)收严;
|
||||
里程 k 形式(30k km/45k miles)除外。质保期不再豁免(政策变更 2026-09-10:
|
||||
k 形式的 km/miles 除外项保留(建材语境已无里程语义,兜底无害)。质保期
|
||||
不再豁免(政策变更 2026-09-10:
|
||||
保修期限是编造的公司政策承诺,一并墙级拦截,见 PRICE_PATTERNS 时间承诺组)。"""
|
||||
import re
|
||||
|
||||
@@ -23,19 +24,43 @@ PRICE_PATTERNS = [
|
||||
# 时间承诺: 去掉 warranty 豁免(政策变更 2026-09-10——保修期限是编造的
|
||||
# 公司政策承诺,不再白名单)
|
||||
re.compile(r"\d+\s*[- ]?(day|week|month)s?\b", re.I),
|
||||
# 年限保修定向(双向): year 不能进时间模式(车龄 "under 5 years old" 会误杀),
|
||||
# 年限保修定向(双向): year 不能进时间模式(使用年限 "under 5 years old" 会误杀),
|
||||
# 只收 数字+年 与 warranty/质保/保修 的组合(含倒装"质保3年"/"warranty of 8 years")
|
||||
re.compile(r"\d+\s*years?\b[^.!?]{0,15}warranty", re.I),
|
||||
re.compile(r"warranty\s*(?:of|for|:)?\s*\d+\s*years?\b", re.I),
|
||||
re.compile(r"\d+\s*年[^。!?]{0,8}(?:质保|保修)"),
|
||||
re.compile(r"(?:质保|保修)[^。!?]{0,8}\d+\s*年"),
|
||||
# 中文时限承诺: 天/周/个月(年不收——"2024年款" 是车型年款)
|
||||
# 中文时限承诺: 天/周/个月(年不收——裸 "2024年" 是年份/项目日期,
|
||||
# 带质保/保修的年限由上方定向组收)
|
||||
re.compile(r"\d+\s*(?:天|周|个月)"),
|
||||
# 裸预算复述(F3,方案B旗舰形态——冒烟 6/6 破防即此形,原模式完全漏检)
|
||||
re.compile(r"(budget|预算)[^\d\n]{0,12}\d", re.I),
|
||||
re.compile(r"(两|三|四|五|六|七|八|九|十)\s*万(?!\s*(公里|km))"),
|
||||
# 金融裸数(F4)
|
||||
re.compile(r"(down\s+payment|monthly\s+payment|首付|月供)[^\d\n]{0,10}\d", re.I),
|
||||
# ---- 多语言墙(建材市场: 欧/英/南美/俄语区/中东) ----
|
||||
# 货币符号: €/£(参考项目只有 $)
|
||||
re.compile(r"[€£]\s?\d"),
|
||||
# 价格名词前置(西/葡/俄/阿): 词后 0-20 非数字字符内出现数字
|
||||
# ⚠️ 拉丁词必须加 \b 词边界——custo 是英文 customs 的子串,外贸高频,
|
||||
# "customs paperwork for 20ft" 会被误杀(Task 3 质量审查发现)
|
||||
# ⚠️ 阿语定冠词 ال 与词连写(السعر): 词首 \b 落在 ل|س 之间(两者皆 \w)
|
||||
# 不成立,备选须收 (?:ال)? 形态——裸 \bسعر\b 实测漏检 "السعر 500 دولار"
|
||||
re.compile(r"\b(?:precio|costo|custo|preço|цена|стоимость|"
|
||||
r"(?:ال)?سعر|(?:ال)?ثمن)\b[^\d\n]{0,20}\d", re.I),
|
||||
# 货币名词后置(数字+货币词): dólares/euros/рублей(西里尔 долларов
|
||||
# 由上方 стоимость 前置组兜住,不单列)
|
||||
# ⚠️ euros 排除 pallets 邻接——"50 euro pallets"(欧标托盘)是建材
|
||||
# 物流高频词,非价格
|
||||
re.compile(r"\d[\d,.]*\s*(?:d[óo]lares?|euros?\b(?!\s*pallets?)|"
|
||||
r"рублей|руб\.?)", re.I),
|
||||
# 多语言时间承诺(西/葡 dias|semanas,俄 дн/недел,阿 أيام;与英文
|
||||
# days/weeks 组同构)
|
||||
# ⚠️ 不收 horas/hours/小时——system.md 的 "reply within 12 hours"
|
||||
# 依赖此豁免(响应时效承诺非交期承诺)
|
||||
# ⚠️ 俄语变格只收 дн(ей|я)/недел[ьи] 主形态,其余变格(дню/неделю
|
||||
# 等)不收——宁可少而准
|
||||
re.compile(r"\d+\s*[- ]?(?:d[ií]as?|semanas?|дн(?:ей|я)|недел[ьи]|أيام)\b"),
|
||||
]
|
||||
|
||||
DEFLECTIONS_EN = [
|
||||
@@ -51,10 +76,22 @@ DEFLECTIONS_ZH = [
|
||||
# F6(D6×过滤器矛盾): 客户已留联系方式时,迂回话术不得再索要——否则与
|
||||
# 注入的"禁止再索要联系方式"提醒直接打架
|
||||
DEFLECTIONS_EN_CONTACT = [
|
||||
"Our manager will send the exact quote to your WhatsApp shortly. Anything else about the cars?",
|
||||
"Our manager will send the exact quote to your WhatsApp shortly. Anything else about our pipes?",
|
||||
]
|
||||
DEFLECTIONS_ZH_CONTACT = [
|
||||
"客户经理稍后会把准确报价发到你的 WhatsApp。还想了解什么车?",
|
||||
"客户经理稍后会把准确报价发到你的 WhatsApp。还想了解什么管道产品?",
|
||||
]
|
||||
|
||||
# ---- Task 5: ES 话术池(南美第一大流量) ----
|
||||
# 与 EN 池语义对齐的西语替换句(注意: 是"替换句",绝不含价格数字,
|
||||
# 且不得触发本墙自身——见 test_spanish_pools_are_wall_clean)
|
||||
DEFLECTIONS_ES = [
|
||||
"El precio exacto depende de la especificación y el flete — nuestro gerente te lo prepara. ¿Me dejas tu WhatsApp o correo?",
|
||||
"Para un precio exacto, nuestro gerente necesita tu especificación y destino — déjame tu WhatsApp y lo tienes hoy.",
|
||||
"Prefiero darte una cifra exacta y no un estimado — déjanos tu WhatsApp y nuestro gerente te lo envía.",
|
||||
]
|
||||
DEFLECTIONS_ES_CONTACT = [
|
||||
"Nuestro gerente te enviará el precio exacto a tu WhatsApp en breve. ¿Algo más sobre nuestras tuberías?",
|
||||
]
|
||||
|
||||
|
||||
@@ -101,24 +138,43 @@ PIVOTS_EN_CONTACT = [
|
||||
PIVOTS_ZH_CONTACT = [
|
||||
"客户经理稍后会把准确报价发到你的 WhatsApp。",
|
||||
]
|
||||
PIVOTS_ES = [
|
||||
"Para el precio exacto — ¿cuál es tu WhatsApp? Nuestro gerente te lo envía.",
|
||||
]
|
||||
PIVOTS_ES_CONTACT = [
|
||||
"Nuestro gerente te enviará el precio exacto a tu WhatsApp en breve.",
|
||||
]
|
||||
|
||||
|
||||
# Task 5: 语言路由三分支——汉字→ZH;西语特征(重音符/ñ/¿¡ 粗匹配,与
|
||||
# 汉字检测同构)→ES;否则 EN。葡语借道 ES 池(拉美沟通惯例相近)
|
||||
def _lang_pool(user_msg: str):
|
||||
if re.search(r"[一-鿿]", user_msg or ""):
|
||||
return "zh"
|
||||
if re.search(r"[áéíóúñ¿¡]", user_msg or ""):
|
||||
return "es"
|
||||
return "en"
|
||||
|
||||
|
||||
def pivot_for(user_msg: str, has_contact: bool = False) -> str:
|
||||
zh = bool(re.search(r"[一-鿿]", user_msg or ""))
|
||||
lang = _lang_pool(user_msg)
|
||||
if has_contact:
|
||||
pool = PIVOTS_ZH_CONTACT if zh else PIVOTS_EN_CONTACT
|
||||
pool = {"zh": PIVOTS_ZH_CONTACT, "es": PIVOTS_ES_CONTACT,
|
||||
"en": PIVOTS_EN_CONTACT}[lang]
|
||||
else:
|
||||
pool = PIVOTS_ZH if zh else PIVOTS_EN
|
||||
pool = {"zh": PIVOTS_ZH, "es": PIVOTS_ES, "en": PIVOTS_EN}[lang]
|
||||
return pool[0]
|
||||
|
||||
|
||||
def deflection_for(user_msg: str, rotate: int = 0,
|
||||
has_contact: bool = False) -> str:
|
||||
zh = bool(re.search(r"[一-鿿]", user_msg or ""))
|
||||
lang = _lang_pool(user_msg)
|
||||
if has_contact:
|
||||
pool = DEFLECTIONS_ZH_CONTACT if zh else DEFLECTIONS_EN_CONTACT
|
||||
pool = {"zh": DEFLECTIONS_ZH_CONTACT, "es": DEFLECTIONS_ES_CONTACT,
|
||||
"en": DEFLECTIONS_EN_CONTACT}[lang]
|
||||
else:
|
||||
pool = DEFLECTIONS_ZH if zh else DEFLECTIONS_EN
|
||||
pool = {"zh": DEFLECTIONS_ZH, "es": DEFLECTIONS_ES,
|
||||
"en": DEFLECTIONS_EN}[lang]
|
||||
return pool[rotate % len(pool)]
|
||||
|
||||
|
||||
@@ -139,7 +195,7 @@ NO_CONTACT_ALREADY = ("Got it — our manager will reach out to you at your "
|
||||
def no_contact_value(sentence: str, user_history: list[dict] | None = None) -> bool:
|
||||
"""零号码墙(v2): AI 输出不得含任何 ≥10 位数字串或邮箱。
|
||||
唯一豁免: 该串出现在近期客户消息中(客户自己号码的礼貌回显)。
|
||||
无值比对——年份/里程/VIN/续航天然安全(已验证)。"""
|
||||
无值比对——年份/规格编号天然安全(已验证)。"""
|
||||
for m in LONG_NUMBER_RE.finditer(sentence or ""):
|
||||
c = digits_only(m.group(0))
|
||||
if user_history and any(c in digits_only(msg.get("content") or "")
|
||||
|
||||
Reference in New Issue
Block a user