314 lines
11 KiB
Python
314 lines
11 KiB
Python
"""续办历史工单:模型意图合同 + 回到当时节点。"""
|
|
|
|
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 setUp(self) -> None:
|
|
from agent.redis_coord.runtime import shutdown_redis_runtime
|
|
|
|
shutdown_redis_runtime()
|
|
|
|
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_after_quote_file_is_deal(self) -> None:
|
|
ledger = MemoryLedger()
|
|
created = ledger.create_ticket(
|
|
sender_id="u6", 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="u6", work_order_no=wo, reply=reply
|
|
)
|
|
self.assertEqual(phase, "wait_deal")
|
|
self.assertTrue(any("成交" in x or copy.BTN_DEAL 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()
|