docs: 双要+国家代码实施计划(5任务TDD)
Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,303 @@
|
||||
# 联系方式双要 + 国家代码完整化 实施计划
|
||||
|
||||
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
||||
|
||||
**Goal:** AI 索要联系方式升级为"WhatsApp(带国家代码)+ 邮箱分步双要",同时再索要墙精确化为按渠道放行缺失渠道的补充索要。
|
||||
|
||||
**Architecture:** 三层闭环——system.md 话术教行为;conversation.py 状态感知提醒(一次性计数 `_cc_asked`/`_email_asked`,号码闭环优先于邮箱);boundary_filter.asks_contact 渠道精确化。推送端补 `_` 前缀过滤防内部状态泄漏。
|
||||
|
||||
**Tech Stack:** 既有栈(FastAPI/pytest,LLM 全 mock)+ 生产真实验收
|
||||
|
||||
**设计文档:** `docs/superpowers/specs/2026-10-08-dual-contact-and-country-code-design.md`(本计划的唯一依据,含全部决策)
|
||||
|
||||
**约定:** 每任务 pytest 全绿才 commit;中文进文件用 Read/Edit 工具(环境坑见 CLAUDE.md)。
|
||||
|
||||
---
|
||||
|
||||
### Task 1: 再索要墙渠道精确化(TDD)
|
||||
|
||||
**Files:**
|
||||
- Modify: `app/services/boundary_filter.py:276-278`(asks_contact)、`app/services/conversation.py:210`(拆 confirmed)、`:239/:258`(两处调用传参)
|
||||
- Test: `tests/test_boundary_filter.py`
|
||||
|
||||
- [ ] **Step 1: 写失败测试**(追加到 test_boundary_filter.py,先看现有 REASK 测试的导入风格)
|
||||
|
||||
```python
|
||||
def test_reask_wall_channel_precision():
|
||||
"""渠道精确化(2026-10-08 spec §4.4): 索要已在手的渠道才丢,
|
||||
索要缺失渠道放行;泛渠道词只要手上有任何号码渠道即丢(宁丢勿放)。"""
|
||||
wa = {"whatsapp"} # 已有 WhatsApp
|
||||
em = {"email"} # 只有邮箱
|
||||
# 精确渠道: 已在手的丢
|
||||
assert asks_contact("What's your WhatsApp number?", wa) is True
|
||||
assert asks_contact("留个邮箱吧", em) is True
|
||||
# 精确渠道: 缺失的放行(双要核心场景)
|
||||
assert asks_contact("And an email address would help too", wa) is False
|
||||
assert asks_contact("你WhatsApp多少?", em) is False
|
||||
# 泛渠道词: 手上有任何号码渠道(whatsapp/phone/wechat)即丢
|
||||
assert asks_contact("What's your number?", wa) is True
|
||||
assert asks_contact("留个电话呗", wa) is True
|
||||
# 泛渠道词: 只有邮箱时放行
|
||||
assert asks_contact("What's your number?", em) is False
|
||||
# 精确+泛同句共存: 任一命中判丢
|
||||
assert asks_contact("leave your number or email", wa) is True
|
||||
# 无参调用: 原行为(命中即丢)不变
|
||||
assert asks_contact("What's your WhatsApp number?") is True
|
||||
assert asks_contact("hello there") is False
|
||||
```
|
||||
|
||||
- [ ] **Step 2: 跑红** `python -m pytest tests/test_boundary_filter.py::test_reask_wall_channel_precision -v` → TypeError(asks_contact 不收第二参)
|
||||
|
||||
- [ ] **Step 3: 实现**。boundary_filter.py 把 asks_contact 替换为(保持 REASK_PATTERNS 不动,新增渠道映射层):
|
||||
|
||||
```python
|
||||
# 渠道精确化(2026-10-08 spec §4.4): REASK 模式命中后按渠道词分辨——
|
||||
# 索要已在手的渠道才丢,索要缺失渠道放行(双要补邮箱场景);泛渠道词
|
||||
# (number/phone/contact/电话/联系方式/号码)只要手上有任何号码渠道即丢,
|
||||
# 宁丢勿放防重复索要;精确+泛同句共存时任一命中判丢。
|
||||
_PRECISE_CHANNEL_RE = re.compile(
|
||||
r"whatsapp|e-?mail|邮箱|微信|wechat", re.I)
|
||||
_GENERIC_CONTACT_RE = re.compile(
|
||||
r"(?:^|\b)(?:number|phone|contact|电话|联系方式|号码)", re.I)
|
||||
|
||||
|
||||
def asks_contact(sentence: str, confirmed_channels: set | None = None) -> bool:
|
||||
"""输出句是否在索要联系方式(已留资时用于丢弃)。
|
||||
|
||||
confirmed_channels=None → 原行为(命中即丢,向后兼容);
|
||||
传入已确认渠道集合(如 {"whatsapp","phone"}) → 渠道精确化。"""
|
||||
s = sentence or ""
|
||||
if confirmed_channels is None:
|
||||
return any(p.search(s) for p in REASK_PATTERNS)
|
||||
if not any(p.search(s) for p in REASK_PATTERNS):
|
||||
return False
|
||||
has_number_channel = bool(
|
||||
confirmed_channels & {"whatsapp", "phone", "wechat"})
|
||||
for m in _PRECISE_CHANNEL_RE.finditer(s):
|
||||
w = m.group(0).lower()
|
||||
ch = ("email" if w in ("e-mail", "email", "邮箱")
|
||||
else "wechat" if w in ("微信", "wechat") else "whatsapp")
|
||||
if ch not in confirmed_channels:
|
||||
return False # 索要缺失的精确渠道 → 放行
|
||||
if has_number_channel and _GENERIC_CONTACT_RE.search(s):
|
||||
return True # 泛词 + 手上有号码 → 丢
|
||||
return True # 命中但无精确渠道词(纯泛词/其他形态) → 保守丢
|
||||
```
|
||||
|
||||
- [ ] **Step 4: 跑绿**(该测试 + 现有全部 REASK 测试不回归)
|
||||
|
||||
- [ ] **Step 5: conversation.py 接线**。`:210` 处把 `has_contact = bool(split_validate(...))` 的结果拆出 confirmed 渠道集合(先读该处上下文确认变量形态),供 `:239`、`:258` 两个调用点用:
|
||||
|
||||
```python
|
||||
_confirmed_contacts, _ = split_validate(...) # 复用既有调用,不再只取 bool
|
||||
has_contact = bool(_confirmed_contacts)
|
||||
_confirmed_channels = set(_confirmed_contacts) # {"whatsapp","phone","email","wechat"}
|
||||
# 两处调用改为:
|
||||
if has_contact and asks_contact(sentence, _confirmed_channels):
|
||||
...
|
||||
elif has_contact and asks_contact(pending, _confirmed_channels):
|
||||
```
|
||||
|
||||
- [ ] **Step 6: 全量绿 + commit**
|
||||
|
||||
```bash
|
||||
python -m pytest -q && git add -A && git commit -m "feat: 再索要墙渠道精确化——索要缺失渠道放行(双要前置)
|
||||
|
||||
Co-Authored-By: Claude Code <noreply@anthropic.com>"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 2: 推送 `_` 前缀过滤(TDD)
|
||||
|
||||
**Files:**
|
||||
- Modify: `app/services/lead_push_service.py:89`
|
||||
- Test: `tests/test_lead_push_service.py`
|
||||
|
||||
- [ ] **Step 1: 失败测试**(追加;参考同文件现有 _build_payload 相关测试的构造方式)
|
||||
|
||||
```python
|
||||
def test_build_payload_filters_internal_underscore_keys():
|
||||
"""内部状态键(_cc_asked/_email_asked)不得泄进推送画像(spec §4.3)。"""
|
||||
# 构造含 _ 前缀键的 profile,走 _build_payload,断言产物无 _ 键
|
||||
# (具体 fixture 构造参考同文件现有 payload 测试;核心断言:)
|
||||
# assert all(not str(k).startswith("_") for k in payload["profile"])
|
||||
```
|
||||
|
||||
(实现者按现有测试风格补全 fixture;断言目标即注释所示。)
|
||||
|
||||
- [ ] **Step 2: 跑红**
|
||||
|
||||
- [ ] **Step 3: 实现**。lead_push_service.py:89 改:
|
||||
|
||||
```python
|
||||
# _ 前缀内部状态(一次性计数等)不进推送画像(spec §4.3——渲染侧
|
||||
# 两处有过滤,推送端曾整体拷贝泄漏)
|
||||
profile_out = {k: v for k, v in (raw_info or {}).items()
|
||||
if not str(k).startswith("_")}
|
||||
```
|
||||
|
||||
- [ ] **Step 4: 全量绿 + commit**
|
||||
|
||||
```bash
|
||||
python -m pytest -q && git add -A && git commit -m "fix: 推送画像过滤_前缀内部键(防CRM泄漏)
|
||||
|
||||
Co-Authored-By: Claude Code <noreply@anthropic.com>"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 3: 状态提醒注入 + 一次性计数(TDD,核心任务)
|
||||
|
||||
**Files:**
|
||||
- Modify: `app/services/conversation.py`(_build_context_reminder 注入区 + stream 方法写回计数)
|
||||
- Test: `tests/test_lead_email_trigger.py`(复用其 manager 流程设施做集成测试)
|
||||
|
||||
**设计要点(实现者必读):**
|
||||
- 新增模块级函数 `_contact_completion_note(raw_info, confirmed, suspect) -> tuple[str, list[str]]`
|
||||
—— 返回 (注入文案, 需落库的计数键列表);纯函数便于单测。
|
||||
- 串行优先级(spec §4.3b):**号码闭环优先**——confirmed 号码均不以 `+`/`00`
|
||||
开头且 `_cc_asked` 未标记 → 注入国家代码追问;仅当号码已带前缀**或**
|
||||
`_cc_asked` 已标记,且 confirmed 含号码渠道不含 email 且 `_email_asked`
|
||||
未标记 → 注入邮箱轻补。一次最多返回一条。
|
||||
- 注入文案(逐字模板,中英双语随 latest_msg 语言选择——参考 `_lang_note_for`
|
||||
的实现形态):
|
||||
- cc 追问英文:`"Could you send the full number with the country code, like +966 5x xxx xxxx? Just so our manager can reach you directly."`(中文版:`"麻烦重发一下带国家代码的完整号码呗(比如 +966 5x xxx xxxx)?客户经理直接加你。"`)
|
||||
- email 轻补英文:`"Got it! And an email address would help too — our manager will send the formal quotation PDF there."`(中文版:`"收到!邮箱也留一个呗?客户经理把正式报价单 PDF 发你邮箱。"`)
|
||||
- 注入格式仿既有 contact_note:`⚠️` 前缀系统指令 + 逐字话术 + "仅此一次,客户不给就不再提"指引
|
||||
- 计数写回:stream 方法在组装 reminder 前调 `_contact_completion_note`,
|
||||
reminder 尾部拼接文案;**流式开始前**把返回的计数键写进
|
||||
`profile.raw_info` 并随本轮事务提交(staticmethod 收到的是内存副本,
|
||||
写回必须在调用方——spec §4.3 实现约束)。
|
||||
|
||||
- [ ] **Step 1: 失败单测**(追加到 test_lead_email_trigger.py,参考其现有 _build_context_reminder 直调风格)
|
||||
|
||||
```python
|
||||
def test_contact_completion_note_serial_priority():
|
||||
"""spec §4.3b: 裸号+无邮箱(主场景)只注入国家代码追问;号码闭环后才可能
|
||||
注入邮箱轻补;各计数消耗后不再注入。"""
|
||||
from app.services.conversation import _contact_completion_note
|
||||
wa_bare = {"whatsapp": "0501234567"} # 裸号(无+/00前缀)
|
||||
wa_full = {"whatsapp": "+966501234567"}
|
||||
wa_email = {"whatsapp": "+966501234567", "email": "a@b.com"}
|
||||
# 1) 裸号+无邮箱 → cc追问,计数["_cc_asked"]
|
||||
note, marks = _contact_completion_note({"联系方式": wa_bare}, wa_bare, {})
|
||||
assert "country code" in note and marks == ["_cc_asked"]
|
||||
# 2) _cc_asked已消耗+仍裸号无邮箱 → 转邮箱轻补
|
||||
note, marks = _contact_completion_note(
|
||||
{"联系方式": wa_bare, "_cc_asked": True}, wa_bare, {})
|
||||
assert "email" in note and marks == ["_email_asked"]
|
||||
# 3) 完整号+无邮箱 → 直接邮箱轻补
|
||||
note, marks = _contact_completion_note({"联系方式": wa_full}, wa_full, {})
|
||||
assert "email" in note and marks == ["_email_asked"]
|
||||
# 4) 双渠道齐 → 不注入
|
||||
note, marks = _contact_completion_note({"联系方式": wa_email}, wa_email, {})
|
||||
assert note == "" and marks == []
|
||||
# 5) email-only → 不注入(不硬要WhatsApp)
|
||||
note, marks = _contact_completion_note(
|
||||
{"联系方式": {"email": "a@b.com"}}, {"email": "a@b.com"}, {})
|
||||
assert note == "" and marks == []
|
||||
# 6) 计数均已消耗 → 不再注入(防死缠)
|
||||
note, marks = _contact_completion_note(
|
||||
{"联系方式": wa_bare, "_cc_asked": True, "_email_asked": True},
|
||||
wa_bare, {})
|
||||
assert note == "" and marks == []
|
||||
```
|
||||
|
||||
- [ ] **Step 2: 跑红**
|
||||
|
||||
- [ ] **Step 3: 实现 `_contact_completion_note`**(纯函数:按上述串行优先级;文案含中英两版,语言随参数选——若直调测试传不了 latest_msg,函数签名加 `lang="en"` 参数,中文由调用方传)
|
||||
|
||||
- [ ] **Step 4: 跑绿单测**
|
||||
|
||||
- [ ] **Step 5: 接线 _build_context_reminder**(有画像路径 parts 拼接处追加 note;空画像早退路径不注入——没联系方式谈不上补全)+ stream 方法流式前写回计数键
|
||||
|
||||
- [ ] **Step 6: 集成测试**(spec §6-6 计数持久化)——用同文件现有 manager 流程测试风格构造两轮对话:第一轮后查 DB `raw_info` 含 `_cc_asked`、第二轮 reminder 不再含追问句
|
||||
|
||||
- [ ] **Step 7: 全量绿 + commit**
|
||||
|
||||
```bash
|
||||
python -m pytest -q && git add -A && git commit -m "feat: 国家代码/邮箱双要状态提醒+一次性计数(串行优先级)
|
||||
|
||||
Co-Authored-By: Claude Code <noreply@anthropic.com>"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 4: system.md 话术升级(7 处,静态断言先行)
|
||||
|
||||
**Files:**
|
||||
- Modify: `app/prompts/system.md`(先通读全文再动笔)
|
||||
- Test: `tests/test_extractor_evaluator_prompts.py` 或新建(静态断言风格)
|
||||
|
||||
- [ ] **Step 1: 写失败静态断言**
|
||||
|
||||
```python
|
||||
def test_system_md_dual_contact_wording():
|
||||
src = open("app/prompts/system.md", encoding="utf-8").read()
|
||||
# 国家代码引导(spec §4.1-1)
|
||||
assert "with the country code" in src
|
||||
# 分步双要+不坚持(§4.1-2/3)
|
||||
assert "formal quotation PDF" in src and "轻补" in src
|
||||
# 追问话术(§4.1-4)
|
||||
assert "send the full number with the country code" in src
|
||||
# 好差示例新增2组(§4.1-5)
|
||||
assert src.count("国家代码") >= 2
|
||||
# 全局禁令改写(§4.1-6——防自锁)
|
||||
assert "已在手的渠道" in src and "缺失渠道" in src
|
||||
# 模板句枚举新增(§4.1-7)
|
||||
assert "国家代码追问句" in src and "邮箱轻补句" in src
|
||||
```
|
||||
|
||||
- [ ] **Step 2: 跑红** → **Step 3: 改 system.md 七处**(话术全文见 spec §4.1;⚠️ 第 6 处"已留资时不用任何索要句"改写是防自锁关键,逐字改为"已留资时不再索要**已在手的渠道**(改确认式);**缺失渠道**可按【联系方式引导策略】轻补一次")→ **Step 4: 跑绿 + 全量绿**(现有 system.md 文案断言若冲突同步更新)
|
||||
|
||||
- [ ] **Step 5: 模板句过墙断言**(spec §6-4,防自锁报警线):
|
||||
|
||||
```python
|
||||
def test_new_templates_pass_reask_wall():
|
||||
"""新模板句逐字原文在所有渠道状态下均不命中 asks_contact(spec §6-4):
|
||||
墙只拦意译索要,不得拦系统注入话术——若扩REASK模式或改措辞致命中,此断言报警。"""
|
||||
from app.services.boundary_filter import asks_contact
|
||||
cc_en = ("Could you send the full number with the country code, "
|
||||
"like +966 5x xxx xxxx? Just so our manager can reach you directly.")
|
||||
em_en = ("Got it! And an email address would help too — our manager "
|
||||
"will send the formal quotation PDF there.")
|
||||
for channels in (None, set(), {"whatsapp"}, {"email"},
|
||||
{"whatsapp", "phone"}, {"whatsapp", "email"}):
|
||||
for s in (cc_en, em_en):
|
||||
assert not asks_contact(s, channels), (s, channels)
|
||||
```
|
||||
|
||||
- [ ] **Step 6: 全量绿 + commit**
|
||||
|
||||
```bash
|
||||
python -m pytest -q && git add -A && git commit -m "feat: system.md双要话术+国家代码引导+全局禁令改写(防自锁)
|
||||
|
||||
Co-Authored-By: Claude Code <noreply@anthropic.com>"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 5: 全量回归 + 生产部署 + 真实 LLM 验收
|
||||
|
||||
- [ ] **Step 1: 全量测试 + 全局残扫**(无意外残留)
|
||||
|
||||
```bash
|
||||
python -m pytest -q
|
||||
```
|
||||
|
||||
- [ ] **Step 2: 部署**(照 CLAUDE.md 发版流程:push → 服务器 pull + build + up → health 验证)
|
||||
|
||||
- [ ] **Step 3: 生产三场景验收**(spec §6-8,用测试号):
|
||||
① 给裸号 `0501234567` → AI 礼貌请其重发完整号(**仅一次**);
|
||||
② 留 WhatsApp → AI 轻补邮箱一次;
|
||||
③ 只留邮箱 → 不被纠缠要 WhatsApp
|
||||
(验收后 admin 删除测试会话;检查推送日志无 `_` 前缀键泄漏)
|
||||
|
||||
- [ ] **Step 4: commit 验收记录**(如有文档更新)
|
||||
|
||||
---
|
||||
|
||||
## 回滚
|
||||
|
||||
任一任务出问题:`git revert <commit>` 重发版(发版流程同上);话术类改动可单独回滚 Task 4 的 commit。
|
||||
Reference in New Issue
Block a user