Files
inquiry_robot/inquiry-agent/tests/test_extract_oral.py
T
jillion886andCursor 99da1faf30 落地私聊空运询价与TMS报价回填,按后台模板出单。
查到价后把币种、单位、数量、单价、备注写入报价单,避免销售只看到光秃秃的金额。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-14 20:25:35 +08:00

267 lines
10 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 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()