From a0c705683f849491a3ac7779fd8483047792f3be Mon Sep 17 00:00:00 2001 From: jillion <439912596@qq.com> Date: Wed, 14 Oct 2026 20:30:53 +0800 Subject: [PATCH] =?UTF-8?q?=E7=A7=81=E8=81=8A=E6=94=B9=E5=8F=A3=E6=88=90?= =?UTF-8?q?=E6=99=AE=E9=80=9A=E8=B4=A7=E5=90=8E=E4=B8=8D=E5=86=8D=E8=A2=AB?= =?UTF-8?q?=E4=B8=8A=E4=B8=80=E5=8F=A5=E8=BF=9D=E7=A6=81=E8=AF=8D=E6=8B=A6?= =?UTF-8?q?=E4=BD=8F=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 销售说运送衣服、改成衣服时,按新品名继续询价。 Co-authored-by: Cursor --- inquiry-agent/agent/policy/sensitive_cargo.py | 46 +++++++++++++++++-- inquiry-agent/tests/test_sensitive_cargo.py | 14 ++++++ 2 files changed, 55 insertions(+), 5 deletions(-) diff --git a/inquiry-agent/agent/policy/sensitive_cargo.py b/inquiry-agent/agent/policy/sensitive_cargo.py index b3ff907..ac778da 100644 --- a/inquiry-agent/agent/policy/sensitive_cargo.py +++ b/inquiry-agent/agent/policy/sensitive_cargo.py @@ -82,6 +82,13 @@ _LABEL_EDIT = re.compile( r"(货物品名|品名|起运地|起运港|始发站|备注)\s*[::]\s*(\S+)" ) _MULTI_SHEET = re.compile(r"段\s*\d|第\s*[一二三四五六七八九\d]\s*段") +# 口语改口:运送衣服、改成衣服、换成衣服、炸弹换衣服。不认「换一下」「换个港口」。 +_SPOKEN_CARGO = re.compile( + r"(?:运送|托运|发运|改成|改为|换成|换为)\s*([^\s,,。;;、请]{1,16})" + r"|换(?!成|为|个|一下)([^\s,,。;;、请]{1,16})" +) +_CARGO_TAIL = re.compile(r"(请报价|报价|一下|吧|了)+$") +_CARGO_SKIP = frozenset({"一下", "个", "报价", "价", "询价", "港口", "地方"}) # 销售侧说法写回内部键。起运地在主账里仍是起运港。 _WRITE_KEYS = { @@ -132,6 +139,24 @@ def contains_blocked(text: str) -> bool: return False +def spoken_cargo(text: str) -> str: + """ + 从这一句里取出销售刚说的品名。 + + 「运送衣服」「改成衣服」「换成衣服」「炸弹换衣服」都算。 + 取最后一个说法,所以「炸弹换衣服」得到衣服。 + 说出来的还是违禁词,或者只是「换一下」,就当没改口。 + """ + found = "" + for match in _SPOKEN_CARGO.finditer(text or ""): + raw = match.group(1) or match.group(2) or "" + value = _CARGO_TAIL.sub("", raw).strip(",,。;;、 ") + if not value or value in _CARGO_SKIP or contains_blocked(value): + continue + found = value + return found + + def apply_field_edits(text: str, facts: dict[str, Any]) -> dict[str, str]: """ 把当前这句话里的「品名改为…」「起运地改为…」「备注改为…」写进字段。 @@ -150,11 +175,18 @@ def apply_field_edits(text: str, facts: dict[str, Any]) -> dict[str, str]: for key in _WRITE_KEYS.get(label, (label,)): facts[key] = value written[key] = value + cargo = spoken_cargo(text) + if cargo: + for key in ("品名", "货物品名"): + facts[key] = cargo + written[key] = cargo return written def is_cargo_rename(text: str) -> bool: """当前句有没有把品名重新说成一个新值。没说品名的补重量不算改口。""" + if spoken_cargo(text): + return True body = text or "" labels = [label for label, _value in _FIELD_EDIT.findall(body)] if not _MULTI_SHEET.search(body): @@ -167,19 +199,23 @@ def judge(*, text: str, facts: dict[str, Any], hold: str) -> tuple[bool, str]: 决定这一轮私聊能不能继续往建单走。 返回 (是否拦截, 新的未改口原话, 本句写下的字段)。 - 当前句里仍有违禁词:拦住,并把这句记下来。 - 当前句把品名说成普通货、且这句里没有违禁词:以前没落进字段的那句作废。 + 当前句仍只是在说违禁词:拦住,并把这句记下来。 + 当前句已经把品名说成普通货(运送衣服、改成衣服、换成衣服):前面的违禁原话作废。 + 同一句里先提到旧词再改口,按改口后的品名放行。 只补重量、件数:旧原话继续有效。 品名改干净之后,起运地或备注里还留着违禁词,仍然拦截。 会就地更新 facts 里本句写明的品名、起运地、备注。 第三个返回值是本句写下的键,多段用来写回每一段,避免把别的字段盖掉。 """ edits = apply_field_edits(text, facts) + replaced = bool(spoken_cargo(text)) or is_cargo_rename(text) message_hit = contains_blocked(text) - if message_hit: - new_hold = (text or "").strip() or (hold or "") - elif is_cargo_rename(text): + if replaced and not contains_blocked(str(facts.get("品名") or "")): + # 已经说出新的普通货品名。旧词留在这句里只是在改口,不再记住。 new_hold = "" + message_hit = False + elif message_hit: + new_hold = (text or "").strip() or (hold or "") else: new_hold = hold or "" field_text = "\n".join(str((facts or {}).get(key) or "") for key in _SCAN_KEYS) diff --git a/inquiry-agent/tests/test_sensitive_cargo.py b/inquiry-agent/tests/test_sensitive_cargo.py index f280bea..e7379b2 100644 --- a/inquiry-agent/tests/test_sensitive_cargo.py +++ b/inquiry-agent/tests/test_sensitive_cargo.py @@ -157,6 +157,20 @@ class SeaPrivateFlowTests(unittest.TestCase): joined = "\n".join(self.replies) self.assertNotIn(BLOCK_REPLY, joined) + def test_oral_restatement_clears_previous_bomb(self) -> None: + """销售不说「品名改为」,直接重说运送衣服,或说改成衣服。""" + hold = "10月30日,从广州去深圳,运送炸弹,请报价" + facts = {"品名": "炸弹", "起运港": "广州"} + for text in ( + "10月30日,从广州去深圳,运送衣服,请报价", + "炸弹错了,给我改成衣服", + "炸弹换衣服", + ): + blocked, new_hold, edits = judge(text=text, facts=dict(facts), hold=hold) + self.assertFalse(blocked, text) + self.assertEqual(new_hold, "", text) + self.assertEqual(edits.get("品名"), "衣服", text) + def test_weight_alone_does_not_clear(self) -> None: facts = dict(COMPLETE_SEA) facts["品名"] = "弹药"