落地私聊空运询价与TMS报价回填,按后台模板出单。
查到价后把币种、单位、数量、单价、备注写入报价单,避免销售只看到光秃秃的金额。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,266 @@
|
||||
"""
|
||||
口述空运抽字段:解析 DeepSeek B JSON,不打外网。
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import sys
|
||||
import unittest
|
||||
from types import SimpleNamespace
|
||||
|
||||
ROOT = os.path.abspath(os.path.join(os.path.dirname(__file__), ".."))
|
||||
if ROOT not in sys.path:
|
||||
sys.path.insert(0, ROOT)
|
||||
|
||||
os.environ.setdefault("LEDGER_BACKEND", "memory")
|
||||
|
||||
from agent.llm.extract_text import extract_inquiry_snapshot
|
||||
from agent.llm.mode_extract_fields import invoke_shell, parse_extract_payload
|
||||
from agent.policy.air_text_flow import AirTextInquiryFlow
|
||||
from agent.ledger.memory_ledger import MemoryLedger
|
||||
|
||||
ORAL = "上海到洛杉矶空运,3件 180kg,体积2.5,普货纸箱"
|
||||
|
||||
B_JSON = {
|
||||
"transport_mode": "AIR",
|
||||
"fields": {
|
||||
"起运港": "上海",
|
||||
"目的港": "洛杉矶",
|
||||
"品名": "普货",
|
||||
"件数": "3",
|
||||
"毛重": "180",
|
||||
"体积": "2.5",
|
||||
"包装方式": "纸箱",
|
||||
"报价日期": "",
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
class OralExtractTests(unittest.TestCase):
|
||||
def test_parse_b_json(self) -> None:
|
||||
parsed = parse_extract_payload(B_JSON)
|
||||
self.assertTrue(parsed["ok"])
|
||||
self.assertEqual(parsed["business_line"], "AIR")
|
||||
self.assertEqual(parsed["facts"]["起运港"], "上海")
|
||||
self.assertNotIn("报价日期", parsed["facts"])
|
||||
|
||||
def test_invoke_shell_with_json(self) -> None:
|
||||
out = invoke_shell(payload={"assistant_json": B_JSON})
|
||||
self.assertTrue(out["ok"])
|
||||
self.assertEqual(out["facts"]["目的港"], "洛杉矶")
|
||||
|
||||
def test_invoke_shell_without_json_keeps_legacy(self) -> None:
|
||||
out = invoke_shell(payload={"a": 1})
|
||||
self.assertEqual(out.get("error"), "extract_fields_shell_not_implemented")
|
||||
|
||||
def test_oral_snapshot_uses_b_not_regex(self) -> None:
|
||||
snap = extract_inquiry_snapshot(ORAL, b_result=parse_extract_payload(B_JSON))
|
||||
self.assertEqual(snap["business_line"], "AIR")
|
||||
self.assertEqual(snap["facts"]["件数"], "3件")
|
||||
self.assertEqual(snap["source"], "deepseek_b")
|
||||
|
||||
def test_oral_without_b_does_not_guess_ports(self) -> None:
|
||||
snap = extract_inquiry_snapshot(ORAL)
|
||||
self.assertEqual(snap["business_line"], "AIR")
|
||||
self.assertFalse(snap["facts"].get("起运港"))
|
||||
|
||||
def test_assemble_air_query_zhuhai_clark(self) -> None:
|
||||
from agent.schema.tms_air_query import assemble_air_query
|
||||
|
||||
out = assemble_air_query(
|
||||
{
|
||||
"起运港": "珠海",
|
||||
"目的港": "克拉克",
|
||||
"品名": "茶叶",
|
||||
"件数": "10件",
|
||||
"毛重": "100KGS",
|
||||
"体积": "2CBM",
|
||||
"包装方式": "托盘",
|
||||
"报价日期": "2026-09-12",
|
||||
}
|
||||
)
|
||||
self.assertTrue(out["ok"])
|
||||
self.assertEqual(out["facts"]["originCode"], "ZUH")
|
||||
self.assertEqual(out["facts"]["destinationCode"], "CRK")
|
||||
self.assertEqual(out["facts"]["packageType"], "2")
|
||||
self.assertEqual(out["facts"]["件数"], "10件")
|
||||
|
||||
bulk = assemble_air_query(
|
||||
{
|
||||
"起运港": "珠海",
|
||||
"目的港": "克拉克",
|
||||
"品名": "茶叶",
|
||||
"件数": "10",
|
||||
"毛重": "100",
|
||||
"体积": "2",
|
||||
"包装方式": "散货",
|
||||
"报价日期": "2026-09-12",
|
||||
}
|
||||
)
|
||||
self.assertTrue(bulk["ok"])
|
||||
self.assertEqual(bulk["facts"]["包装方式"], "散货")
|
||||
self.assertNotIn("包装方式", bulk["missing"])
|
||||
|
||||
def test_fly_route_overwrites_stale_nkg_kul(self) -> None:
|
||||
from agent.schema.field_validate import harvest_oral_measures
|
||||
from agent.schema.tms_air_query import assemble_air_query
|
||||
from agent.policy import inquiry_copy as copy
|
||||
|
||||
first = harvest_oral_measures(
|
||||
"从南京飞吉隆坡,9月14日茶叶,托盘,4.2cbm,1.5KG",
|
||||
{},
|
||||
)
|
||||
self.assertEqual(first["起运港"], "南京")
|
||||
self.assertEqual(first["目的港"], "吉隆坡")
|
||||
assembled = assemble_air_query(
|
||||
{**first, "originCode": "NKG", "destinationCode": "KUL", "件数": "10"}
|
||||
)
|
||||
card = copy.inquiry_card(work_order_no="WO1", facts=assembled["facts"])
|
||||
self.assertIn("起运地:南京", card)
|
||||
self.assertIn("目的地:吉隆坡", card)
|
||||
self.assertNotIn("起运地:NKG", card)
|
||||
self.assertNotIn("目的地:KUL", card)
|
||||
second = harvest_oral_measures(
|
||||
"北京飞吉隆坡,9月14日茶叶,4.2cbm,1.5KG",
|
||||
{**assembled["facts"]},
|
||||
)
|
||||
again = assemble_air_query(second)
|
||||
self.assertEqual(again["facts"]["起运港"], "北京")
|
||||
self.assertEqual(again["facts"]["originCode"], "PEK")
|
||||
self.assertEqual(again["facts"]["destinationCode"], "KUL")
|
||||
shown = copy.inquiry_card(work_order_no="WO2", facts=again["facts"])
|
||||
self.assertIn("起运地:北京", shown)
|
||||
self.assertNotIn("起运地:NKG", shown)
|
||||
self.assertNotIn("起运地:南京", shown)
|
||||
|
||||
def test_oral_pack_and_quote_date_from_current_sentence(self) -> None:
|
||||
from agent.schema.field_validate import harvest_oral_measures
|
||||
|
||||
first = harvest_oral_measures(
|
||||
"南京飞吉隆坡,9月15日茶叶,托盘,4.2cbm,1.5KG",
|
||||
{},
|
||||
)
|
||||
self.assertEqual(first["包装方式"], "托盘")
|
||||
self.assertEqual(first["报价日期"], "2026-09-15")
|
||||
self.assertEqual(first["毛重"], "1.5KG")
|
||||
self.assertEqual(first["体积"], "4.2cbm")
|
||||
second = harvest_oral_measures(
|
||||
"京飞吉隆坡,9月16日茶叶,散货,5.2cbm,2.5KG",
|
||||
first,
|
||||
)
|
||||
self.assertEqual(second["包装方式"], "散货")
|
||||
self.assertEqual(second["报价日期"], "2026-09-16")
|
||||
self.assertEqual(second["毛重"], "2.5KG")
|
||||
self.assertEqual(second["体积"], "5.2cbm")
|
||||
|
||||
def test_clarify_replaces_pallet_with_bulk_and_keeps_quote_date(self) -> None:
|
||||
replies: list[str] = []
|
||||
|
||||
def reply(text: str, extra=None) -> None:
|
||||
replies.append(text)
|
||||
_ = extra
|
||||
|
||||
flow = AirTextInquiryFlow(MemoryLedger())
|
||||
flow.on_text(
|
||||
sender_id="u_date",
|
||||
text="南京飞吉隆坡,9月15日茶叶,托盘,4.2cbm,1.5KG",
|
||||
reply=reply,
|
||||
injected_facts={
|
||||
"起运港": "南京",
|
||||
"目的港": "吉隆坡",
|
||||
"品名": "茶叶",
|
||||
"包装方式": "托盘",
|
||||
},
|
||||
injected_mode="AIR",
|
||||
)
|
||||
self.assertEqual(flow.session_of("u_date").facts["包装方式"], "托盘")
|
||||
self.assertEqual(flow.session_of("u_date").facts["报价日期"], "2026-09-15")
|
||||
flow.on_text(
|
||||
sender_id="u_date",
|
||||
text="京飞吉隆坡,9月16日茶叶,散货,5.2cbm,2.5KG",
|
||||
reply=reply,
|
||||
injected_facts={
|
||||
"起运港": "南京",
|
||||
"目的港": "吉隆坡",
|
||||
"品名": "茶叶",
|
||||
},
|
||||
injected_mode="AIR",
|
||||
)
|
||||
sess = flow.session_of("u_date")
|
||||
self.assertEqual(sess.facts["包装方式"], "散货")
|
||||
self.assertEqual(sess.facts["报价日期"], "2026-09-16")
|
||||
last = replies[-1]
|
||||
self.assertIn("包装类型:散货", last)
|
||||
self.assertIn("报价日期:2026-09-16", last)
|
||||
|
||||
def test_weight_keeps_user_kg_not_kgs(self) -> None:
|
||||
from agent.policy import inquiry_copy as copy
|
||||
from agent.schema.field_validate import harvest_oral_measures
|
||||
|
||||
facts = harvest_oral_measures("从南京飞吉隆坡,茶叶,1.5KG,4.2cbm,托盘", {})
|
||||
self.assertEqual(facts["毛重"], "1.5KG")
|
||||
shown = copy.inquiry_card(
|
||||
work_order_no="WO1",
|
||||
facts={**facts, "起运港": "南京", "目的港": "吉隆坡", "品名": "茶叶", "件数": "10"},
|
||||
)
|
||||
self.assertIn("重量:1.5KG", shown)
|
||||
self.assertNotIn("1.5KGS", shown)
|
||||
|
||||
def test_oral_units_fill_weight_volume_without_b(self) -> None:
|
||||
snap = extract_inquiry_snapshot(
|
||||
"珠海到克拉克空运,茶叶 10件 100KGS 2CBM 托盘"
|
||||
)
|
||||
self.assertEqual(snap["facts"]["件数"], "10件")
|
||||
self.assertEqual(snap["facts"]["毛重"], "100KGS")
|
||||
self.assertEqual(snap["facts"]["体积"], "2CBM")
|
||||
self.assertFalse(snap["facts"].get("起运港"))
|
||||
|
||||
def test_labeled_weight_alias(self) -> None:
|
||||
from agent.llm.extract_text import parse_labeled_facts
|
||||
from agent.schema.field_validate import normalize_facts
|
||||
|
||||
facts = parse_labeled_facts("重量(KG):180\n体积(CBM):2.5")
|
||||
norm = normalize_facts(facts)
|
||||
self.assertEqual(norm["毛重"], "180")
|
||||
self.assertEqual(norm["体积"], "2.5")
|
||||
|
||||
def test_chat_fn_fixture_drives_flow(self) -> None:
|
||||
def chat_fn(**kwargs):
|
||||
return SimpleNamespace(
|
||||
ok=True,
|
||||
content="",
|
||||
error="",
|
||||
tool_calls=[
|
||||
{
|
||||
"function": {
|
||||
"name": "extract_inquiry_fields",
|
||||
"arguments": __import__("json").dumps(B_JSON, ensure_ascii=False),
|
||||
}
|
||||
}
|
||||
],
|
||||
)
|
||||
|
||||
replies: list[str] = []
|
||||
|
||||
def reply(text: str, extra=None) -> None:
|
||||
replies.append(text)
|
||||
_ = extra
|
||||
|
||||
flow = AirTextInquiryFlow(MemoryLedger())
|
||||
phase = flow.on_text(
|
||||
sender_id="oral1",
|
||||
text=ORAL,
|
||||
reply=reply,
|
||||
chat_fn=chat_fn,
|
||||
)
|
||||
self.assertEqual(phase, "wait_collab")
|
||||
joined = "\n".join(replies)
|
||||
self.assertIn("工单号:", joined)
|
||||
self.assertIn("起运地:上海", joined)
|
||||
self.assertIn("目的地:洛杉矶", joined)
|
||||
self.assertNotIn("【询价卡片】", joined)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
Reference in New Issue
Block a user