落地私聊空运询价与TMS报价回填,按后台模板出单。

查到价后把币种、单位、数量、单价、备注写入报价单,避免销售只看到光秃秃的金额。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-09-14 20:25:35 +08:00
co-authored by Cursor
parent cea3a6e51e
commit 99da1faf30
78 changed files with 8838 additions and 321 deletions
+116
View File
@@ -0,0 +1,116 @@
"""
文字询价字段抽取入口。
本文件职责:生产走 DeepSeek B;单测可注入 facts;无网时只认「键:值」结构化行,不猜港口。
禁止:用正则把「上海到洛杉矶」拆成起运目的;禁止改六态。
"""
from __future__ import annotations
import logging
import re
from typing import Any, Optional
logger = logging.getLogger(__name__)
_MODE_WORDS = (
("空运", "AIR"),
("海运", "SEA"),
("陆运", "LAND"),
)
# 仅「键:值」一行一条,键必须是合同字段或展示名
_LABEL_LINE = re.compile(
r"^(起运地|起运港|目的地|目的港|品名|件数|毛重|体积|重量|重量\(KG\)|体积\(CBM\)|"
r"包装方式|包装类型|货量|整柜或拼柜|箱型箱量|报价日期|车型数量)\s*[::]\s*(.+)$"
)
_LABEL_INLINE = re.compile(
r"(起运地|起运港|目的地|目的港|品名|件数|毛重|体积|重量|重量\(KG\)|体积\(CBM\)|"
r"包装方式|包装类型|货量|整柜或拼柜|箱型箱量|报价日期|车型数量)\s*[::]\s*(\S+)"
)
def detect_transport_mode(text: str) -> str:
"""
运输方式:空运/海运/陆运,或空运口语「飞」(南京飞吉隆坡)。
写了海运/陆运以书面为准。只说飞就是空运,补问件数时不得再问运输方式。
"""
raw = text or ""
found = [code for word, code in _MODE_WORDS if word in raw]
if len(found) == 1:
return found[0]
if len(found) > 1:
return ""
if "飞" in raw:
return "AIR"
return ""
def parse_labeled_facts(text: str) -> dict[str, str]:
"""只采集「字段名:值」,整行优先;同一句里的标签也收。不猜港口。"""
facts: dict[str, str] = {}
raw = text or ""
for line in raw.splitlines():
m = _LABEL_LINE.match(line.strip())
if m:
facts[m.group(1)] = m.group(2).strip()
if facts:
return facts
for m in _LABEL_INLINE.finditer(raw):
facts[m.group(1)] = m.group(2).strip()
return facts
def extract_inquiry_snapshot(
text: str,
*,
injected_facts: Optional[dict[str, Any]] = None,
injected_mode: str = "",
b_result: Optional[dict[str, Any]] = None,
allow_b: bool = False,
chat_fn: Optional[Any] = None,
) -> dict[str, Any]:
"""
抽出业务线 + 字段快照。
优先 injected_*(单测);其次合并标签行与 DeepSeek B(口语)。
标签行覆盖 B 的同名字段。B 未出现的字段保持空,不本地猜港口。
件数/毛重/体积若原话已带 TMS 单位(件/KGS/CBM),空字段可机读补上。
"""
from agent.schema.field_validate import harvest_oral_measures, normalize_facts
if injected_facts is not None:
mode = (injected_mode or detect_transport_mode(text) or "").upper()
return {
"business_line": mode,
"land_subtype": "",
"facts": harvest_oral_measures(text, normalize_facts(dict(injected_facts))),
"source": "injected",
}
mode = detect_transport_mode(text)
facts = parse_labeled_facts(text)
source = "labeled_or_mode"
b_payload = b_result
if b_payload is None and (allow_b or chat_fn is not None):
from agent.llm.mode_extract_fields import invoke_extract_fields
b_payload = invoke_extract_fields(
text, allow_network=allow_b, chat_fn=chat_fn
)
if b_payload and b_payload.get("ok"):
merged = dict(b_payload.get("facts") or {})
merged.update(facts)
facts = merged
mode = mode or str(b_payload.get("business_line") or "")
source = "deepseek_b"
facts = harvest_oral_measures(text, normalize_facts(facts))
if not mode and facts.get("起运港") and facts.get("目的港") and "飞" in (text or ""):
mode = "AIR"
logger.info("extract.snapshot mode=%s keys=%s src=%s", mode or "-", sorted(facts), source)
return {
"business_line": mode,
"land_subtype": (b_payload or {}).get("land_subtype") or "",
"facts": facts,
"source": source,
}