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

242 lines
7.6 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.
"""
空运查价参数:对照 TMS V2 /quote/v2/query 是否带全。
本文件职责:询价卡片出现前,确认销售侧空运必填能拼出查价正文。
三字码能投影就带;对不上不拦建单、不拦查价。
包装:销售原文非空即收下,禁止白名单拦补问。
禁止:猜没写过的港口;因「对不上字典」跳过 TMS 或对销售说教;改六态。
"""
from __future__ import annotations
import logging
import re
from typing import Any
logger = logging.getLogger(__name__)
# 与 tms-air-exact-seed.json 对齐,只用于把已写地名填成 TMS 要的代码
_AIRPORTS = {
"上海": "SHA",
"浦东": "PVG",
"上海浦东": "PVG",
"虹桥": "SHA",
"上海虹桥": "SHA",
"洛杉矶": "LAX",
"深圳": "SZX",
"广州": "CAN",
"香港": "HKG",
"北京首都": "PEK",
"北京大兴": "PKX",
"珠海": "ZUH",
"克拉克": "CRK",
"南京": "NKG",
"吉隆坡": "KUL",
"北京": "PEK",
}
# TMS air_packaging_type 实测只认 1/2/3。1=全部,这条航线无价;
# 2/3 能出与现网相同的标准价。托盘走 2,纸箱/木箱走 3。
_PACKS = {
"全部": "1",
"托盘": "2",
"卡板": "2",
"散货": "2",
"纸箱": "3",
"木箱": "3",
}
_IATA = re.compile(r"^[A-Za-z]{3}$")
_IATA_PAIR = re.compile(
r"(?<![A-Za-z])([A-Za-z]{3})\s*[-–—/]\s*([A-Za-z]{3})(?![A-Za-z])"
)
# 原话里的 AAA-BBB 不一定是航线,货币/单位不能当成起运目的。
_IATA_PAIR_BLOCK = frozenset(
{
"USD",
"CNY",
"EUR",
"GBP",
"HKD",
"JPY",
"CBM",
"KGS",
"PCS",
"PKG",
"PDF",
"JPG",
"PNG",
"AND",
"THE",
"FOR",
"NOT",
"MAX",
"MIN",
"GMT",
"UTC",
}
)
_NUMBER = re.compile(r"(\d+(?:\.\d+)?)")
def _first_number(raw: str) -> str:
hit = _NUMBER.search((raw or "").strip())
return hit.group(1) if hit else ""
def resolve_airport_code(raw: str) -> str:
"""销售原文是地名或三字码时,给出 TMS originCode/destinationCode。"""
text = (raw or "").strip()
if not text:
return ""
if _IATA.match(text):
return text.upper()
return _AIRPORTS.get(text, "")
def harvest_named_air_route(text: str, facts: dict[str, str] | None = None) -> dict[str, str]:
"""
当前原话里的航线覆盖起运地、目的地。
图上/原话写了 SZX-CGK 这类三字码对,必须保持英文,禁止改写成深圳/雅加达。
「从南京飞吉隆坡」仍按对照表收中文地名。换城市必须丢掉旧三字码。
"""
merged = dict(facts or {})
raw = text or ""
pair = _IATA_PAIR.search(raw)
if pair:
origin_code = pair.group(1).upper()
dest_code = pair.group(2).upper()
blocked = origin_code in _IATA_PAIR_BLOCK or dest_code in _IATA_PAIR_BLOCK
if origin_code != dest_code and not blocked:
merged["起运港"] = origin_code
merged["目的港"] = dest_code
merged.pop("originCode", None)
merged.pop("destinationCode", None)
return merged
names = sorted(_AIRPORTS.keys(), key=len, reverse=True)
for origin in names:
for dest in names:
if origin == dest:
continue
token = f"{origin}飞{dest}"
if token in raw or f"从{token}" in raw:
merged["起运港"] = origin
merged["目的港"] = dest
merged.pop("originCode", None)
merged.pop("destinationCode", None)
return merged
return merged
def resolve_package_type(raw: str) -> str:
"""
只给 TMS 出站用的 packageType,不决定销售是否算已填包装。
认得出代码就用;认不出默认 2(不要默认 1,1=全部在现网航线常无价)。
禁止:因不在对照表而返回空,从而再向销售补问。
"""
text = (raw or "").strip()
if not text:
return "2"
if text in _PACKS:
return _PACKS[text]
if text.isdigit():
return text
return "2"
def assemble_air_query(facts: dict[str, Any] | None) -> dict[str, Any]:
"""
把已采集字段拼成 TMS 空运查价参数。
不齐则 ok=False,missing_display 给销售补问;不建单、不出卡片。
齐了则 facts 带上 originCode/destinationCode/packageType 与纯数字件数毛重体积。
包装只看销售是否说了,散货等原文原样保留。
"""
src = dict(facts or {})
origin = str(src.get("起运港") or "").strip()
dest = str(src.get("目的港") or "").strip()
cargo = str(src.get("品名") or "").strip()
pieces = _first_number(str(src.get("件数") or ""))
weight = _first_number(str(src.get("毛重") or ""))
volume = _first_number(str(src.get("体积") or ""))
pack_raw = str(src.get("包装方式") or src.get("包装类型") or "").strip()
pack_code = resolve_package_type(pack_raw)
quote_date = str(src.get("报价日期") or "").strip()
# 三字码必须按当前地名重算,禁止沿用上一句的 NKG/KUL
origin_code = resolve_airport_code(origin)
dest_code = resolve_airport_code(dest)
missing: list[str] = []
missing_display: list[str] = []
# 销售侧必填只看 8 项是否非空。三字码是出站投影,对不上不拿来再补问起运地/目的地。
if not origin:
missing.append("起运港")
missing_display.append("起运地")
if not dest:
missing.append("目的港")
missing_display.append("目的地")
if not cargo:
missing.append("品名")
missing_display.append("品名")
if not pieces:
missing.append("件数")
missing_display.append("件数")
if not weight:
missing.append("毛重")
missing_display.append("重量")
if not volume:
missing.append("体积")
missing_display.append("体积")
if not pack_raw:
missing.append("包装方式")
missing_display.append("包装类型")
if not quote_date:
missing.append("报价日期")
missing_display.append("报价日期")
ok = not missing
out_facts = dict(src)
if origin:
out_facts["起运港"] = origin
if origin_code:
out_facts["originCode"] = origin_code
else:
out_facts.pop("originCode", None)
if dest:
out_facts["目的港"] = dest
if dest_code:
out_facts["destinationCode"] = dest_code
else:
out_facts.pop("destinationCode", None)
if cargo:
out_facts["品名"] = cargo
# 件数/重量/体积保留销售原文(1.5KG),数字只作出站,禁止回写成 1.5 再被补成 KGS
if str(src.get("件数") or "").strip():
out_facts["件数"] = str(src.get("件数")).strip()
if str(src.get("毛重") or "").strip():
out_facts["毛重"] = str(src.get("毛重")).strip()
if str(src.get("体积") or "").strip():
out_facts["体积"] = str(src.get("体积")).strip()
if pack_raw:
out_facts["包装方式"] = pack_raw
out_facts["packageType"] = pack_code
if quote_date:
out_facts["报价日期"] = quote_date
logger.info(
"tms.air.assemble ok=%s origin=%s dest=%s pack=%s missing=%s",
ok,
origin_code or "-",
dest_code or "-",
pack_raw or pack_code or "-",
missing,
)
return {
"ok": ok,
"missing": missing,
"missing_display": missing_display,
"facts": out_facts,
}