fix: P4'——等待态画像目标国家新现作为报国确定性信号(不赌extractor分类)
缺陷(复验 2/2 稳定复现): 同轮给号+报国首问轮丢弃(P2,设计内)→次轮再报国 extractor 漏填 cc_reply(画像已记目标国家,对重复陈述倾向不分类)→报国未 触发重算→客户 yes→错国号(+34)被 confirm 零标注出站。 - conversation.py 轮末钩子: cc_reply 未消费时,等待态(cc_due)且非首问轮, 画像"目标国家"新现或值变化(与轮前快照比对,占位集不算)→ name_to_region 转 ISO 走 transit country 分支(与 cc_reply.country 同路径);国名不识别 →忽略;首问轮仍不消费(P2 语义保持)。轮前快照在 stream_process_message 本轮写回前取值传入。 - extractor.md: cc_reply 段补"重复报国仍要分类"强化句(与 Python 侧信号 双保险——画像已记场景 extractor 漏填时由确定性信号兜底)。 - 测试: 主场景重算+20/customer_stated/attempts 消费;对照组: 首问轮不消费+ 同值不变不重复触发(合并流)、占位值忽略、值变化触发、国名不识别忽略。 全量 320 passed(基线 315+5)。 Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
@@ -1976,3 +1976,109 @@ def test_cross_session_dedup_merges_cc_e164_canon(async_db, monkeypatch):
|
||||
asyncio.run(_send(m2, c2.session_id, "my whatsapp is +966501234567"))
|
||||
assert [s[0] for s in sent2] == ["nudge"] # e164 形态同号 → 仍判回访
|
||||
asyncio.run(m2.db.close())
|
||||
|
||||
|
||||
# ---- P4'(2026-10-08 复验): 报国漏填→错国号零标注——画像目标国家确定性信号 ----
|
||||
# 缺陷链: 同轮"给号+报国"首问轮丢弃(P2,设计内)→次轮再报国 extractor 漏填
|
||||
# cc_reply(画像已记目标国家,对重复陈述倾向不分类)→报国未触发重算→客户
|
||||
# "yes"→错国号(+34)被 confirm 零标注出站。修复: 轮末钩子读画像"目标国家"
|
||||
# 新现/值变化作确定性信号(不赌 extractor 分类),走 transit country 同路径。
|
||||
def _wa_of(manager, conv_id):
|
||||
return ((asyncio.run(manager._get_profile(conv_id))
|
||||
.raw_info or {}).get("_cc") or {}).get("whatsapp")
|
||||
|
||||
|
||||
def test_cc_profile_country_signal_recalc(async_db, monkeypatch):
|
||||
"""P4' 主场景: 等待态(asked 已标,非首问)轮 extractor 漏填 cc_reply,但
|
||||
画像"目标国家"本轮新现(轮前无此键)→ 确定性报国信号: name_to_region 转
|
||||
ISO 走 transit country 分支——e164 重算 +20 域、source=customer_stated、
|
||||
attempts 消费(次轮回显新号待确认,错号不再可能被 confirm)。"""
|
||||
_sa(monkeypatch)
|
||||
ai = _RecAI(extract=[{}, {"目标国家": "埃及"}])
|
||||
(manager, conv), _ = _setup(async_db, monkeypatch, ai=ai,
|
||||
source=ConversationSource.standalone)
|
||||
asyncio.run(_send(manager, conv.session_id, "my whatsapp is 0501234567"))
|
||||
wa = _wa_of(manager, conv.id)
|
||||
assert wa.get("asked") is True and wa["attempts"] == 0 # 前提: 等待态非首问
|
||||
asyncio.run(_send(manager, conv.session_id, "no, I am in Egypt"))
|
||||
wa = _wa_of(manager, conv.id)
|
||||
assert wa["e164"] == "+20501234567" # 按报国(EG=+20)重算
|
||||
assert wa["source"] == "customer_stated"
|
||||
assert wa["attempts"] == 1 # 信号消费一轮
|
||||
assert wa["confirmed"] is False # 仍待确认(次轮回显新号)
|
||||
asyncio.run(manager.db.close())
|
||||
|
||||
|
||||
def test_cc_profile_country_signal_first_ask_free_and_no_retrigger(
|
||||
async_db, monkeypatch):
|
||||
"""对照组①: 首问轮(asked 未标)同信号新现 → 不消费(P2 语义保持——同轮
|
||||
给号+报国的首问轮丢弃,靠次轮信号触发);对照组④: 轮前已有"目标国家:
|
||||
埃及"且本轮同值不变 → 不重复触发(去抖: 值不变无新信息),该轮按无回应
|
||||
语义消耗 attempts。"""
|
||||
_sa(monkeypatch)
|
||||
ai = _RecAI(extract=[{"目标国家": "埃及"}, {"目标国家": "埃及"}])
|
||||
(manager, conv), _ = _setup(async_db, monkeypatch, ai=ai,
|
||||
source=ConversationSource.standalone)
|
||||
# 轮1: 同轮给号+报国——首问轮信号新现但不消费
|
||||
asyncio.run(_send(manager, conv.session_id,
|
||||
"my whatsapp is 0501234567, I am in Egypt"))
|
||||
wa = _wa_of(manager, conv.id)
|
||||
assert wa["attempts"] == 0 and wa["source"] == "ip_proposed"
|
||||
assert wa["e164"] == "+966501234567" # 首问轮未按画像重算
|
||||
profile = asyncio.run(manager._get_profile(conv.id))
|
||||
assert (profile.raw_info or {}).get("目标国家") == "埃及" # 画像字段照常写入
|
||||
# 轮2: 客户再报国,extractor 同值重复输出(画像已记)→ 值不变不触发
|
||||
asyncio.run(_send(manager, conv.session_id, "no, I am in Egypt"))
|
||||
wa = _wa_of(manager, conv.id)
|
||||
assert wa["attempts"] == 1 # 无回应语义消耗(非信号消费)
|
||||
assert wa["e164"] == "+966501234567" and wa["source"] == "ip_proposed"
|
||||
asyncio.run(manager.db.close())
|
||||
|
||||
|
||||
def test_cc_profile_country_signal_placeholder_ignored(async_db, monkeypatch):
|
||||
"""对照组②: 占位值("无")不构成信号——extractor 违约把指令标记输出成
|
||||
字段值(extractor.md 明令禁止但 GLM 不保证)时,不得消费为报国。"""
|
||||
_sa(monkeypatch)
|
||||
ai = _RecAI(extract=[{}, {"目标国家": "无"}])
|
||||
(manager, conv), _ = _setup(async_db, monkeypatch, ai=ai,
|
||||
source=ConversationSource.standalone)
|
||||
asyncio.run(_send(manager, conv.session_id, "my whatsapp is 0501234567"))
|
||||
asyncio.run(_send(manager, conv.session_id, "no, I am in Egypt"))
|
||||
wa = _wa_of(manager, conv.id)
|
||||
assert wa["e164"] == "+966501234567" and wa["source"] == "ip_proposed"
|
||||
assert wa["attempts"] == 1 # 无回应语义,信号被忽略
|
||||
asyncio.run(manager.db.close())
|
||||
|
||||
|
||||
def test_cc_profile_country_signal_value_change_recalc(async_db, monkeypatch):
|
||||
"""值变化分支: 轮前"西班牙"、本轮变"埃及"→ 与新现同权触发重算
|
||||
(判据是"新出现或值变化",换国是更强的报国信号)。"""
|
||||
_sa(monkeypatch)
|
||||
ai = _RecAI(extract=[{"目标国家": "西班牙"}, {"目标国家": "埃及"}])
|
||||
(manager, conv), _ = _setup(async_db, monkeypatch, ai=ai,
|
||||
source=ConversationSource.standalone)
|
||||
asyncio.run(_send(manager, conv.session_id,
|
||||
"my whatsapp is 0501234567, I am in Spain"))
|
||||
wa = _wa_of(manager, conv.id)
|
||||
assert wa["e164"] == "+966501234567" # 首问轮不消费(轮1 已记西班牙)
|
||||
asyncio.run(_send(manager, conv.session_id, "no, I am in Egypt"))
|
||||
wa = _wa_of(manager, conv.id)
|
||||
assert wa["e164"] == "+20501234567" # 值变化 → 重算
|
||||
assert wa["source"] == "customer_stated" and wa["attempts"] == 1
|
||||
asyncio.run(manager.db.close())
|
||||
|
||||
|
||||
def test_cc_profile_country_signal_unknown_country_ignored(
|
||||
async_db, monkeypatch):
|
||||
"""NAME_TO_REGION 不命中 → 忽略(不重算不撒谎,与 cc_reply 国名不识别
|
||||
同口径): 信号不触发重算,该轮按无回应语义消耗。"""
|
||||
_sa(monkeypatch)
|
||||
ai = _RecAI(extract=[{}, {"目标国家": "Atlantis"}])
|
||||
(manager, conv), _ = _setup(async_db, monkeypatch, ai=ai,
|
||||
source=ConversationSource.standalone)
|
||||
asyncio.run(_send(manager, conv.session_id, "my whatsapp is 0501234567"))
|
||||
asyncio.run(_send(manager, conv.session_id, "no, I live in Atlantis"))
|
||||
wa = _wa_of(manager, conv.id)
|
||||
assert wa["e164"] == "+966501234567" and wa["source"] == "ip_proposed"
|
||||
assert wa["attempts"] == 1 # 无回应语义,信号被忽略
|
||||
asyncio.run(manager.db.close())
|
||||
|
||||
Reference in New Issue
Block a user