Files
inquiry_robot/inquiry-agent/agent/schema/sea_options.py
T

253 lines
9.7 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""
海运运输分类:只认 Port to Port、其他。
本文件职责:从原话收成标准词、生成补问清单;贸易条款标签行原样优先。
禁止:发明第三项;把分类带去 TMS;改六态。
线程:纯函数,可在 Worker 槽调用。
"""
from __future__ import annotations
import re
SEA_CLASSES = ("Port to Port", "其他")
# 补问清单下一行「(如:Port to Port、其他)」只是举例,不能当成销售已选。
_CLARIFY_EXAMPLE_LINE = re.compile(r"^\s*[((]如\s*[::]")
# 标签行优先:避免「贸易条款:Port to Port」整句扫描时把运输分类也收成 Port to Port。
_LABELED_SEA_CLASS = re.compile(
r"(?im)^\s*运输分类(?:(必填)|\(必填\))?\s*[::]\s*(.+?)\s*$"
)
_LABELED_TRADE_TERMS = re.compile(
r"(?im)^\s*贸易条款(?:(必填)|\(必填\)|(非必填)|\(非必填\))?\s*[::]\s*(.+?)\s*$"
)
_TRADE_TERMS_LINE = re.compile(r"(?im)^\s*贸易条款")
# 长词优先,避免 P2P 误伤其它词;门到门不在名单里。
_ALIASES = (
("port to port", "Port to Port"),
("港到港", "Port to Port"),
("p2p", "Port to Port"),
("其它", "其他"),
("其他", "其他"),
)
def canonicalize_sea_class(raw: str) -> str:
"""
把销售原话收成 Port to Port 或 其他;对不上返回空串。
「门到门」不算。先匹配更长别名。副作用:无。
"""
text = (raw or "").strip()
if not text:
return ""
# 企微常把空格发成不换行空格,Port to Port 对不上普通空格就会整段丢掉。
folded = re.sub(r"\s+", " ", text).strip()
lowered = folded.lower()
squashed = re.sub(r"\s+", "", lowered)
if "port to port" in lowered or "porttoport" in squashed:
return "Port to Port"
for needle, canon in _ALIASES:
if needle == "port to port":
continue
if needle in lowered or needle in text or needle in squashed:
return canon
stripped = text.replace("运输分类", "").replace(":", "").replace(":", "").strip()
if stripped in SEA_CLASSES:
return stripped
return ""
def _without_clarify_examples(text: str) -> str:
"""丢掉补问举例行。销售只在冒号后作答,「(如:…)」整行不参与判断。"""
return "\n".join(
line for line in (text or "").splitlines() if not _CLARIFY_EXAMPLE_LINE.match(line)
)
def _without_trade_terms_lines(text: str) -> str:
"""整句扫运输分类时丢掉「贸易条款:…」行,避免条款里的 Port to Port 冒充分类。"""
return "\n".join(
line for line in (text or "").splitlines() if not _TRADE_TERMS_LINE.match(line.strip())
)
def _labeled_value(text: str, pattern: re.Pattern[str]) -> str:
"""取原文最后一次标签行冒号后的值;没有则空串。"""
last = ""
for hit in pattern.finditer(_without_clarify_examples(text or "")):
last = (hit.group(1) or "").strip()
return last
def harvest_sea_class(text: str, current: dict[str, str] | None = None) -> dict[str, str]:
"""
当前句能认则覆盖;认不出则保留已有标准值。
优先「运输分类:」标签行(WO202609280012:条款写 Port to Port、分类写其它)。
整句扫描时排除贸易条款行,避免条款里的 Port to Port 盖掉「其它」。
「其它」与「其他」都收成「其他」。非法旧值会丢掉,避免「门到门」一直占着必填位。
"""
merged = dict(current or {})
cleaned = _without_clarify_examples(text or "")
labeled = _labeled_value(cleaned, _LABELED_SEA_CLASS)
if labeled:
found_labeled = canonicalize_sea_class(labeled)
if found_labeled:
merged["运输分类"] = found_labeled
return merged
# 整句扫描:不含贸易条款行
found = canonicalize_sea_class(_without_trade_terms_lines(cleaned))
if found:
merged["运输分类"] = found
return merged
kept = canonicalize_sea_class(str(merged.get("运输分类") or ""))
if kept:
merged["运输分类"] = kept
return merged
from agent.schema.land_options import canonicalize_land_class
# 多段陆运补「急件」时,不能按海运名单把已收的陆运分类清掉。
land_kept = canonicalize_land_class(str(merged.get("运输分类") or ""))
if land_kept:
merged["运输分类"] = land_kept
return merged
merged.pop("运输分类", None)
return merged
# 运输分类用语,不能冒充贸易条款。销售原话写了「贸易条款」才保留。
_DOOR_TRADE_TERMS = frozenset(
{
"门到门",
"door to door",
"door-to-door",
"doortodoor",
"d2d",
}
)
def harvest_sea_trade_terms(text: str, facts: dict[str, str] | None = None) -> dict[str, str]:
"""
海运贸易条款:标签行冒号后原样优先。
WO202609280012:销售写「贸易条款:Port to Port」,模型却收成「门到门」。
有标签行则强制用标签值;门到门是否丢掉仍交给 drop_invented_door_trade_terms。
"""
merged = dict(facts or {})
labeled = _labeled_value(text or "", _LABELED_TRADE_TERMS)
if labeled:
merged["贸易条款"] = labeled
return merged
# 补问举例让销售直接回这两个词。整句只有其中一个,或长句里写了其中一个,都记成贸易条款。
_SHORT_SEA_TRADE_REPLY = re.compile(
r"^\s*(门到门|港到港|door\s*to\s*door|door-to-door|d2d)\s*[.。!!??]?\s*$",
re.IGNORECASE,
)
def short_sea_trade_reply(text: str) -> str:
"""
销售整句只回门到门或港到港时,收成贸易条款。
举例行不算。长句里没写这两个词时,不能从这里编出一个条款。
返回「门到门」或「港到港」;对不上返回空串。
"""
cleaned = _without_clarify_examples(text or "").strip()
hit = _SHORT_SEA_TRADE_REPLY.match(cleaned)
if not hit:
return ""
raw = (hit.group(1) or "").strip()
folded = re.sub(r"[\s-]+", "", raw).lower()
if raw == "港到港":
return "港到港"
if raw == "门到门" or folded in {"doortodoor", "d2d"}:
return "门到门"
return ""
def drop_invented_door_trade_terms(text: str, facts: dict[str, str] | None = None) -> dict[str, str]:
"""
丢掉冒充贸易条款的「门到门」。
测服 WO202609200013:原话没写贸易条款,确认卡却出现「贸易条款:门到门」。
门到门只是运输分类说法,不是 FOB/CIF 这类条款;原话没有「贸易条款」四字时不准收下。
销售明确写了「贸易条款:门到门」则保留原文,不改六态。
若标签行写的是 Port to Port / 港到港等,禁止再用模型的「门到门」盖掉。
"""
merged = harvest_sea_trade_terms(text, facts)
trade = str(merged.get("贸易条款") or "").strip()
if not trade:
return merged
labeled = _labeled_value(text or "", _LABELED_TRADE_TERMS)
if labeled:
# 标签已写死条款:以标签为准(含 Port to Port)
merged["贸易条款"] = labeled.strip()
return merged
if "贸易条款" in (text or ""):
return merged
if trade.lower().replace(" ", "") in {x.replace(" ", "") for x in _DOOR_TRADE_TERMS} or trade in _DOOR_TRADE_TERMS:
merged.pop("贸易条款", None)
return merged
def _spoken_door_in_text(text: str) -> str:
"""
长句里写了「门到门」或「港到港」就是贸易条款。
举例行「(如:门到门、港到港)」先丢掉,不能当成已填。
原话没有这两个词时返回空,禁止把模型猜的条款写回去。
同一句里两个词都有时,取靠后的那个。
「Port to Port」仍是运输分类,不在这里改写成贸易条款。
「广州港到巴生港」不含「港到港」这三个连续字,不能误收。
"""
cleaned = _without_clarify_examples(text or "")
found: list[tuple[int, str]] = []
for word in ("门到门", "港到港"):
idx = cleaned.rfind(word)
if idx >= 0:
found.append((idx, word))
door = re.search(r"door\s*[- ]?to\s*[- ]?door", cleaned, re.I)
if door:
found.append((door.start(), "门到门"))
if not found:
return ""
found.sort(key=lambda item: item[0])
return found[-1][1]
def apply_spoken_sea_trade_reply(text: str, facts: dict[str, str] | None = None) -> dict[str, str]:
"""
先丢掉模型冒充的门到门,再按原话收回贸易条款。
整句只回「门到门」或「港到港」要收下。长句里写了其中一个(例如先报货、再回「2」选海运)
也是贸易条款,不能因为原话没有「贸易条款」四个字就丢掉。
原话没出现「门到门」时,旧草稿里的门到门继续丢掉(WO202609200013)。
原话没出现「港到港」时,模型填的港到港同样丢掉。
销售已写「贸易条款:…」标签时以标签为准,不用句中的词覆盖。返回新 dict。
"""
merged = drop_invented_door_trade_terms(text, facts)
if _labeled_value(text or "", _LABELED_TRADE_TERMS):
return merged
spoken = short_sea_trade_reply(text)
if spoken:
merged["贸易条款"] = spoken
return merged
door = _spoken_door_in_text(text)
if door:
merged["贸易条款"] = door
return merged
if str(merged.get("贸易条款") or "").strip() == "港到港":
merged.pop("贸易条款", None)
return merged
def sea_class_prompt() -> str:
"""缺运输分类时只列两项,不再追问「其他是什么」。"""
return "运输分类(必填):请回复 Port to Port 或 其他。"