""" 海运运输分类:只认 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 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 sea_class_prompt() -> str: """缺运输分类时只列两项,不再追问「其他是什么」。""" return "运输分类(必填):请回复 Port to Port 或 其他。"