空运未填报价日期时不默认当天,查出舱位后由销售点选再写回日期。
包装类型里的托盘不再被抄进增值服务。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,169 @@
|
||||
"""
|
||||
空运增值服务:多项原词、示例行不当已填、传给 TMS 的是 addedServices 列表。
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import sys
|
||||
import unittest
|
||||
|
||||
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("YTD_ENV", "test")
|
||||
|
||||
from agent.llm.extract_text import extract_inquiry_snapshot
|
||||
from agent.policy import inquiry_copy as copy
|
||||
from agent.schema.added_services import harvest_added_services, split_added_services
|
||||
from agent.schema.tms_air_query import assemble_air_query
|
||||
|
||||
|
||||
_EXAMPLE = "(如:托盘、前置仓、带电、卡转、卡转低消、MUST GO、备装、分泡、温控、快件、DG、TC)"
|
||||
|
||||
|
||||
class AirAddedServicesTests(unittest.TestCase):
|
||||
def test_split_enumeration_and_chinese_comma(self) -> None:
|
||||
"""顿号、中文逗号、英文逗号拆成同一组,空格不拆开 MUST GO。"""
|
||||
self.assertEqual(
|
||||
split_added_services("托盘、前置仓、带电"),
|
||||
["托盘", "前置仓", "带电"],
|
||||
)
|
||||
self.assertEqual(
|
||||
split_added_services("托盘,前置仓,带电"),
|
||||
["托盘", "前置仓", "带电"],
|
||||
)
|
||||
self.assertEqual(
|
||||
split_added_services("托盘,前置仓,带电"),
|
||||
["托盘", "前置仓", "带电"],
|
||||
)
|
||||
self.assertEqual(split_added_services("MUST GO、带电"), ["MUST GO", "带电"])
|
||||
|
||||
def test_example_line_is_not_filled(self) -> None:
|
||||
text = "增值服务(非必填):\n" + _EXAMPLE
|
||||
self.assertEqual(harvest_added_services(text), [])
|
||||
|
||||
def test_labeled_value_keeps_order_and_canonical_english(self) -> None:
|
||||
dun = harvest_added_services("增值服务(非必填):托盘、前置仓、带电")
|
||||
comma = harvest_added_services("增值服务(非必填):托盘,前置仓,带电")
|
||||
self.assertEqual(dun, ["托盘", "前置仓", "带电"])
|
||||
self.assertEqual(comma, ["托盘", "前置仓", "带电"])
|
||||
spoken = harvest_added_services("这票 must go,要dg和tc")
|
||||
self.assertEqual(spoken, ["MUST GO", "DG", "TC"])
|
||||
|
||||
def test_longest_match_and_negation(self) -> None:
|
||||
self.assertEqual(harvest_added_services("要卡转低消"), ["卡转低消"])
|
||||
self.assertNotIn("卡转", harvest_added_services("要卡转低消"))
|
||||
self.assertEqual(harvest_added_services("不带电"), [])
|
||||
self.assertEqual(harvest_added_services("带电"), ["带电"])
|
||||
|
||||
def test_outside_list_only_when_labeled(self) -> None:
|
||||
"""名单外的词必须写在增值服务后面才记;12 个词只要出现就记。"""
|
||||
self.assertEqual(harvest_added_services("要打木架"), [])
|
||||
self.assertEqual(
|
||||
harvest_added_services("增值服务:打木架"),
|
||||
["打木架"],
|
||||
)
|
||||
self.assertEqual(
|
||||
harvest_added_services("包装纸箱,要卡转低消,带电,增值服务:打木架"),
|
||||
["卡转低消", "带电", "打木架"],
|
||||
)
|
||||
|
||||
def test_extract_stores_added_services_and_keeps_other_package(self) -> None:
|
||||
snap = extract_inquiry_snapshot(
|
||||
"空运到吉隆坡,茶叶,10件,100公斤,2方,包装纸箱,要卡转低消,带电,增值服务:打木架",
|
||||
injected_mode="AIR",
|
||||
)
|
||||
facts = snap["facts"]
|
||||
self.assertEqual(facts.get("added_services"), "卡转低消、带电、打木架")
|
||||
self.assertEqual(facts.get("包装方式"), "纸箱")
|
||||
self.assertEqual(facts.get("是否含电"), "是")
|
||||
|
||||
def test_pallet_does_not_replace_another_package(self) -> None:
|
||||
snap = extract_inquiry_snapshot(
|
||||
"空运,包装是纸箱,要托盘",
|
||||
injected_mode="AIR",
|
||||
)
|
||||
facts = snap["facts"]
|
||||
self.assertEqual(facts.get("包装方式"), "纸箱")
|
||||
self.assertEqual(facts.get("added_services"), "托盘")
|
||||
|
||||
def test_blank_service_line_does_not_copy_package_or_next_field(self) -> None:
|
||||
"""包装类型写托盘、增值服务留空时,不能把托盘和下一行报价日期抄进增值服务。"""
|
||||
text = (
|
||||
"运输方式:空运\n"
|
||||
"起运地:-\n"
|
||||
"目的地:CRK\n"
|
||||
"品名:LMM振动测试定位治具\n"
|
||||
"件数:1件\n"
|
||||
"重量(KG):1.5KG\n"
|
||||
"体积(CBM):100CBM\n"
|
||||
"包装类型:托盘\n"
|
||||
"增值服务:\n"
|
||||
"报价日期(非必填):"
|
||||
)
|
||||
self.assertEqual(harvest_added_services(text), [])
|
||||
snap = extract_inquiry_snapshot(text)
|
||||
self.assertNotIn("added_services", snap["facts"])
|
||||
self.assertEqual(snap["facts"].get("包装方式"), "托盘")
|
||||
card = copy.air_confirm_card(snap["facts"])
|
||||
self.assertIn("包装类型(必填):托盘", card)
|
||||
self.assertIn("增值服务(非必填):\n", card)
|
||||
self.assertNotIn("托盘、报价日期", card)
|
||||
|
||||
def test_pallet_alone_is_package_and_service(self) -> None:
|
||||
snap = extract_inquiry_snapshot(
|
||||
"空运,不带电,包装托盘,目的地吉隆坡",
|
||||
injected_mode="AIR",
|
||||
)
|
||||
facts = snap["facts"]
|
||||
self.assertEqual(facts.get("包装方式"), "托盘")
|
||||
self.assertEqual(facts.get("added_services"), "托盘")
|
||||
self.assertNotIn("带电", facts.get("added_services") or "")
|
||||
self.assertEqual(facts.get("是否含电"), "否")
|
||||
|
||||
def test_confirm_lists_optional_and_ticket_hides_empty(self) -> None:
|
||||
card = copy.air_confirm_card({"目的港": "吉隆坡", "包装方式": "纸箱"})
|
||||
self.assertIn("增值服务(非必填):", card)
|
||||
clarify = copy.ask_clarify(
|
||||
facts={"目的港": "吉隆坡"},
|
||||
missing_keys=["品名"],
|
||||
transport_mode="空运",
|
||||
)
|
||||
self.assertIn("增值服务(非必填):\n" + _EXAMPLE, clarify)
|
||||
lines = copy.inquiry_field_lines(
|
||||
work_order_no="WO1",
|
||||
facts={"目的港": "吉隆坡", "包装方式": "纸箱", "added_services": "托盘、带电"},
|
||||
transport_mode="空运",
|
||||
)
|
||||
self.assertIn("增值服务:托盘、带电", lines)
|
||||
empty = copy.inquiry_field_lines(
|
||||
work_order_no="WO1",
|
||||
facts={"目的港": "吉隆坡", "包装方式": "纸箱"},
|
||||
transport_mode="空运",
|
||||
)
|
||||
self.assertFalse(any(line.startswith("增值服务") for line in empty))
|
||||
|
||||
def test_assemble_keeps_services_without_blocking(self) -> None:
|
||||
ready = {
|
||||
"目的港": "吉隆坡",
|
||||
"品名": "茶叶",
|
||||
"件数": "10",
|
||||
"毛重": "100",
|
||||
"体积": "2",
|
||||
"包装方式": "纸箱",
|
||||
"报价日期": "2026-10-13",
|
||||
"added_services": "托盘、前置仓、带电",
|
||||
}
|
||||
out = assemble_air_query(ready)
|
||||
self.assertTrue(out["ok"], out)
|
||||
self.assertEqual(out["facts"]["added_services"], "托盘、前置仓、带电")
|
||||
bare = dict(ready)
|
||||
bare.pop("added_services")
|
||||
self.assertTrue(assemble_air_query(bare)["ok"])
|
||||
self.assertNotIn("added_services", assemble_air_query(bare)["facts"])
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -87,6 +87,7 @@ class AirRouteOptionsTests(unittest.TestCase):
|
||||
"毛重": "100",
|
||||
"体积": "2",
|
||||
"包装方式": "托盘",
|
||||
"报价日期": "2026-09-16",
|
||||
"tms_multi": "1",
|
||||
}
|
||||
phase = flow.on_text(
|
||||
@@ -144,6 +145,7 @@ class AirRouteOptionsTests(unittest.TestCase):
|
||||
"毛重": "21",
|
||||
"体积": "1",
|
||||
"包装方式": "散货",
|
||||
"报价日期": "2026-09-16",
|
||||
}
|
||||
self.assertEqual(
|
||||
flow.on_text(sender_id="u_hgh", text="空运", reply=reply, injected_facts=facts, injected_mode="AIR"),
|
||||
@@ -174,6 +176,7 @@ class AirRouteOptionsTests(unittest.TestCase):
|
||||
"毛重": "100",
|
||||
"体积": "2",
|
||||
"包装方式": "托盘",
|
||||
"报价日期": "2026-09-16",
|
||||
"tms_multi": "1",
|
||||
}
|
||||
flow.on_text(sender_id="u_zuh", text="空运", reply=reply, injected_facts=facts, injected_mode="AIR")
|
||||
@@ -199,6 +202,7 @@ class AirRouteOptionsTests(unittest.TestCase):
|
||||
"毛重": "100",
|
||||
"体积": "2",
|
||||
"包装方式": "托盘",
|
||||
"报价日期": "2026-09-16",
|
||||
"tms_multi": "1",
|
||||
}
|
||||
phase = flow.on_text(
|
||||
@@ -413,6 +417,118 @@ class AirRouteOptionsTests(unittest.TestCase):
|
||||
self.assertIn("window.close()", desktop)
|
||||
self.assertNotIn("closeWindow", dead)
|
||||
|
||||
def test_blank_date_always_opens_cabin_page_and_writes_flight_date(self) -> None:
|
||||
"""没填日期:1 条舱位也要进 H5;选定后用该条总价,航班日期写回,不再查价。"""
|
||||
from agent.schema.air_route_options import sort_cabin_options
|
||||
|
||||
later = {
|
||||
"originAirportCode": "PVG",
|
||||
"destinationAirportCode": "LAX",
|
||||
"flightDate": "2026-10-21",
|
||||
"schedule": "D1",
|
||||
"airlineName": "东航",
|
||||
"salePrice": 1000,
|
||||
"currency": "CNY",
|
||||
}
|
||||
earlier = {
|
||||
"originAirportCode": "PVG",
|
||||
"destinationAirportCode": "LAX",
|
||||
"flightDate": "2026-10-20",
|
||||
"schedule": "D234567",
|
||||
"airlineName": "南航",
|
||||
"salePrice": 3200,
|
||||
"currency": "CNY",
|
||||
}
|
||||
ordered = sort_cabin_options([later, earlier])
|
||||
self.assertEqual(option_flight_date_of(ordered[0]), "2026-10-20")
|
||||
|
||||
class Ledger(MemoryLedger):
|
||||
def __init__(self) -> None:
|
||||
super().__init__()
|
||||
self.queries = 0
|
||||
|
||||
def query_tms(self, **kwargs):
|
||||
self.queries += 1
|
||||
out = super().query_tms(**kwargs)
|
||||
out["quote"]["airOptions"] = [later, earlier]
|
||||
return out
|
||||
|
||||
ledger = Ledger()
|
||||
flow = AirTextInquiryFlow(ledger)
|
||||
replies: list[str] = []
|
||||
|
||||
def reply(text: str, extra=None) -> None:
|
||||
replies.append(text)
|
||||
_ = extra
|
||||
|
||||
facts = {
|
||||
"起运港": "上海",
|
||||
"目的港": "洛杉矶",
|
||||
"品名": "普货",
|
||||
"件数": "3",
|
||||
"毛重": "180",
|
||||
"体积": "2.5",
|
||||
"包装方式": "纸箱",
|
||||
}
|
||||
self.assertEqual(
|
||||
flow.on_text(sender_id="u_cabin", text="空运", reply=reply, injected_facts=facts, injected_mode="AIR"),
|
||||
"wait_confirm",
|
||||
)
|
||||
self.assertIn("报价日期(非必填):", replies[-1])
|
||||
self.assertNotIn("报价日期:2026", replies[-1])
|
||||
self.assertEqual(flow.on_text(sender_id="u_cabin", text="确定", reply=reply), "wait_air_route")
|
||||
self.assertEqual(ledger.queries, 1)
|
||||
self.assertTrue(any("条舱位" in x for x in replies))
|
||||
self.assertFalse(any("条线路" in x for x in replies))
|
||||
self.assertEqual(flow.select_air_route(sender_id="u_cabin", index=0, reply=reply), "wait_collab")
|
||||
self.assertEqual(ledger.queries, 1)
|
||||
sess = flow.session_of("u_cabin")
|
||||
self.assertEqual(sess.facts.get("报价日期"), "2026-10-20")
|
||||
self.assertIn("3200", str(sess.quote.get("total")))
|
||||
|
||||
def test_cabin_page_shows_six_fields_and_total_only(self) -> None:
|
||||
html = render_air_route_page(
|
||||
{
|
||||
"cabin_pick": True,
|
||||
"work_order_no": "WO202610130001",
|
||||
"facts": {},
|
||||
"quote": {
|
||||
"airOptions": [
|
||||
{
|
||||
"originAirportCode": "PVG",
|
||||
"destinationAirportCode": "LAX",
|
||||
"flightDate": "2026-10-20",
|
||||
"schedule": "D234567",
|
||||
"airlineName": "南航",
|
||||
"salePrice": 3200,
|
||||
"currency": "CNY",
|
||||
"departureCharges": [
|
||||
{"chargeName": "运输费", "currency": "CNY", "unitPrice": 11}
|
||||
],
|
||||
}
|
||||
]
|
||||
},
|
||||
}
|
||||
)
|
||||
self.assertIn("选择舱位", html)
|
||||
self.assertIn("起运地", html)
|
||||
self.assertIn("目的地", html)
|
||||
self.assertIn("航班日期", html)
|
||||
self.assertIn("2026-10-20", html)
|
||||
self.assertIn("班期", html)
|
||||
self.assertIn("D234567", html)
|
||||
self.assertIn("航司", html)
|
||||
self.assertIn("南航", html)
|
||||
self.assertIn("3200", html)
|
||||
self.assertNotIn("运输费", html)
|
||||
self.assertNotIn("userid", html.lower())
|
||||
|
||||
|
||||
def option_flight_date_of(option: dict) -> str:
|
||||
from agent.schema.air_route_options import option_flight_date
|
||||
|
||||
return option_flight_date(option)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
|
||||
@@ -140,11 +140,16 @@ class AirTextInquiryTests(unittest.TestCase):
|
||||
),
|
||||
)
|
||||
|
||||
def test_quote_date_defaults_today_not_in_missing(self) -> None:
|
||||
def test_air_quote_date_stays_blank_and_not_in_missing(self) -> None:
|
||||
"""空运没说报价日期不填当天,也不拿来补问。海运、陆运仍默认当天。"""
|
||||
result = validate_required_fields(facts=COMPLETE_AIR, business_line="AIR")
|
||||
self.assertTrue(result["ok"])
|
||||
self.assertTrue(result["facts"]["报价日期"])
|
||||
self.assertFalse(result["facts"].get("报价日期"))
|
||||
self.assertNotIn("报价日期", result["missing"])
|
||||
sea = validate_required_fields(facts={"起运港": "上海"}, business_line="SEA")
|
||||
self.assertTrue(sea["facts"].get("报价日期"))
|
||||
land = validate_required_fields(facts={"运输类型": "国内整车"}, business_line="LAND")
|
||||
self.assertTrue(land["facts"].get("报价日期"))
|
||||
|
||||
def test_fly_is_air_and_followup_keeps_mode(self) -> None:
|
||||
phase = self._on_text(
|
||||
@@ -284,7 +289,7 @@ class AirTextInquiryTests(unittest.TestCase):
|
||||
self.assertIn("目的地", last)
|
||||
self.assertIn("件数", last)
|
||||
self.assertIn("如:", last)
|
||||
self.assertIn("报价日期:", last)
|
||||
self.assertIn("报价日期(非必填):", last)
|
||||
self.assertNotIn("报价日期:(如", last)
|
||||
|
||||
def test_clarify_paste_reply_fills_missing_required(self) -> None:
|
||||
|
||||
Reference in New Issue
Block a user