确定运输方式前的多句询价带到所选方式并按该方式抽字段;成交已定后放舱不再推跟进,未成交原因不再被当成新询价。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-10-03 09:54:33 +08:00
co-authored by Cursor
parent d989154485
commit 0a14b4cc15
18 changed files with 620 additions and 28 deletions
@@ -516,6 +516,36 @@ def harvest_oral_collab(text: str) -> dict[str, str]:
return out
# 原话里单独出现才收下。举例行「(如:DDP、DAP、CIF、CIP)」整行跳过,避免示例被当成已填。
_TRADE_TERM_CODES = ("DDP", "DAP", "DDU", "CIF", "CIP", "CPT", "CFR", "FOB", "FCA", "EXW")
_TRADE_TERM_RE = re.compile(
r"(?i)(?<![A-Za-z])(" + "|".join(_TRADE_TERM_CODES) + r")(?![A-Za-z])"
)
_TRADE_TERM_EXAMPLE_LINE = re.compile(r"^\s*[((]如\s*[::]")
def harvest_spoken_trade_term(text: str) -> dict[str, str]:
"""
原话里单独写出的 DAP、DDP、CIF 等记成贸易条款。
只认这一组术语本身,不从「门到门」猜。同一句出现两个不同术语时不选。
补问举例行整行不看。已有「贸易条款」标签值的,由调用方决定不覆盖。
"""
kept: list[str] = []
for line in (text or "").splitlines() or [""]:
if _TRADE_TERM_EXAMPLE_LINE.match((line or "").strip()):
continue
kept.append(line)
found: list[str] = []
for match in _TRADE_TERM_RE.finditer("\n".join(kept)):
code = match.group(1).upper()
if code not in found:
found.append(code)
if len(found) != 1:
return {}
return {"贸易条款": found[0]}
# 箱型原文:40HQ*1、1x40HQ、40尺高柜。货物数量不能收这种串。
_CONTAINER_SPEC = re.compile(
r"(?i)(20|40|45)\s*['\"′尺]?\s*(HQ|GP|NOR|RH|HC|高柜|平柜|柜)"
@@ -549,6 +579,39 @@ def project_sea_cargo_qty(facts: dict[str, str] | None) -> dict[str, str]:
return merged
# 选定方式后,不属于该方式首次询价的键拿掉。海运件数先投影成货量再删。
_SEA_DROP_AFTER_PROJECT = ("件数", "毛重", "体积", "包装方式", "包装类型")
_LAND_DROP_FOREIGN = ("件数", "货量", "货物数量", "整柜或拼柜", "箱型箱量", "贸易条款")
_AIR_DROP_FOREIGN = ("货量", "货物数量", "整柜或拼柜", "箱型箱量")
def shape_facts_for_chosen_mode(facts: dict[str, str] | None, business_line: str) -> dict[str, str]:
"""
运输方式刚定下来时,按该方式的询价字段收口。
海运留下货物数量、整柜或拼柜、箱型箱量、贸易条款、运输分类;件数里的数量先写入货量。
陆运留下运输类型、线路类别、运输分类和重量体积或车型,不留海运箱型、空运件数。
空运留下件数、毛重、体积、包装方式,不留海运箱型。
不猜新值,只删不属于本方式的键。返回新 dict。
"""
line = (business_line or "").strip().upper()
merged = {str(k): str(v).strip() for k, v in dict(facts or {}).items() if str(v).strip()}
if line == "SEA":
merged = project_sea_cargo_qty(merged)
for key in _SEA_DROP_AFTER_PROJECT:
merged.pop(key, None)
return merged
if line == "LAND":
for key in _LAND_DROP_FOREIGN:
merged.pop(key, None)
return merged
if line == "AIR":
for key in _AIR_DROP_FOREIGN:
merged.pop(key, None)
return merged
return merged
def apply_quote_date_default(facts: dict[str, str]) -> dict[str, str]:
"""销售未提供报价日期时填当天;已有则不覆盖。"""
merged = dict(facts)
+41
View File
@@ -144,6 +144,33 @@ def harvest_sea_trade_terms(text: str, facts: dict[str, str] | None = None) -> d
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]:
"""
丢掉冒充贸易条款的「门到门」。
@@ -169,6 +196,20 @@ def drop_invented_door_trade_terms(text: str, facts: dict[str, str] | None = Non
return merged
def apply_spoken_sea_trade_reply(text: str, facts: dict[str, str] | None = None) -> dict[str, str]:
"""
先丢掉模型冒充的门到门,再收下销售整句回复的门到门或港到港。
测服 2026-10-03:补问举例是门到门、港到港,销售回「门到门」仍被当成没填,并提示参数提取不全。
原话没出现这两个词时,旧草稿里的门到门继续丢掉。返回新 dict。
"""
merged = drop_invented_door_trade_terms(text, facts)
spoken = short_sea_trade_reply(text)
if spoken:
merged["贸易条款"] = spoken
return merged
def sea_class_prompt() -> str:
"""缺运输分类时只列两项,不再追问「其他是什么」。"""
return "运输分类(必填):请回复 Port to Port 或 其他。"