确定运输方式前的多句询价带到所选方式并按该方式抽字段;成交已定后放舱不再推跟进,未成交原因不再被当成新询价。
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -963,6 +963,7 @@ class AirGroupCollabTests(unittest.TestCase):
|
||||
ledger=self.ledger,
|
||||
injected_intent="deal_lost",
|
||||
)
|
||||
before = len(self._texts())
|
||||
phase = handle_air_group(
|
||||
self._msg(sender="air1", text=f"{wo}释放舱位", chat="wr_air_1", mid="r2"),
|
||||
flow=self.flow,
|
||||
@@ -970,6 +971,9 @@ class AirGroupCollabTests(unittest.TestCase):
|
||||
injected_intent="air_release",
|
||||
)
|
||||
self.assertEqual(phase, "release_ok")
|
||||
added = "\n".join(self._texts()[before:])
|
||||
self.assertIn("释放", added)
|
||||
self.assertNotIn("请确认成交状态", added)
|
||||
|
||||
def test_sales_cannot_lock(self) -> None:
|
||||
wo = self._create_air()
|
||||
|
||||
@@ -230,6 +230,41 @@ class ContinueTicketTests(unittest.TestCase):
|
||||
self.assertEqual(phase, "wait_deal")
|
||||
self.assertFalse(any("成交" in x or copy.BTN_DEAL in x for x in replies))
|
||||
|
||||
def test_resume_spoken_deal_closes(self) -> None:
|
||||
"""报价文件已出后,原话写工单号加已成交,要入账并回复,不能没反应。"""
|
||||
ledger = MemoryLedger()
|
||||
created = ledger.create_ticket(
|
||||
sender_id="u6b", business_line="AIR", facts=COMPLETE_AIR
|
||||
)
|
||||
wo = created["work_order_no"]
|
||||
ledger.upsert_quote(
|
||||
work_order_no=wo,
|
||||
quote={"total": "USD 1", "eta": "-", "valid_until": "-"},
|
||||
to_status="已报价",
|
||||
)
|
||||
ledger.render_quote(
|
||||
work_order_no=wo,
|
||||
business_line="AIR",
|
||||
facts=COMPLETE_AIR,
|
||||
quote={"total": "USD 1"},
|
||||
)
|
||||
flow = AirTextInquiryFlow(ledger)
|
||||
replies: list[str] = []
|
||||
|
||||
def reply(text: str, extra=None) -> None:
|
||||
replies.append(text)
|
||||
_ = extra
|
||||
|
||||
phase = flow.resume_historical(
|
||||
sender_id="u6b",
|
||||
work_order_no=wo,
|
||||
reply=reply,
|
||||
text=f"{wo}已成交",
|
||||
)
|
||||
self.assertEqual(phase, "done")
|
||||
self.assertTrue(any("已记录为「已成交」" in x for x in replies))
|
||||
self.assertIn(wo, "\n".join(replies))
|
||||
|
||||
def test_resume_missing_wo_asks(self) -> None:
|
||||
flow = AirTextInquiryFlow(MemoryLedger())
|
||||
replies: list[str] = []
|
||||
|
||||
@@ -70,6 +70,18 @@ class OralExtractTests(unittest.TestCase):
|
||||
self.assertEqual(snap["business_line"], "AIR")
|
||||
self.assertFalse(snap["facts"].get("起运港"))
|
||||
|
||||
def test_bare_dap_is_trade_term(self) -> None:
|
||||
"""句尾单独的 DAP 记成贸易条款,不依赖模型有没有这个字段。"""
|
||||
snap = extract_inquiry_snapshot("查一下空运,出货日期9月30日广州到香港的普货价格,DAP")
|
||||
self.assertEqual(snap["business_line"], "AIR")
|
||||
self.assertEqual(snap["facts"].get("贸易条款"), "DAP")
|
||||
|
||||
def test_trade_term_example_line_is_not_filled(self) -> None:
|
||||
snap = extract_inquiry_snapshot("贸易条款(非必填):\n(如:DDP、DAP、CIF、CIP)")
|
||||
self.assertFalse(snap["facts"].get("贸易条款"))
|
||||
labeled = extract_inquiry_snapshot("贸易条款:CIF\n(如:DDP、DAP、CIF、CIP)")
|
||||
self.assertEqual(labeled["facts"].get("贸易条款"), "CIF")
|
||||
|
||||
def test_labeled_customs_port(self) -> None:
|
||||
self.assertEqual(parse_labeled_facts("通关口岸:皇岗").get("通关口岸"), "皇岗")
|
||||
snap = extract_inquiry_snapshot("通关口岸(非必填):皇岗")
|
||||
|
||||
@@ -414,6 +414,148 @@ class LandDispatchTests(unittest.TestCase):
|
||||
self.assertNotIn("你好,我是询价机器人", last)
|
||||
self.assertNotIn("请直接描述客户需求", last)
|
||||
|
||||
def test_lost_reason_not_taken_as_new_inquiry(self) -> None:
|
||||
"""未成交后下一句是原因。旧报价单被判成调价时,不能改问运输方式。"""
|
||||
from agent.policy.air_text_flow import FlowSession
|
||||
|
||||
sea = get_sea_text_flow()
|
||||
created = sea._ledger.create_ticket(
|
||||
sender_id="lost-1",
|
||||
business_line="SEA",
|
||||
facts={"起运港": "广州港", "目的港": "巴生港", "品名": "机器设备"},
|
||||
)
|
||||
wo = str(created.get("work_order_no") or "")
|
||||
sea._ledger.transition(work_order_no=wo, to_status="未成交")
|
||||
sea._save(
|
||||
FlowSession(
|
||||
sender_id="lost-1",
|
||||
thread_id="lost-1",
|
||||
phase="wait_lost_reason",
|
||||
business_line="SEA",
|
||||
work_order_no=wo,
|
||||
status="未成交",
|
||||
wait_version=2,
|
||||
)
|
||||
)
|
||||
get_land_text_flow()._save(
|
||||
FlowSession(
|
||||
sender_id="lost-1",
|
||||
thread_id="lost-1",
|
||||
phase="wait_deal",
|
||||
business_line="LAND",
|
||||
work_order_no="WO202609240045",
|
||||
status="已报价",
|
||||
quote={"total": "CNY 1"},
|
||||
)
|
||||
)
|
||||
phase = handle_text_inquiry(
|
||||
InboundMessage(sender_id="lost-1", message_id="m-lost", content="客户不需要了"),
|
||||
prefer_quoted=True,
|
||||
store=self.store,
|
||||
)
|
||||
self.assertEqual(phase, "done")
|
||||
body = self._outbox_text()
|
||||
self.assertIn("已记下未成交原因:客户不需要了", body)
|
||||
self.assertNotIn("告知我询价的运输方式", body)
|
||||
|
||||
def test_several_turns_before_mode_follow_the_chosen_line(self) -> None:
|
||||
"""定运输方式前连续几句询价字段都要留下,选定后再整份带上。不只认上一句。"""
|
||||
from agent.policy.air_text_flow import FlowSession, get_air_text_flow
|
||||
|
||||
turns = (
|
||||
(
|
||||
"a",
|
||||
"10月1日,深圳-吉隆坡,散货、普货报价",
|
||||
{
|
||||
"起运港": "深圳",
|
||||
"目的港": "吉隆坡",
|
||||
"品名": "普货",
|
||||
"包装方式": "散货",
|
||||
"报价日期": "2026-10-01",
|
||||
},
|
||||
),
|
||||
(
|
||||
"b",
|
||||
"10件,毛重100公斤,体积2方",
|
||||
{"件数": "10", "毛重": "100", "体积": "2"},
|
||||
),
|
||||
)
|
||||
for label, reply, mode in (
|
||||
("空运", "3", "AIR"),
|
||||
("海运", "海运", "SEA"),
|
||||
("陆运", "1", "LAND"),
|
||||
):
|
||||
reset_sea_text_flow_for_test()
|
||||
reset_land_text_flow_for_test()
|
||||
reset_air_text_flow_for_test()
|
||||
sid = "premode-" + mode
|
||||
# 上一单还在海运,新的几句里没有运输方式,不能把这些字段丢掉。
|
||||
get_sea_text_flow()._save(
|
||||
FlowSession(
|
||||
sender_id=sid,
|
||||
thread_id=sid,
|
||||
phase="tms_miss",
|
||||
business_line="SEA",
|
||||
work_order_no="WO-OLD",
|
||||
facts={"目的港": "旧港口"},
|
||||
)
|
||||
)
|
||||
for mid, content, facts in turns:
|
||||
phase = handle_text_inquiry(
|
||||
InboundMessage(sender_id=sid, message_id=sid + mid, content=content),
|
||||
injected_facts=facts,
|
||||
injected_mode="",
|
||||
store=self.store,
|
||||
)
|
||||
self.assertEqual(phase, "need_mode", label)
|
||||
waiting = get_sea_text_flow().session_of(sid)
|
||||
self.assertEqual(waiting.facts.get("目的港"), "吉隆坡", label)
|
||||
self.assertIn("10", waiting.facts.get("件数") or "", label)
|
||||
self.assertIn("深圳-吉隆坡", waiting.immutable_text)
|
||||
self.assertIn("10件", waiting.immutable_text)
|
||||
before = len(self.store._outbox)
|
||||
phase = handle_text_inquiry(
|
||||
InboundMessage(sender_id=sid, message_id=sid + "mode", content=reply),
|
||||
store=self.store,
|
||||
)
|
||||
self.assertNotEqual(phase, "need_mode", label)
|
||||
added = "\n".join(i.content for i in list(self.store._outbox.values())[before:])
|
||||
owner = {
|
||||
"AIR": get_air_text_flow(),
|
||||
"SEA": get_sea_text_flow(),
|
||||
"LAND": get_land_text_flow(),
|
||||
}[mode]
|
||||
sess = owner.session_of(sid)
|
||||
self.assertIsNotNone(sess, label)
|
||||
self.assertEqual((sess.business_line or "").upper(), mode, label)
|
||||
self.assertEqual(sess.facts.get("起运港"), "深圳", label)
|
||||
self.assertEqual(sess.facts.get("目的港"), "吉隆坡", label)
|
||||
self.assertEqual(sess.facts.get("品名"), "普货", label)
|
||||
self.assertEqual(sess.facts.get("报价日期"), "2026-10-01", label)
|
||||
self.assertNotEqual(sess.facts.get("目的港"), "旧港口", label)
|
||||
if mode == "AIR":
|
||||
self.assertIn("10", sess.facts.get("件数") or "", label)
|
||||
self.assertNotIn("整柜或拼柜", sess.facts, label)
|
||||
self.assertNotIn("箱型箱量", sess.facts, label)
|
||||
elif mode == "SEA":
|
||||
self.assertIn("10", sess.facts.get("货量") or "", label)
|
||||
self.assertNotIn("件数", sess.facts, label)
|
||||
self.assertNotIn("毛重", sess.facts, label)
|
||||
self.assertNotIn("包装方式", sess.facts, label)
|
||||
self.assertIn("货物数量", added)
|
||||
self.assertIn("整柜或拼柜", added)
|
||||
self.assertIn("箱型箱量", added)
|
||||
self.assertIn("贸易条款", added)
|
||||
self.assertNotIn("件数(必填)", added)
|
||||
else:
|
||||
self.assertIn("100", sess.facts.get("毛重") or "", label)
|
||||
self.assertIn("2", sess.facts.get("体积") or "", label)
|
||||
self.assertNotIn("件数", sess.facts, label)
|
||||
self.assertNotIn("箱型箱量", sess.facts, label)
|
||||
self.assertNotIn("贸易条款", sess.facts, label)
|
||||
self.assertIn("运输类型", added)
|
||||
self.assertNotIn("件数(必填)", added)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
|
||||
@@ -60,6 +60,17 @@ class SeaOptionsTest(unittest.TestCase):
|
||||
kept = drop_invented_door_trade_terms("贸易条款:门到门", {"贸易条款": "门到门"})
|
||||
self.assertEqual(kept["贸易条款"], "门到门")
|
||||
|
||||
def test_short_reply_door_or_port_is_trade_term(self):
|
||||
from agent.schema.sea_options import apply_spoken_sea_trade_reply
|
||||
|
||||
door = apply_spoken_sea_trade_reply("门到门", {})
|
||||
self.assertEqual(door.get("贸易条款"), "门到门")
|
||||
port = apply_spoken_sea_trade_reply("港到港", {"运输分类": "Port to Port"})
|
||||
self.assertEqual(port.get("贸易条款"), "港到港")
|
||||
self.assertEqual(port.get("运输分类"), "Port to Port")
|
||||
example = apply_spoken_sea_trade_reply("(如:门到门、港到港)", {})
|
||||
self.assertNotIn("贸易条款", example)
|
||||
|
||||
def test_wo012_trade_port_class_other(self):
|
||||
"""条款写 Port to Port、分类写其它:二者各归各栏,禁止互盖。"""
|
||||
text = (
|
||||
|
||||
@@ -206,6 +206,13 @@ class SeaTextInquiryTests(unittest.TestCase):
|
||||
self.assertIn("运输分类", check["missing"])
|
||||
self.assertIn("贸易条款", check["missing_display"])
|
||||
self.assertIn("运输分类", check["missing_display"])
|
||||
text = copy.ask_clarify(
|
||||
facts=check["facts"],
|
||||
missing_keys=check["missing"],
|
||||
transport_mode="海运",
|
||||
)
|
||||
self.assertIn("贸易条款(必填):\n(如:门到门、港到港)", text)
|
||||
self.assertNotIn("如:FOB", text)
|
||||
|
||||
def test_sea_rejects_door_to_door_class(self) -> None:
|
||||
facts = {
|
||||
@@ -324,6 +331,29 @@ class SeaTextInquiryTests(unittest.TestCase):
|
||||
self.assertIn("贸易条款", joined)
|
||||
self.assertNotEqual(self.flow.session_of("s-door").facts.get("贸易条款"), "门到门")
|
||||
|
||||
def test_reply_door_to_door_fills_trade_terms(self) -> None:
|
||||
"""补问后只回门到门,要记成贸易条款,不能回参数提取不全。"""
|
||||
first = dict(COMPLETE_SEA)
|
||||
first.pop("贸易条款")
|
||||
phase = self.flow.on_text(
|
||||
sender_id="s-door-reply",
|
||||
text="先缺条款",
|
||||
reply=self._reply,
|
||||
injected_facts=first,
|
||||
injected_mode="SEA",
|
||||
)
|
||||
self.assertEqual(phase, "clarify")
|
||||
self.replies.clear()
|
||||
phase = self.flow.on_text(
|
||||
sender_id="s-door-reply",
|
||||
text="门到门",
|
||||
reply=self._reply,
|
||||
)
|
||||
self.assertEqual(phase, "wait_collab")
|
||||
joined = "\n".join(self.replies)
|
||||
self.assertIn("贸易条款:门到门", joined)
|
||||
self.assertNotIn("参数提取不全", joined)
|
||||
|
||||
def test_harvest_class_from_reply_then_quotes(self) -> None:
|
||||
first = dict(COMPLETE_SEA)
|
||||
first.pop("运输分类")
|
||||
|
||||
Reference in New Issue
Block a user