Files
inquiry_robot/inquiry-agent/tests/test_continue_ticket.py
T

377 lines
13 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.
"""续办历史工单:模型意图合同 + 回到当时节点。"""
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",
"包装方式": "纸箱",
}
def _until_quoted(flow, **kwargs) -> str:
"""必填齐先出核对卡,单测自动回确定再建单查价。"""
phase = flow.on_text(**kwargs)
if phase == "wait_confirm":
phase = flow.on_text(
sender_id=kwargs["sender_id"],
text="确定",
reply=kwargs["reply"],
)
return phase
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 {})
_until_quoted(
flow,
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.assertFalse(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_skip_collab_after_restart_from_ledger(self) -> None:
"""
测服 WO202609230031:进程重启后主账把空运已报价收成 wait_adopt,
点「不需要协同,直接生成报价单」不能再静默丢掉。
"""
ledger = MemoryLedger()
live = AirTextInquiryFlow(ledger)
sink: list[str] = []
def reply(text: str, extra=None) -> None:
sink.append(text)
_ = extra
created = ledger.create_ticket(
sender_id="u-skip-restart", 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 标准报价",
},
to_status="已报价",
)
view = ledger.get_for_agent(work_order_no=wo, sender_id="u-skip-restart")
self.assertEqual(view.get("wait_phase"), "wait_adopt")
restarted = AirTextInquiryFlow(ledger)
phase = restarted.on_button(
sender_id="u-skip-restart",
action=copy.BTN_SKIP_COLLAB,
reply=reply,
card_meta={
"event_key": f"skip_collab:{wo}",
"task_id": f"price-{wo}-deadbeef",
"response_code": "rc-skip-restart",
},
)
self.assertEqual(phase, "wait_adopt")
joined = "\n".join(sink)
self.assertIn("已跳过内部协同", joined)
self.assertIn(f"{wo}报价方案", joined)
self.assertEqual(restarted.session_of("u-skip-restart").phase, "wait_adopt")
self.assertNotIn(copy.BTN_SKIP_COLLAB, restarted.session_of("u-skip-restart").allowed)
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
_until_quoted(
live,
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["品名"] = "服装"
_until_quoted(
live,
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()