Files
inquiry_robot/inquiry-agent/agent/policy/air_quote_copy.py
T
jillion886andCursor 9ffba70741 陆运补问把货物品名放进选项清单,并按销售原文回显已填项。
空着的运输分类不再被菜单第一项认成已填。空运报价改为货物信息加价格方案,锁舱成功回执补起运地、目的地、航司和航班日期。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-10-14 17:39:54 +08:00

363 lines
13 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 报价正文:私聊价格明细,以及协同群有价时的货物信息和 TMS 段。
本文件职责:只排销售和群里看得见的文字。费用名、单价、单位、备注、合计、有效期、航司、计费重、货型都取 TMS 原文。
起运地、品名、件数等仍用工单上销售已填的内容。
禁止:改按钮、改卡片、改海运陆运、没查到价时另写一套、本地编备注或货型。
"""
from __future__ import annotations
import json
from typing import Any
# 货型没有单独合同字段。TMS 若带回下列键,原样展示;都没有就「-」。
_CARGO_TYPE_KEYS = (
"cargoType",
"goodsType",
"cargoRatio",
"weightVolumeRatio",
"volumeWeightRatio",
"heavyBubbleRatio",
"bubbleRatio",
"densityRatio",
"cargoShape",
)
_REMARK_KEYS = (
"remark",
"remarks",
"note",
"specialRemarks",
"special_remarks",
"quoteRemark",
"description",
)
def air_private_quote_text(
*,
work_order_no: str,
facts: dict[str, str] | None,
quote: dict[str, Any] | None,
) -> str:
"""
私聊查到一条空运 TMS 报价后的完整文字。
先货物信息,再「价格方案如下」。协同说明和按钮不在这里,仍由后面的卡片发。
副作用:无。线程:调用方线程。
"""
lines = air_cargo_lines(work_order_no=work_order_no, facts=facts, quote=quote)
lines.append("")
lines.append("价格方案如下:")
lines.extend(air_price_lines(quote, facts))
return "\n".join(lines)
def air_cargo_lines(
*,
work_order_no: str,
facts: dict[str, str] | None,
quote: dict[str, Any] | None,
) -> list[str]:
"""
货物信息。工单字段与现在确认工单同一套单位;航司、计费重、货型只认 TMS。
报价日期没填写成「-」,不写「非必填」。增值服务有内容才多一行。
"""
from agent.policy.inquiry_copy import _with_unit
src = dict(facts or {})
origin = (src.get("起运港") or src.get("起运地") or "-").strip() or "-"
dest = (src.get("目的港") or src.get("目的地") or "-").strip() or "-"
cargo = (src.get("品名") or "-").strip() or "-"
pack = (src.get("包装方式") or src.get("包装类型") or "-").strip() or "-"
quote_date = (src.get("报价日期") or "").strip()
if quote_date in {"", "-", "—", "-"}:
quote_date = "-"
lines = [
f"工单号:{(work_order_no or '').strip()}",
"运输方式:空运",
f"起运地:{origin}",
f"目的地:{dest}",
f"航司:{_airline_display(quote, src)}",
f"品名:{cargo}",
f"件数:{_with_unit(str(src.get('件数') or ''), '件')}",
f"重量(KG):{_with_unit(str(src.get('毛重') or ''), 'KGS')}",
f"体积(CBM):{_with_unit(str(src.get('体积') or ''), 'CBM')}",
f"包装类型:{pack}",
]
added = (src.get("added_services") or src.get("增值服务") or "").strip()
if added and added not in {"-", "—", "-"}:
lines.append(f"增值服务:{added}")
lines.append(f"计费重:{_chargeable_display(quote, src)}")
lines.append(f"货型:{_cargo_type_display(quote, src)}")
lines.append(f"报价日期:{quote_date}")
return lines
def air_price_lines(
quote: dict[str, Any] | None,
facts: dict[str, str] | None = None,
) -> list[str]:
"""
TMS 价格段:费用行、备注、费用合计、报价有效期。
有单价且有计价单位:单价CNY11/KG。按票:CNY300/票。只有金额:CNY 100.0。
备注每次都有,TMS 没带回就「-」。不写时效。
"""
from agent.policy.inquiry_copy import money_text, tms_valid_until_display
src = dict(quote or {})
header_ccy = str(src.get("currency") or src.get("currencyCode") or "").strip()
lines = [_format_fee(row, header_ccy) for row in _fee_rows(src, facts)]
lines = [line for line in lines if line]
lines.append(f"备注:{_remark_display(src, facts)}")
total = money_text(str(src.get("total") or "-").strip() or "-", header_ccy)
lines.append(f"费用合计:{total}")
lines.append(f"报价有效期:{tms_valid_until_display(src)}")
return lines
def _airline_display(quote: dict[str, Any] | None, facts: dict[str, str]) -> str:
"""航司优先代码(TH)。只有中文名时用中文名。没有就「-」。"""
option = _option(quote, facts)
blobs = [option, dict(quote or {})]
for blob in blobs:
for key in ("airlineCode", "carrierCode"):
text = str(blob.get(key) or "").strip()
if text:
return text
for blob in blobs:
raw = str(blob.get("airline") or blob.get("carrier") or "").strip()
if raw and _looks_like_code(raw):
return raw
for blob in blobs:
name = str(blob.get("airlineName") or "").strip()
if name:
return name
raw = str(blob.get("airline") or blob.get("carrier") or "").strip()
if raw:
return raw
return "-"
def _looks_like_code(raw: str) -> bool:
"""两到四位字母数字才当航司代码,避免把中文名收成代码。"""
token = raw.replace(" ", "")
return token.isascii() and token.isalnum() and 2 <= len(token) <= 4
def _chargeable_display(quote: dict[str, Any] | None, facts: dict[str, str]) -> str:
"""计费重。纯数字补 KG;TMS 已带单位则原样。没有就「-」。"""
option = _option(quote, facts)
for blob in (option, dict(quote or {})):
raw = blob.get("chargeableWeightKg")
if raw in (None, "", "-"):
raw = blob.get("chargeable_weight_kg") or blob.get("chargeableWeight")
text = str(raw or "").strip()
if not text or text in {"-", "—", "-"}:
continue
upper = text.upper()
if any(upper.endswith(unit) for unit in ("KG", "KGS", "公斤", "千克")):
return text
return f"{_compact_number(text)}KG"
return "-"
def _cargo_type_display(quote: dict[str, Any] | None, facts: dict[str, str]) -> str:
"""货型只展示 TMS 带回的原文,例如 1:212。没有就「-」。"""
option = _option(quote, facts)
for blob in (option, dict(quote or {})):
for key in _CARGO_TYPE_KEYS:
text = str(blob.get(key) or "").strip()
if text and text not in {"-", "—", "-"}:
return text
return "-"
def _remark_display(quote: dict[str, Any] | None, facts: dict[str, str] | None) -> str:
"""报价备注。整单没有时,把费用行上不重复的备注拼起来。仍没有就「-」。"""
src = dict(quote or {})
option = _option(src, facts or {})
for blob in (src, option):
for key in _REMARK_KEYS:
text = str(blob.get(key) or "").strip()
if text and text not in {"-", "—", "-"}:
return text
notes: list[str] = []
for row in _fee_rows(src, facts):
note = str(row.get("remark") or row.get("note") or row.get("description") or "").strip()
if note and note not in notes and note not in {"-", "—", "-"}:
notes.append(note)
return ";".join(notes) if notes else "-"
def _option(quote: dict[str, Any] | None, facts: dict[str, str] | None) -> dict[str, Any]:
"""群摘要和私聊共用:已选定的那条,或仅一条,或起运目的对得上的那条。"""
from agent.schema.air_route_options import resolve_display_air_option
found = resolve_display_air_option(quote, facts)
return dict(found or {})
def _fee_rows(quote: dict[str, Any] | None, facts: dict[str, str] | None) -> list[dict[str, Any]]:
"""
该条线路的原始费用。
先读舱位上的空运段、起运港、目的地。多条线路对不上时不借用整单费用。
已经有其它分项时,去掉「空运费=总价」这一行兜底。
"""
from agent.schema.air_route_options import (
drop_air_freight_total_alias,
parse_air_options,
resolve_display_air_option,
)
src = dict(quote or {})
option = resolve_display_air_option(src, facts)
rows: list[dict[str, Any]] = []
if option:
rows.extend(_named_rows(option.get("feeItems") or option.get("fee_items")))
rows.extend(
_named_rows(
option.get("departureCharges")
or option.get("departure_charges")
or option.get("charges")
)
)
rows.extend(
_named_rows(
option.get("destinationCharges")
or option.get("destination_charges")
or option.get("arrivalCharges")
)
)
rows = _dedupe(rows)
if not rows and len(parse_air_options(src)) > 1:
return []
if not rows:
rows = _named_rows(src.get("feeItems") or src.get("fee_items"))
if not rows:
rows = _named_rows(src.get("fee_lines") or src.get("fee_rows"))
if not rows:
return []
projected = drop_air_freight_total_alias({"total": src.get("total"), "fee_lines": rows})
kept = projected.get("fee_lines")
return [row for row in kept if isinstance(row, dict)] if isinstance(kept, list) else rows
def _named_rows(raw: Any) -> list[dict[str, Any]]:
"""费用数组收成字典。字符串 JSON 也认。没有名称的跳过。"""
parsed = _parse_list(raw)
out: list[dict[str, Any]] = []
for item in parsed:
row = dict(item)
name = str(
row.get("name") or row.get("chargeName") or row.get("feeName") or row.get("label") or ""
).strip()
if not name:
continue
row["name"] = name
out.append(row)
return out
def _parse_list(raw: Any) -> list[dict[str, Any]]:
if isinstance(raw, str) and raw.strip():
try:
raw = json.loads(raw)
except json.JSONDecodeError:
return []
if not isinstance(raw, list):
return []
return [dict(item) for item in raw if isinstance(item, dict)]
def _dedupe(rows: list[dict[str, Any]]) -> list[dict[str, Any]]:
"""同名费用保留先出现的桶,和主账投影一致。"""
seen: set[str] = set()
out: list[dict[str, Any]] = []
for row in rows:
key = str(row.get("name") or "").strip().lower()
if not key or key in seen:
continue
seen.add(key)
out.append(row)
return out
def _format_fee(row: dict[str, Any], header_ccy: str) -> str:
"""一条费用。没有可展示的金额就不出现。"""
from agent.policy.inquiry_copy import money_text
name = str(row.get("name") or "").strip()
if not name:
return ""
ccy = _currency(str(row.get("currency") or row.get("currencyCode") or header_ccy or ""))
unit = _bill_unit(str(row.get("unit") or row.get("unitName") or row.get("chargeUnit") or ""))
unit_price = _first_text(row.get("unitPrice"), row.get("unit_price"))
amount = _first_text(row.get("amount"), row.get("value"))
if "%" in amount or name.endswith("费率") or name in {"税率", "燃油附加费率"}:
shown = amount if "%" in amount else f"{amount}%"
return f"{name}:{shown}"
if unit and unit_price and unit != "票":
price = _compact_number(unit_price)
prefix = f"单价{ccy}{price}" if ccy else f"单价{price}"
return f"{name}:{prefix}/{unit}"
if unit == "票":
num = unit_price or amount
if num:
body = f"{ccy}{_compact_number(num)}" if ccy else _compact_number(num)
return f"{name}:{body}/票"
shown = amount or unit_price
if not shown:
return ""
return f"{name}:{money_text(shown, ccy)}"
def _bill_unit(raw: str) -> str:
"""计价单位收成 KG / 票 / CBM。空串表示 TMS 没给单位。"""
text = (raw or "").strip()
if not text:
return ""
token = text.upper().replace("/", "").replace("每", "").replace(" ", "")
if token in {"票", "SHIPMENT", "BL"} or text in {"票", "/票", "每票"}:
return "票"
if token in {"KG", "KGS", "公斤", "千克"}:
return "KG"
if token in {"CBM", "M3", "立方米", "立方"}:
return "CBM"
return text.lstrip("/")
def _currency(raw: str) -> str:
code = (raw or "").strip().upper()
if code == "RMB":
return "CNY"
return code
def _first_text(*values: Any) -> str:
for value in values:
text = str(value if value is not None else "").strip()
if text and text not in {"-", "—", "-", "None"}:
return text
return ""
def _compact_number(raw: str) -> str:
"""单价和按票金额去掉多余的尾零:11.00 写成 11。解析不了就原样。"""
text = (raw or "").strip().replace(",", "")
for prefix in ("CNY", "USD", "EUR", "HKD", "RMB"):
if text.upper().startswith(prefix):
text = text[len(prefix):].strip()
break
try:
number = float(text)
except ValueError:
return text
if number == int(number):
return str(int(number))
return f"{number:.4f}".rstrip("0").rstrip(".")