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

350 lines
14 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
from datetime import datetime
from zoneinfo import ZoneInfo
ROOT = os.path.abspath(os.path.join(os.path.dirname(__file__), ".."))
if ROOT not in sys.path:
sys.path.insert(0, ROOT)
from agent.policy import handoff_human as hh
from agent.policy import inquiry_copy as copy
from agent.policy.air_text_flow import FlowSession
class _Sess:
def __init__(self) -> None:
self.clarify_missing_sig = ""
self.clarify_retry = 0
self.handoff_offer = ""
self.work_order_no = ""
self.virtual_work_order_no = ""
class HandoffCoreTests(unittest.TestCase):
def setUp(self) -> None:
hh.reset_virtual_seq_for_test()
def test_same_missing_set_offers_after_three_asks(self) -> None:
sess = _Sess()
keys = ["起运港", "目的港"]
self.assertEqual(hh.note_clarify_round(sess, keys), "ask")
self.assertEqual(sess.clarify_retry, 0)
self.assertEqual(hh.note_clarify_round(sess, keys), "ask")
self.assertEqual(sess.clarify_retry, 1)
self.assertEqual(hh.note_clarify_round(sess, keys), "ask")
self.assertEqual(sess.clarify_retry, 2)
self.assertEqual(hh.note_clarify_round(sess, keys), "handoff")
self.assertEqual(sess.clarify_retry, 3)
def test_changed_missing_resets_and_voids_clarify_offer(self) -> None:
sess = _Sess()
hh.note_clarify_round(sess, ["起运港"])
hh.note_clarify_round(sess, ["起运港"])
sess.handoff_offer = "clarify"
self.assertEqual(hh.note_clarify_round(sess, ["目的港"]), "ask")
self.assertEqual(sess.clarify_retry, 0)
self.assertEqual(sess.handoff_offer, "")
def test_virtual_no_prefix_not_wo(self) -> None:
no = hh.next_virtual_no(now=datetime(2026, 9, 20, tzinfo=ZoneInfo("Asia/Shanghai")))
self.assertTrue(no.startswith("VT20260920"))
self.assertEqual(len(no), 14)
self.assertFalse(no.startswith("WO"))
self.assertTrue(hh.is_virtual_no(no))
self.assertFalse(hh.is_virtual_no("WO202609200001"))
def test_handoff_brief_dashes_collab_and_glues_ticket(self) -> None:
text = copy.handoff_group_brief(
work_order_no="WO202609190018",
facts={
"运输类型": "跨境集拼",
"线路类别": "中港/中亚/中欧",
"运输分类": "普件(东南亚、中亚、中欧涉及境外段清派等特殊要求)",
"起运地": "深圳市",
"目的地": "南京市",
"毛重": "1KG",
"报价日期": "2026-09-19",
},
business_line="LAND",
product_names=["林绵勋", "罗小华", "吴季林"],
)
self.assertIn("@林绵勋 @罗小华 @吴季林", text)
self.assertIn("WO202609190018转人工", text)
self.assertNotIn("工单号:", text.split("转人工")[0][-6:])
self.assertIn("货物品名:-", text)
self.assertIn("客户名称:-", text)
self.assertNotIn("TMS报价", text)
def test_already_collab_copy(self) -> None:
self.assertEqual(copy.HANDOFF_ALREADY_COLLAB, "该工单已拉产品进群协同。")
def test_air_hint_contains_virtual(self) -> None:
text = copy.handoff_air_hint(work_order_no="VT202609200001")
self.assertEqual(
text,
"触发转人工,请手动把航线和智能体拉进原有空运群,并发送工单号 VT202609200001。",
)
self.assertNotIn("拉产品进群", text)
def test_clarify_progress_and_stale_wo(self) -> None:
self.assertTrue(copy.clarify_progress_line(1).startswith("参数提取不全(1/3)"))
self.assertEqual(copy.with_clarify_progress("请确认并补充以下信息:", 0), "请确认并补充以下信息:")
wrapped = copy.with_clarify_progress("请确认并补充以下信息:", 2)
self.assertTrue(wrapped.startswith("参数提取不全(2/3)"))
self.assertIn("请确认并补充以下信息", wrapped)
sess = _Sess()
sess.work_order_no = "WO202609180018"
sess.phase = "clarify"
no = hh.ticket_no_for_handoff_offer(sess)
self.assertTrue(str(no).startswith("VT"))
self.assertEqual(sess.work_order_no, "")
kept = _Sess()
kept.work_order_no = "WO202609180018"
kept.phase = "wait_collab"
self.assertEqual(hh.ticket_no_for_handoff_offer(kept), "WO202609180018")
class HandoffFlowOfferTests(unittest.TestCase):
"""海运:首次补问不计数,之后连问三次仍不齐出卡;点卡前仍可继续补字段。"""
def setUp(self) -> None:
hh.reset_virtual_seq_for_test()
from agent.policy.sea_text_flow import reset_sea_text_flow_for_test
self.flow = reset_sea_text_flow_for_test()
self.replies: list[str] = []
def reply(text: str, extra=None):
self.replies.append(text)
return "out"
self.reply = reply
def _partial(self, text: str) -> str:
return self.flow.on_text(sender_id="s1", text=text, reply=self.reply)
def test_three_same_clarify_then_card(self) -> None:
self._partial("海运询价")
self._partial("还是那些")
self._partial("不知道")
phase = self._partial("还没有")
self.assertEqual(phase, "handoff_offer")
joined = "\n".join(self.replies)
self.assertIn("参数提取不全(3/3)", joined)
self.assertIn(copy.BTN_PULL_COLLAB, joined)
idx3 = next(i for i, t in enumerate(self.replies) if "参数提取不全(3/3)" in t)
self.assertLess(idx3 + 1, len(self.replies))
self.assertIn(copy.BTN_PULL_COLLAB, self.replies[idx3 + 1])
sess = self.flow.session_of("s1")
self.assertEqual(sess.handoff_offer, "clarify")
self.assertIn(copy.BTN_PULL_COLLAB, sess.allowed)
def test_click_pull_sends_handoff_brief_without_tms(self) -> None:
class FakeGroup:
def __init__(self) -> None:
self.created: list[dict] = []
self.sent: list[dict] = []
def create_group(self, *, name: str, userids: list[str]) -> dict:
self.created.append({"name": name, "userids": list(userids)})
return {"ok": True, "chat_id": "wr_h1", "error": ""}
def send_group(self, *, chat_id: str, content: str, mention_userids=None) -> dict:
self.sent.append({"chat_id": chat_id, "content": content})
return {"ok": True}
def send_group_file(self, **kwargs) -> dict:
return {"ok": True}
groups = FakeGroup()
self.flow._group_client = groups
self.flow.ledger.set_sea_staff(
[
{
"name": "林绵勋",
"wecomId": "prod1",
"roleCode": "sea",
"routes": "海运·全部线路",
"status": "active",
}
]
)
for text in ("海运询价", "还是那些", "不知道", "还没有"):
self._partial(text)
phase = self.flow.on_button(
sender_id="s1",
action=copy.BTN_PULL_COLLAB,
reply=self.reply,
)
self.assertEqual(phase, "handoff_done")
self.assertTrue(groups.created)
self.assertTrue(groups.created[0]["name"].startswith("VT"))
brief = groups.sent[0]["content"]
self.assertIn("转人工", brief)
self.assertNotIn("TMS报价", brief)
sess = self.flow.session_of("s1")
self.assertTrue(sess.handoff_committed)
self.assertTrue(sess.work_order_no == "" or sess.status == "转人工")
def test_new_inquiry_after_commit_starts_fresh(self) -> None:
class FakeGroup:
def create_group(self, *, name: str, userids: list[str]) -> dict:
return {"ok": True, "chat_id": "wr_h1", "error": ""}
def send_group(self, *, chat_id: str, content: str, mention_userids=None) -> dict:
return {"ok": True}
def send_group_file(self, **kwargs) -> dict:
return {"ok": True}
self.flow._group_client = FakeGroup()
self.flow.ledger.set_sea_staff(
[
{
"name": "林绵勋",
"wecomId": "prod1",
"roleCode": "sea",
"routes": "海运·全部线路",
"status": "active",
}
]
)
for text in ("海运询价", "还是那些", "不知道", "还没有"):
self._partial(text)
self.flow.on_button(
sender_id="s1",
action=copy.BTN_PULL_COLLAB,
reply=self.reply,
)
self.replies.clear()
phase = self._partial("海运询价")
self.assertNotEqual(phase, "handoff_done")
joined = "\n".join(self.replies)
self.assertNotIn("已转人工,本单不再自动跟进", joined)
self.assertIn("请确认并补充以下信息", joined)
sess = self.flow.session_of("s1")
self.assertFalse(sess.handoff_committed)
self.assertEqual(sess.phase, "clarify")
def test_already_has_group_only_prompts(self) -> None:
self.flow._save(
FlowSession(
sender_id="s1",
thread_id="s1",
phase="collab_group",
business_line="SEA",
work_order_no="WO202609200099",
collab_chat_id="wr_exist",
status="已报价",
)
)
phase = self.flow.offer_handoff(sender_id="s1", text="转人工", reply=self.reply)
self.assertEqual(phase, "handoff_already")
self.assertEqual(self.replies[-1], copy.HANDOFF_ALREADY_COLLAB)
self.assertFalse(self.flow.session_of("s1").handoff_committed)
def test_route_contract_accepts_handoff_human(self) -> None:
from agent.routing.deepseek_a import DeepSeekARouter
from agent.routing.ordered_actions import ACTION_INTENTS, parse_ordered_actions_arguments
self.assertEqual(ACTION_INTENTS.get("HANDOFF_HUMAN"), "handoff_human")
parsed = parse_ordered_actions_arguments(
{
"decision_id": "d-h",
"intent": "handoff_human",
"ordered_action": {
"action_type": "HANDOFF_HUMAN",
"order": 1,
"target": {"work_order_no": ""},
"evidence": {"source_ref": "inbound:u1", "content_digest": "a" * 64},
},
}
)
self.assertEqual(parsed.to_route_decision().intent, "handoff_human")
router = DeepSeekARouter.from_settings()
sys_msg, _user = router.build_messages(text="帮我转人工", sender_id="u1")
self.assertIn("handoff_human", sys_msg["content"])
self.assertIn("口令不写死", sys_msg["content"])
self.assertIn("抱怨慢", sys_msg["content"])
def test_first_clarify_does_not_count(self) -> None:
self._partial("海运询价")
self.assertTrue(self.replies)
self.assertNotIn("参数提取不全(1/3)", self.replies[0])
self.assertIn("请确认并补充以下信息", self.replies[0])
self._partial("还是那些")
self.assertTrue(self.replies[-1].startswith("参数提取不全(1/3)"))
def test_air_handoff_before_create_uses_virtual_not_old_wo(self) -> None:
from agent.policy.air_text_flow import reset_air_text_flow_for_test
flow = reset_air_text_flow_for_test()
replies: list[str] = []
sess = FlowSession(
sender_id="air1",
thread_id="air1",
phase="clarify",
business_line="AIR",
work_order_no="WO202609180018",
facts={"起运港": "ZUH", "目的港": "CRK", "品名": "衣服"},
)
flow._save(sess)
phase = flow._offer_handoff_now(
sender_id="air1",
reason="clarify",
reply=lambda t, extra=None: replies.append(t) or "out",
sess=sess,
)
self.assertEqual(phase, "handoff_offer")
self.assertTrue(replies)
self.assertIn("VT", replies[0])
self.assertNotIn("WO202609180018", replies[0])
self.assertTrue((sess.virtual_work_order_no or "").startswith("VT"))
self.assertEqual(sess.work_order_no, "")
def test_third_clarify_sends_card_even_if_routed_as_adjust(self) -> None:
from agent.channel.queue import get_message_store
from agent.channel.wecom.models import InboundMessage
from agent.handlers.text_inquiry import handle_text_inquiry
from agent.routing.decision import RouteDecision
from agent.routing.dispatch import dispatch_inbound
store = get_message_store()
handle_text_inquiry(
InboundMessage(sender_id="s1", message_id="h1", content="海运询价"),
store=store,
flow=self.flow,
)
handle_text_inquiry(
InboundMessage(sender_id="s1", message_id="h2", content="还是那些"),
store=store,
flow=self.flow,
)
handle_text_inquiry(
InboundMessage(sender_id="s1", message_id="h3", content="不知道"),
store=store,
flow=self.flow,
)
name = dispatch_inbound(
InboundMessage(sender_id="s1", message_id="h4", content="1"),
RouteDecision(intent="quote_adjust"),
)
self.assertEqual(name, "quote_adjust")
sess = self.flow.session_of("s1")
self.assertEqual(sess.phase, "handoff_offer")
bodies = "\n".join(i.content for i in store._outbox.values())
self.assertIn("参数提取不全(3/3)", bodies)
self.assertIn(copy.BTN_PULL_COLLAB, bodies)
if __name__ == "__main__":
unittest.main()