落地私聊空运询价与TMS报价回填,按后台模板出单。
查到价后把币种、单位、数量、单价、备注写入报价单,避免销售只看到光秃秃的金额。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,278 @@
|
||||
"""续办历史工单:模型意图合同 + 回到当时节点。"""
|
||||
|
||||
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")
|
||||
os.environ.setdefault("REDIS_BACKEND", "memory")
|
||||
os.environ.setdefault("CHECKPOINT_BACKEND", "memory")
|
||||
os.environ.setdefault("MESSAGE_STORE_BACKEND", "memory")
|
||||
os.environ.setdefault("LLM_DATA_USAGE_CONFIRMED", "false")
|
||||
os.environ.setdefault("LLM_ALLOW_NETWORK", "false")
|
||||
os.environ.setdefault("LEDGER_BACKEND", "memory")
|
||||
|
||||
from agent.channel.wecom.models import InboundMessage
|
||||
from agent.handlers.continue_thread import handle_continue_thread
|
||||
from agent.ledger.memory_ledger import MemoryLedger
|
||||
from agent.policy import inquiry_copy as copy
|
||||
from agent.policy.air_text_flow import AirTextInquiryFlow
|
||||
from agent.routing import IntentRouter
|
||||
from agent.routing.deepseek_a import DeepSeekARouter
|
||||
from agent.routing.dispatch import dispatch_inbound
|
||||
from agent.routing.ordered_actions import parse_ordered_actions_arguments
|
||||
|
||||
|
||||
COMPLETE_AIR = {
|
||||
"起运港": "上海",
|
||||
"目的港": "洛杉矶",
|
||||
"品名": "普货",
|
||||
"件数": "3",
|
||||
"毛重": "180",
|
||||
"体积": "2.5",
|
||||
"包装方式": "纸箱",
|
||||
}
|
||||
|
||||
|
||||
class ContinueTicketTests(unittest.TestCase):
|
||||
def test_model_contract_accepts_continue_intent(self) -> None:
|
||||
digest = "a" * 64
|
||||
parsed = parse_ordered_actions_arguments(
|
||||
{
|
||||
"decision_id": "d1",
|
||||
"intent": "continue_thread",
|
||||
"ordered_action": {
|
||||
"action_type": "CONTINUE_HISTORICAL_TICKET",
|
||||
"order": 1,
|
||||
"target": {"work_order_no": "WO202609140034"},
|
||||
"evidence": {
|
||||
"source_ref": "inbound:u1",
|
||||
"content_digest": digest,
|
||||
},
|
||||
},
|
||||
}
|
||||
)
|
||||
decision = parsed.to_route_decision()
|
||||
self.assertEqual(decision.intent, "continue_thread")
|
||||
self.assertEqual(decision.thread_id, "WO202609140034")
|
||||
|
||||
def test_model_junk_digest_still_continues(self) -> None:
|
||||
"""测服实测:模型会乱填 content_digest,不能因此整句判失败。"""
|
||||
router = DeepSeekARouter.from_settings()
|
||||
decision = router.route_from_model_args(
|
||||
{
|
||||
"decision_id": "x",
|
||||
"intent": "continue_thread",
|
||||
"ordered_action": {
|
||||
"action_type": "CONTINUE_HISTORICAL_TICKET",
|
||||
"order": "1",
|
||||
"target": {"work_order_no": "WO202609140037"},
|
||||
"evidence": {
|
||||
"source_ref": "user",
|
||||
"content_digest": "not-a-hash",
|
||||
},
|
||||
},
|
||||
},
|
||||
text="我要继续聊聊WO202609140037",
|
||||
sender_id="WuJiLin",
|
||||
)
|
||||
self.assertEqual(decision.intent, "continue_thread")
|
||||
self.assertEqual(decision.thread_id, "WO202609140037")
|
||||
|
||||
def test_stub_natural_language_not_fixed_command(self) -> None:
|
||||
router = DeepSeekARouter.from_settings()
|
||||
hit = router.route_stub_fixture(
|
||||
text="我要继续咨询工单WO202609140034",
|
||||
sender_id="WuJiLin",
|
||||
)
|
||||
self.assertEqual(hit.intent, "continue_thread")
|
||||
self.assertEqual(hit.thread_id, "WO202609140034")
|
||||
again = router.route_stub_fixture(
|
||||
text="把刚才那单 WO202609140034 再出一下方案",
|
||||
sender_id="WuJiLin",
|
||||
)
|
||||
self.assertEqual(again.intent, "continue_thread")
|
||||
fresh = router.route_stub_fixture(
|
||||
text="上海到洛杉矶空运,3件 180kg 茶叶",
|
||||
sender_id="WuJiLin",
|
||||
)
|
||||
self.assertEqual(fresh.intent, "ordinary_text_inquiry")
|
||||
self.assertEqual(fresh.thread_id, "")
|
||||
# 无网夹具认不出「只贴工单号」;正式环境必须由模型认。
|
||||
bare = router.route_stub_fixture(
|
||||
text="WO202609140034",
|
||||
sender_id="WuJiLin",
|
||||
)
|
||||
self.assertEqual(bare.intent, "ordinary_text_inquiry")
|
||||
|
||||
def test_card_click_skips_model(self) -> None:
|
||||
router = IntentRouter(allow_network=False)
|
||||
hit = router.route_inbound(
|
||||
text="adopt_excel:WO202609140034",
|
||||
sender_id="WuJiLin",
|
||||
event="template_card_event",
|
||||
)
|
||||
self.assertEqual(hit.intent, "card_action")
|
||||
|
||||
def test_resume_scheme_from_session(self) -> None:
|
||||
flow = AirTextInquiryFlow(MemoryLedger())
|
||||
replies: list[str] = []
|
||||
extras: list[dict] = []
|
||||
|
||||
def reply(text: str, extra=None) -> None:
|
||||
replies.append(text)
|
||||
extras.append(dict(extra) if extra else {})
|
||||
|
||||
flow.on_text(
|
||||
sender_id="u1",
|
||||
text="空运",
|
||||
reply=reply,
|
||||
injected_facts=COMPLETE_AIR,
|
||||
injected_mode="AIR",
|
||||
)
|
||||
wo = flow.session_of("u1").work_order_no
|
||||
replies.clear()
|
||||
extras.clear()
|
||||
flow.on_button(sender_id="u1", action=copy.BTN_SKIP_COLLAB, reply=reply)
|
||||
self.assertEqual(flow.session_of("u1").phase, "wait_adopt")
|
||||
replies.clear()
|
||||
extras.clear()
|
||||
phase = flow.resume_historical(
|
||||
sender_id="u1", work_order_no=wo, reply=reply
|
||||
)
|
||||
self.assertEqual(phase, "wait_adopt")
|
||||
self.assertTrue(any(f"{wo}报价方案" in x for x in replies))
|
||||
self.assertTrue(
|
||||
any((x.get("template_card") or {}).get("main_title", {}).get("title") == f"{wo}报价方案" for x in extras)
|
||||
)
|
||||
|
||||
def test_resume_from_ledger_quoted(self) -> None:
|
||||
ledger = MemoryLedger()
|
||||
flow = AirTextInquiryFlow(ledger)
|
||||
created = ledger.create_ticket(
|
||||
sender_id="u2", business_line="AIR", facts=COMPLETE_AIR
|
||||
)
|
||||
wo = created["work_order_no"]
|
||||
ledger.upsert_quote(
|
||||
work_order_no=wo,
|
||||
quote={
|
||||
"total": "USD 1,280",
|
||||
"eta": "3-5 天",
|
||||
"valid_until": "2026-09-20",
|
||||
"source_label": "TMS 标准报价",
|
||||
"fee_lines": [{"name": "空运费", "amount": "USD 1,200"}],
|
||||
},
|
||||
to_status="已报价",
|
||||
)
|
||||
replies: list[str] = []
|
||||
|
||||
def reply(text: str, extra=None) -> None:
|
||||
replies.append(text)
|
||||
_ = extra
|
||||
|
||||
phase = flow.resume_historical(
|
||||
sender_id="u2", work_order_no=wo, reply=reply
|
||||
)
|
||||
self.assertEqual(phase, "wait_adopt")
|
||||
self.assertTrue(any("报价方案" in x for x in replies))
|
||||
|
||||
def test_resume_missing_wo_asks(self) -> None:
|
||||
flow = AirTextInquiryFlow(MemoryLedger())
|
||||
replies: list[str] = []
|
||||
phase = flow.resume_historical(
|
||||
sender_id="u3", work_order_no="", reply=replies.append
|
||||
)
|
||||
self.assertEqual(phase, "need_work_order")
|
||||
self.assertTrue(any("工单号" in x for x in replies))
|
||||
|
||||
def test_dispatch_passes_work_order(self) -> None:
|
||||
ledger = MemoryLedger()
|
||||
flow = AirTextInquiryFlow(ledger)
|
||||
created = ledger.create_ticket(
|
||||
sender_id="u4", 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="已报价",
|
||||
)
|
||||
from agent.channel.queue import MemoryMessageStore
|
||||
from agent.routing.decision import RouteDecision
|
||||
|
||||
store = MemoryMessageStore()
|
||||
msg = InboundMessage(
|
||||
sender_id="u4",
|
||||
message_id="m-cont",
|
||||
content="帮我把那单再看一眼",
|
||||
)
|
||||
phase = handle_continue_thread(
|
||||
msg, thread_id=wo, flow=flow, store=store
|
||||
)
|
||||
self.assertEqual(phase, "wait_adopt")
|
||||
decision = RouteDecision(intent="continue_thread", thread_id=wo)
|
||||
# dispatch 走全局 flow,这里只断言意图表仍指向 continue
|
||||
self.assertEqual(decision.intent, "continue_thread")
|
||||
_ = dispatch_inbound
|
||||
|
||||
def test_adopt_excel_after_restart_stays_on_card_ticket(self) -> None:
|
||||
"""点 0040 方案卡不能落到刚续办的 0037:进程无书签时按卡上工单号从主账收回。"""
|
||||
ledger = MemoryLedger()
|
||||
live = AirTextInquiryFlow(ledger)
|
||||
sink: list[str] = []
|
||||
|
||||
def reply(text: str, extra=None) -> None:
|
||||
sink.append(text)
|
||||
_ = extra
|
||||
|
||||
live.on_text(
|
||||
sender_id="u5",
|
||||
text="空运",
|
||||
reply=reply,
|
||||
injected_facts=COMPLETE_AIR,
|
||||
injected_mode="AIR",
|
||||
)
|
||||
wo_old = live.session_of("u5").work_order_no
|
||||
live.on_button(sender_id="u5", action=copy.BTN_SKIP_COLLAB, reply=reply)
|
||||
other = dict(COMPLETE_AIR)
|
||||
other["品名"] = "服装"
|
||||
live.on_text(
|
||||
sender_id="u5",
|
||||
text="空运",
|
||||
reply=reply,
|
||||
injected_facts=other,
|
||||
injected_mode="AIR",
|
||||
)
|
||||
wo_new = live.session_of("u5").work_order_no
|
||||
self.assertNotEqual(wo_old, wo_new)
|
||||
|
||||
restarted = AirTextInquiryFlow(ledger)
|
||||
restarted.resume_historical(
|
||||
sender_id="u5", work_order_no=wo_new, reply=reply
|
||||
)
|
||||
sink.clear()
|
||||
phase = restarted.on_button(
|
||||
sender_id="u5",
|
||||
action=copy.BTN_ADOPT_EXCEL,
|
||||
reply=reply,
|
||||
card_meta={
|
||||
"event_key": f"adopt_excel:{wo_old}",
|
||||
"task_id": f"scheme-{wo_old}-deadbeef",
|
||||
"response_code": "rc-old-scheme",
|
||||
},
|
||||
)
|
||||
self.assertEqual(phase, "wait_deal")
|
||||
joined = "\n".join(sink)
|
||||
self.assertIn(wo_old, joined)
|
||||
self.assertNotIn(wo_new, joined)
|
||||
self.assertIn("Excel", joined)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
Reference in New Issue
Block a user