From cb3cbe88288b8e594ae2700889399a9765e1a8a2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=B5=85=E5=BF=B5?= Date: Mon, 2 Nov 2026 22:26:16 +0800 Subject: [PATCH] =?UTF-8?q?feat(wall):=20=E5=BB=BA=E6=9D=90=E9=A6=96?= =?UTF-8?q?=E6=AC=A1=E7=B4=A2=E8=A6=81=E6=94=B9=E5=8F=8C=E8=A6=81(WhatsApp?= =?UTF-8?q?+=E9=82=AE=E7=AE=B1=E4=B8=80=E8=B5=B7=E8=A6=81)+=E5=B7=B2?= =?UTF-8?q?=E7=95=99=E8=B5=84=E8=AF=9D=E6=9C=AF=E6=B8=A0=E9=81=93=E4=B8=AD?= =?UTF-8?q?=E6=80=A7+=E7=BC=BA=E5=A4=B1=E6=B8=A0=E9=81=93=E8=A1=A5?= =?UTF-8?q?=E9=97=AE=E9=80=9A=E7=94=A8=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Code --- seed_profiles/jiancai/wall.json | 46 +++++++++++++++--------------- tests/test_boundary_filter.py | 2 +- tests/test_cc_integration.py | 2 +- tests/test_dual_ask_static.py | 48 ++++++++++++++++++++++++++++++++ tests/test_lead_email_trigger.py | 12 ++++---- 5 files changed, 79 insertions(+), 31 deletions(-) create mode 100644 tests/test_dual_ask_static.py diff --git a/seed_profiles/jiancai/wall.json b/seed_profiles/jiancai/wall.json index cd54a02..8a3086d 100644 --- a/seed_profiles/jiancai/wall.json +++ b/seed_profiles/jiancai/wall.json @@ -66,57 +66,57 @@ ], "deflections": { "en": [ - "Prices vary a lot by config and shipping — our manager will prepare an exact quote for you. What's your WhatsApp or email?", - "For an exact price our manager needs your config and destination — drop your WhatsApp and you'll have the quote today.", - "I'd rather give you an exact number than a rough guess — leave your WhatsApp and our manager will send it over." + "Prices vary a lot by config and shipping — our manager will prepare an exact quote for you. What's your WhatsApp and email?", + "For an exact price our manager needs your config and destination — drop your WhatsApp and email and you'll have the quote today.", + "I'd rather give you an exact number than a rough guess — leave your WhatsApp and email and our manager will send it over." ], "zh": [ - "价格要看配置和目的地,客户经理会给你准确报价——留个WhatsApp或邮箱?", - "我让客户经理按你的情况出准确报价,留个联系方式,今天就能发你。" + "价格要看配置和目的地,客户经理会给你准确报价——留个WhatsApp和邮箱?", + "我让客户经理按你的情况出准确报价,留个WhatsApp和邮箱,今天就能发你。" ], "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." + "El precio exacto depende de la especificación y el flete — nuestro gerente te lo prepara. ¿Me dejas tu WhatsApp y tu correo?", + "Para un precio exacto, nuestro gerente necesita tu especificación y destino — déjanos tu WhatsApp y correo y lo tienes hoy.", + "Prefiero darte una cifra exacta y no un estimado — déjanos tu WhatsApp y correo y nuestro gerente te lo envía." ] }, "deflections_contact": { "en": [ - "Our manager will send the exact quote to your WhatsApp shortly. Anything else about our pipes?" + "Our manager will send you the exact quote shortly. Anything else about our pipes?" ], "zh": [ - "客户经理稍后会把准确报价发到你的 WhatsApp。还想了解什么管道产品?" + "客户经理稍后会把准确报价发给你。还想了解什么管道产品?" ], "es": [ - "Nuestro gerente te enviará el precio exacto a tu WhatsApp en breve. ¿Algo más sobre nuestras tuberías?" + "Nuestro gerente te enviará el precio exacto en breve. ¿Algo más sobre nuestras tuberías?" ] }, "pivots": { "en": [ - "Actually, for the exact price — what's your WhatsApp? Our manager will send it over." + "Actually, for the exact price — what's your WhatsApp and email? Our manager will send it over." ], "zh": [ - "具体的报价,客户经理会发给你——留个WhatsApp或邮箱?" + "具体的报价,客户经理会发给你——留个WhatsApp和邮箱?" ], "es": [ - "Para el precio exacto — ¿cuál es tu WhatsApp? Nuestro gerente te lo envía." + "Para el precio exacto — ¿me dejas tu WhatsApp y tu correo? Nuestro gerente te lo envía." ] }, "pivots_contact": { "en": [ - "Our manager will send the exact price to your WhatsApp shortly." + "Our manager will send you the exact price shortly." ], "zh": [ - "客户经理稍后会把准确报价发到你的 WhatsApp。" + "客户经理稍后会把准确报价发给你。" ], "es": [ - "Nuestro gerente te enviará el precio exacto a tu WhatsApp en breve." + "Nuestro gerente te enviará el precio exacto en breve." ] }, "no_contact": { "web": "Just tap the WhatsApp button at the top of this chat — our manager will reply within 12 hours!", - "app": "Drop your WhatsApp and our manager will reach out to you directly with the full quote!", - "already": "Got it — our manager will reach out to you at your WhatsApp shortly with the full quote!" + "app": "Drop your WhatsApp and email — our manager will reach out to you directly with the full quote!", + "already": "Got it — our manager will reach out to you shortly with the full quote!" }, "zero_number": { "long_number": "\\+?\\d[\\d\\s\\-().]{8,17}\\d", @@ -184,8 +184,8 @@ "normal": "⚠️ 职能边界提醒:客户正在询问价格/优惠/交付时效/实质安排。绝对禁止给出任何价格数字(包括复述客户自己的预算)、交付时间或任何承诺,按职能边界对应话术处理。" }, "contact_ask": { - "app_already": "⚠️ 客户正在索要我方联系方式,且客户已留过自己的联系方式。请逐字使用这句:\"Our manager will reach out to you at your WhatsApp shortly with the full quote!\"(中文同理)。绝对禁止输出任何号码或邮箱,也绝对禁止再索要客户已提供过的联系方式(缺失渠道可按轻补指引提一次;号码缺国家代码时按系统提醒回显补全后的完整号码请客户确认——仅系统提供的号码,禁止自编)。", - "app": "⚠️ 客户正在索要我方联系方式(最高意向信号)。当前为 App 会话,没有页面按钮——禁止使用任何 tap/button 按钮话术!请逐字使用这句:\"Drop your WhatsApp and our manager will reach out to you directly with the full quote!\"(中文同理引导客户留联系方式)。绝对禁止输出任何号码或邮箱。", + "app_already": "⚠️ 客户正在索要我方联系方式,且客户已留过自己的联系方式。请逐字使用这句:\"Our manager will reach out to you shortly with the full quote!\"(中文同理)。绝对禁止输出任何号码或邮箱,也绝对禁止再索要客户已提供过的联系方式(缺失渠道——号码或邮箱——可再礼貌提一次:邮箱按轻补指引逐字句、号码自然补问,不纠缠;号码缺国家代码时按系统提醒回显补全后的完整号码请客户确认——仅系统提供的号码,禁止自编)。", + "app": "⚠️ 客户正在索要我方联系方式(最高意向信号)。当前为 App 会话,没有页面按钮——禁止使用任何 tap/button 按钮话术!请逐字使用这句:\"Drop your WhatsApp and email — our manager will reach out to you directly with the full quote!\"(中文同理引导客户留联系方式)。绝对禁止输出任何号码或邮箱。", "web": "⚠️ 客户正在索要我方联系方式(最高意向信号)。请逐字使用这句指路:\"{no_contact_web}\"(邮箱同理指 Email 按钮)。绝对禁止口头报出任何号码或邮箱。" }, "language_follow": { @@ -193,9 +193,9 @@ "es": "⚠️ 语言提醒:客户这条消息是西语——你的回复全程用西语,模板句跟随客户语言自然表达(系统提醒提供的逐字话术优先用其语言版本,无则自然翻译保持意思与礼貌度);系统提供的补全号码逐字照抄,绝不改动数字。" }, "lead_given": { - "forbidden": "⚠️ 客户已提供联系方式({shown})。绝对禁止再索要客户已提供过的联系方式;缺失渠道仅可按轻补指引提一次;号码缺国家代码时按系统提醒回显补全后的完整号码请客户确认(仅系统提供的号码,禁止自编)。", + "forbidden": "⚠️ 客户已提供联系方式({shown})。绝对禁止再索要客户已提供过的联系方式;缺失渠道(号码或邮箱)仅可再礼貌提一次:邮箱按轻补指引逐字句、号码自然补问,不纠缠;号码缺国家代码时按系统提醒回显补全后的完整号码请客户确认(仅系统提供的号码,禁止自编)。", "respond_script": "需要回应时可直接逐字使用:\"{no_contact_already}\"(中文同理)。确认收到后继续解答客户问题即可,不必急着结束对话。", - "boundary_tail": "客户已留过联系方式——边界请求直接告知'客户经理会把准确报价/安排发到你 WhatsApp'即可,禁止再索要联系方式(缺失渠道可按轻补指引提一次;号码缺国家代码时按系统提醒回显补全后的完整号码请客户确认——仅系统提供的号码,禁止自编)。", + "boundary_tail": "客户已留过联系方式——边界请求直接告知'客户经理会把准确报价/安排发给你'即可,禁止再索要联系方式(缺失渠道——号码或邮箱——可再礼貌提一次:邮箱按轻补指引逐字句、号码自然补问,不纠缠;号码缺国家代码时按系统提醒回显补全后的完整号码请客户确认——仅系统提供的号码,禁止自编)。", "suspect": "⚠️ 客户疑似已提供联系方式({shown})。禁止重新索要;可礼貌地请客户确认一次。" }, "cc_guide": { diff --git a/tests/test_boundary_filter.py b/tests/test_boundary_filter.py index 7ce56db..f6e4223 100644 --- a/tests/test_boundary_filter.py +++ b/tests/test_boundary_filter.py @@ -101,7 +101,7 @@ def test_split_sentences_decimal_not_split(): def test_pivot_contact_aware(): - assert "WhatsApp?" in pivot_for("how much?") + assert "WhatsApp" in pivot_for("how much?") assert "WhatsApp?" not in pivot_for("how much?", has_contact=True) assert "客户经理" in pivot_for("多少钱") diff --git a/tests/test_cc_integration.py b/tests/test_cc_integration.py index c5cf8f8..3642527 100644 --- a/tests/test_cc_integration.py +++ b/tests/test_cc_integration.py @@ -119,7 +119,7 @@ def test_reminder_cc_pending_overrides_d6(): assert "+254 712 345678" in text assert "is that right" in text.lower() # 回显确认句(模板同源句式) assert "回显补全后的完整号码" in text # D6 改写后的 cc 豁免条款 - assert "缺失渠道仅可按轻补指引提一次" in text # 防自锁改写在位 + assert "缺失渠道(号码或邮箱)仅可再礼貌提一次" in text # 防自锁改写在位 def test_affirm_confirms_no_resend(async_db, monkeypatch): diff --git a/tests/test_dual_ask_static.py b/tests/test_dual_ask_static.py new file mode 100644 index 0000000..8974cf4 --- /dev/null +++ b/tests/test_dual_ask_static.py @@ -0,0 +1,48 @@ +"""双要话术静态锚(spec 2026-11-02-jiancai-dual-contact-ask §5.3)。 + +首次索要联系方式 = WhatsApp+邮箱一起要(双要);已留资后的确认/收尾话术 +渠道中性(邮箱-only 客户不再听到"发到你 WhatsApp")。wall.json 侧锚在 +本文件前半,system.md/company_facts.md 侧锚在后半(Task 2 追加)。 +""" +import json +from pathlib import Path + +SEED = Path(__file__).resolve().parents[1] / "seed_profiles" / "jiancai" + + +def _wall() -> dict: + return json.loads((SEED / "wall.json").read_text(encoding="utf-8")) + + +# ---- wall 侧 ---- + +def test_deflections_dual_ask_all_languages(): + w = _wall() + assert any("and email" in s for s in w["deflections"]["en"]) + assert any("和邮箱" in s for s in w["deflections"]["zh"]) + assert any("y tu correo" in s for s in w["deflections"]["es"]) + + +def test_pivots_dual_ask_all_languages(): + w = _wall() + assert "and email" in w["pivots"]["en"][0] + assert "和邮箱" in w["pivots"]["zh"][0] + assert "y tu correo" in w["pivots"]["es"][0] + + +def test_post_contact_templates_channel_neutral(): + """已留资确认/收尾模板不含渠道词(三语逐一钉死,防只用 en 子串漏检)。""" + w = _wall() + for group in ("deflections_contact", "pivots_contact"): + for lang, items in w[group].items(): + for s in items: + assert "whatsapp" not in s.lower(), (group, lang, s) + assert "whatsapp" not in w["no_contact"]["already"].lower() + + +def test_app_reverse_ask_dual_and_synced(): + """App 反向留资句双要,且 no_contact.app = contact_ask.app 逐字句同源。""" + w = _wall() + app = w["no_contact"]["app"] + assert "and email" in app + assert app in w["reminders"]["contact_ask"]["app"] diff --git a/tests/test_lead_email_trigger.py b/tests/test_lead_email_trigger.py index 4477f72..3faf74e 100644 --- a/tests/test_lead_email_trigger.py +++ b/tests/test_lead_email_trigger.py @@ -451,7 +451,7 @@ def test_reminder_boundary_branches_on_contact_state(): text = ConversationManager._build_context_reminder(raw, "多少钱", []) assert "职能边界" in text and "禁止再索要联系方式" in text # Task 3 防自锁改写: 本分支尾部补缺失渠道豁免(子串由尾补保留,非 :651 改写行) - assert "缺失渠道可按轻补指引提一次" in text + assert "缺失渠道——号码或邮箱——可再礼貌提一次" in text # 未留联系方式: 引导留资 text2 = ConversationManager._build_context_reminder({}, "多少钱", []) assert "职能边界" in text2 and "禁止再索要联系方式" not in text2 @@ -678,19 +678,19 @@ def test_reminder_app_contact_ask_gated_by_existing_contact(): text = ConversationManager._build_context_reminder( raw, "how can I contact you", [], source="app") assert "Drop your WhatsApp" not in text - assert "reach out to you at your WhatsApp" in text # 已留资版逐字脚本 + assert "reach out to you shortly with the full quote" in text # 已留资版逐字脚本 def test_reminder_app_contact_ask_confirmed_allows_missing_channel_once(): # Task 3 收尾: contact_note app+已留资分支的第三处禁令同步改写(禁令集完整)。 # 防空转设计: 双渠道齐 → 轻补 note 不注入;消息无价格词 → 边界分支不渲染; - # 此时"缺失渠道可按轻补指引提一次"只能来自该分支改写文本 - # (:651 版是"仅可按",不构成子串)。 + # 此时"缺失渠道——号码或邮箱——可再礼貌提一次"只能来自该分支改写文本 + # (forbidden 版是"仅可",不构成子串)。 raw = {"联系方式": {"whatsapp": "+254712345678", "email": "a@b.com"}} text = ConversationManager._build_context_reminder( raw, "how can I contact you?", [], source="app") - assert "reach out to you at your WhatsApp" in text # 已留资版逐字脚本在场 - assert "缺失渠道可按轻补指引提一次" in text # 第三处禁令改写钉死 + assert "reach out to you shortly with the full quote" in text # 已留资版逐字脚本在场 + assert "缺失渠道——号码或邮箱——可再礼貌提一次" in text # 第三处禁令改写钉死 assert "email address would help too" not in text # 双渠道齐→无轻补注入(防空转)