Files
inquiry_robot/inquiry-agent/agent/llm/extract_text.py
T

136 lines
4.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.
"""
文字询价字段抽取入口。
本文件职责:生产走 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"包装方式|包装类型|货量|货物数量|整柜或拼柜|箱型箱量|报价日期|车型数量|"
r"贸易条款|货好时间|HS编码|HS|是否含油|是否含电|是否含磁)\s*[::]\s*(.+)$"
)
_LABEL_INLINE = re.compile(
r"(起运地|起运港|目的地|目的港|品名|件数|毛重|体积|重量|重量\(KG\)|体积\(CBM\)|"
r"包装方式|包装类型|货量|货物数量|整柜或拼柜|箱型箱量|报价日期|车型数量|"
r"贸易条款|货好时间|HS编码|HS|是否含油|是否含电|是否含磁)\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,
}
def extract_collab_fields(text: str) -> dict[str, str]:
"""
群里协同字段:只收带标签的非空值。
闲聊、报价金额、附件本轮不进这里。HS 折成 HS编码。
"""
from agent.schema.field_validate import SEA_COLLAB_FIELDS, normalize_facts
labeled = normalize_facts(parse_labeled_facts(text))
out: dict[str, str] = {}
for key in SEA_COLLAB_FIELDS:
val = (labeled.get(key) or "").strip()
if val:
out[key] = val
return out