落地陆运群内协同:按填写样例拉群并只列本单协同项,进群把私聊已抽到的字段带上。
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,305 @@
|
||||
"""
|
||||
陆运群内协同:摘要只列本单协同项、补字段过滤、预填。
|
||||
不连企微 / 真实 TMS / 8180。
|
||||
"""
|
||||
|
||||
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.group_collab import handle_group_collab
|
||||
from agent.ledger.memory_ledger import MemoryLedger
|
||||
from agent.policy import inquiry_copy as copy
|
||||
from agent.policy.land_text_flow import LandTextInquiryFlow
|
||||
from agent.routing.group_intent import INTENT_FIELDS, INTENT_QUOTE
|
||||
|
||||
|
||||
class CountingLedger(MemoryLedger):
|
||||
def __init__(self) -> None:
|
||||
super().__init__()
|
||||
self.last_created_no = ""
|
||||
|
||||
def create_ticket(self, **kwargs):
|
||||
out = super().create_ticket(**kwargs)
|
||||
self.last_created_no = str(out.get("work_order_no") or "")
|
||||
return out
|
||||
|
||||
|
||||
class FakeGroupClient:
|
||||
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_land_collab", "error": ""}
|
||||
|
||||
def add_group_members(self, *, chat_id: str, userids: list[str]) -> dict:
|
||||
return {"ok": True}
|
||||
|
||||
def send_group(self, *, chat_id: str, content: str, mention_userids: list | None = None) -> dict:
|
||||
self.sent.append({"chat_id": chat_id, "content": content, "kind": "text"})
|
||||
return {"ok": True}
|
||||
|
||||
def send_group_file(self, *, chat_id: str, filename: str, filepath: str = "", file_b64: str = "") -> dict:
|
||||
self.sent.append({"chat_id": chat_id, "filename": filename, "kind": "file"})
|
||||
return {"ok": True}
|
||||
|
||||
|
||||
class LandGroupCollabTests(unittest.TestCase):
|
||||
def setUp(self) -> None:
|
||||
self.ledger = CountingLedger()
|
||||
self.ledger.set_quote_templates(
|
||||
[
|
||||
{
|
||||
"templateId": "mem-default-land",
|
||||
"name": "陆运默认",
|
||||
"bizType": "陆运",
|
||||
"keywords": [],
|
||||
"isDefault": True,
|
||||
}
|
||||
]
|
||||
)
|
||||
self.ledger.set_land_staff(
|
||||
[
|
||||
{
|
||||
"roleCode": "land",
|
||||
"status": "active",
|
||||
"wecomId": "p_dom",
|
||||
"name": "乙",
|
||||
"routes": "国内长途/零担,国内城配/拖车/打包,中港/中亚/中欧,东南亚",
|
||||
}
|
||||
]
|
||||
)
|
||||
self.groups = FakeGroupClient()
|
||||
self.flow = LandTextInquiryFlow(self.ledger, group_client=self.groups)
|
||||
self.replies: list[str] = []
|
||||
|
||||
def _reply(self, text: str, extra=None) -> str:
|
||||
self.replies.append(text)
|
||||
return ""
|
||||
|
||||
def _group_texts(self) -> str:
|
||||
return "\n".join(x.get("content") or "" for x in self.groups.sent if x.get("kind") == "text")
|
||||
|
||||
def _open(
|
||||
self,
|
||||
*,
|
||||
option_text: str,
|
||||
extra_facts: dict | None = None,
|
||||
cargo: dict | None = None,
|
||||
) -> tuple[str, str]:
|
||||
facts = {
|
||||
"起运港": "广州",
|
||||
"目的港": "深圳",
|
||||
"品名": "衣服",
|
||||
"体积": "100CBM",
|
||||
"毛重": "50kg",
|
||||
}
|
||||
facts.update(cargo or {})
|
||||
facts.update(extra_facts or {})
|
||||
self.flow.on_text(
|
||||
sender_id="u1",
|
||||
text=option_text,
|
||||
reply=self._reply,
|
||||
injected_facts=facts,
|
||||
injected_mode="LAND",
|
||||
)
|
||||
self.flow.on_text(sender_id="u1", text="确定", reply=self._reply)
|
||||
self.replies.clear()
|
||||
phase = self.flow.on_button(
|
||||
sender_id="u1",
|
||||
action=copy.BTN_PULL_COLLAB,
|
||||
reply=self._reply,
|
||||
)
|
||||
self.assertEqual(phase, "collab_group")
|
||||
wo = self.flow.session_of("u1").work_order_no
|
||||
return wo, "wr_land_collab"
|
||||
|
||||
def _msg(self, *, sender: str, text: str, mid: str) -> InboundMessage:
|
||||
return InboundMessage(
|
||||
sender_id=sender,
|
||||
message_id=mid,
|
||||
content=text,
|
||||
msg_type="text",
|
||||
chat_id="wr_land_collab",
|
||||
chat_type="group",
|
||||
)
|
||||
|
||||
def test_ltl_brief_only_four_collab_and_prefill_pack(self) -> None:
|
||||
self._open(
|
||||
option_text="国内运输拼车 国内长途/零担 急件 包装方式纸箱",
|
||||
extra_facts={"包装方式": "纸箱"},
|
||||
)
|
||||
brief = self._group_texts()
|
||||
self.assertIn("包装方式:纸箱", brief)
|
||||
self.assertIn("客户名称:-", brief)
|
||||
self.assertIn("货值:-", brief)
|
||||
self.assertIn("是否为危险品:-", brief)
|
||||
self.assertNotIn("贸易条款", brief)
|
||||
self.assertNotIn("商品海关编码", brief)
|
||||
self.assertNotIn("海关编码", brief)
|
||||
|
||||
def test_domestic_ftl_longhaul_can_pull_and_brief_order(self) -> None:
|
||||
"""国内整车配长途:允许拉群;协同顺序是客户名称、货值、包装方式、是否为危险品。"""
|
||||
self._open(
|
||||
option_text="国内运输整车 国内长途/零担 急件",
|
||||
cargo={"车型/数量": "9米/1"},
|
||||
)
|
||||
self.assertTrue(self.groups.created)
|
||||
brief = self._group_texts()
|
||||
collab_part = brief.split("协同补充字段")[1]
|
||||
self.assertIn("客户名称:-", collab_part)
|
||||
self.assertIn("货值:-", collab_part)
|
||||
self.assertIn("包装方式:-", collab_part)
|
||||
self.assertIn("是否为危险品:-", collab_part)
|
||||
self.assertLess(collab_part.find("客户名称"), collab_part.find("货值"))
|
||||
self.assertLess(collab_part.find("货值"), collab_part.find("包装方式"))
|
||||
self.assertLess(collab_part.find("包装方式"), collab_part.find("是否为危险品"))
|
||||
self.assertNotIn("贸易条款", brief)
|
||||
self.assertNotIn("海关编码", brief)
|
||||
# 车型/数量只在询价字段,不进协同。
|
||||
self.assertIn("车型/数量:9米/1", brief.split("协同补充字段")[0])
|
||||
self.assertNotIn("车型/数量", collab_part)
|
||||
|
||||
def test_cross_ftl_vehicle_stays_inquiry(self) -> None:
|
||||
wo, _ = self._open(
|
||||
option_text="跨境整车 东南亚 急件",
|
||||
cargo={
|
||||
"目的港": "河内",
|
||||
"车型/数量": "9米/1",
|
||||
},
|
||||
)
|
||||
brief = self._group_texts()
|
||||
self.assertIn("车型/数量:9米/1", brief.split("协同补充字段")[0])
|
||||
self.assertNotIn("车型/数量", brief.split("协同补充字段")[1])
|
||||
self.groups.sent.clear()
|
||||
phase = handle_group_collab(
|
||||
self._msg(sender="u1", text="车型/数量 12米/1", mid="v1"),
|
||||
flow=self.flow,
|
||||
ledger=self.ledger,
|
||||
injected_intent=INTENT_FIELDS,
|
||||
)
|
||||
self.assertEqual(phase, "collab_not_recognized")
|
||||
ticket = self.ledger.get_ticket(work_order_no=wo)
|
||||
self.assertEqual(ticket.facts.get("车型/数量"), "9米/1")
|
||||
self.assertFalse(ticket.collab_facts.get("车型/数量"))
|
||||
|
||||
def test_hk_ftl_vehicle_is_collab(self) -> None:
|
||||
wo, _ = self._open(
|
||||
option_text="中港整车 中港/中亚/中欧 急件",
|
||||
cargo={"目的港": "香港"},
|
||||
)
|
||||
brief = self._group_texts()
|
||||
collab_part = brief.split("协同补充字段")[1]
|
||||
self.assertIn("车型/数量:-", collab_part)
|
||||
self.assertNotIn("车型/数量", brief.split("协同补充字段")[0])
|
||||
self.groups.sent.clear()
|
||||
phase = handle_group_collab(
|
||||
self._msg(sender="u1", text="车型/数量 9米/1", mid="hk1"),
|
||||
flow=self.flow,
|
||||
ledger=self.ledger,
|
||||
injected_intent=INTENT_FIELDS,
|
||||
)
|
||||
self.assertEqual(phase, "collab_field_saved")
|
||||
ticket = self.ledger.get_ticket(work_order_no=wo)
|
||||
self.assertEqual(ticket.collab_facts.get("车型/数量"), "9米/1")
|
||||
self.assertFalse(ticket.facts.get("车型/数量"))
|
||||
self.assertIn("车型/数量:9米/1", self._group_texts())
|
||||
|
||||
def test_ltl_rejects_trade_terms_only(self) -> None:
|
||||
self._open(option_text="国内运输拼车 国内长途/零担 急件")
|
||||
self.groups.sent.clear()
|
||||
phase = handle_group_collab(
|
||||
self._msg(sender="u1", text="贸易条款 FOB", mid="t1"),
|
||||
flow=self.flow,
|
||||
ledger=self.ledger,
|
||||
injected_intent=INTENT_FIELDS,
|
||||
)
|
||||
self.assertEqual(phase, "collab_not_recognized")
|
||||
text = self._group_texts()
|
||||
self.assertIn("客户名称", text)
|
||||
self.assertNotIn("货好时间", text)
|
||||
|
||||
def test_ltl_keeps_name_drops_trade_terms(self) -> None:
|
||||
wo, _ = self._open(option_text="国内运输拼车 国内长途/零担 急件")
|
||||
self.groups.sent.clear()
|
||||
phase = handle_group_collab(
|
||||
self._msg(sender="u1", text="客户名称张三,贸易条款 FOB", mid="t2"),
|
||||
flow=self.flow,
|
||||
ledger=self.ledger,
|
||||
injected_intent=INTENT_FIELDS,
|
||||
)
|
||||
self.assertEqual(phase, "collab_field_saved")
|
||||
ticket = self.ledger.get_ticket(work_order_no=wo)
|
||||
self.assertEqual(ticket.collab_facts.get("客户名称"), "张三")
|
||||
self.assertFalse(ticket.collab_facts.get("贸易条款"))
|
||||
self.assertIn("客户名称:张三", self._group_texts())
|
||||
|
||||
def test_dg_and_value_and_bare_pack(self) -> None:
|
||||
wo, _ = self._open(option_text="国内运输拼车 国内长途/零担 急件")
|
||||
handle_group_collab(
|
||||
self._msg(sender="u1", text="普货", mid="dg1"),
|
||||
flow=self.flow,
|
||||
ledger=self.ledger,
|
||||
injected_intent=INTENT_FIELDS,
|
||||
)
|
||||
ticket = self.ledger.get_ticket(work_order_no=wo)
|
||||
self.assertEqual(ticket.collab_facts.get("是否为危险品"), "否")
|
||||
handle_group_collab(
|
||||
self._msg(sender="u1", text="危险品", mid="dg2"),
|
||||
flow=self.flow,
|
||||
ledger=self.ledger,
|
||||
injected_intent=INTENT_FIELDS,
|
||||
)
|
||||
ticket = self.ledger.get_ticket(work_order_no=wo)
|
||||
self.assertEqual(ticket.collab_facts.get("是否为危险品"), "是")
|
||||
handle_group_collab(
|
||||
self._msg(sender="u1", text="货值 10万 包装方式编织袋", mid="v1"),
|
||||
flow=self.flow,
|
||||
ledger=self.ledger,
|
||||
injected_intent=INTENT_FIELDS,
|
||||
)
|
||||
ticket = self.ledger.get_ticket(work_order_no=wo)
|
||||
self.assertEqual(ticket.collab_facts.get("货值"), "10万")
|
||||
self.assertEqual(ticket.collab_facts.get("包装方式"), "编织袋")
|
||||
self.groups.sent.clear()
|
||||
phase = handle_group_collab(
|
||||
self._msg(sender="u1", text="编织袋", mid="v2"),
|
||||
flow=self.flow,
|
||||
ledger=self.ledger,
|
||||
injected_intent=INTENT_FIELDS,
|
||||
)
|
||||
self.assertEqual(phase, "collab_not_recognized")
|
||||
|
||||
def test_product_can_quote_without_collab(self) -> None:
|
||||
wo, _ = self._open(option_text="国内运输拼车 国内长途/零担 急件")
|
||||
phase = handle_group_collab(
|
||||
self._msg(sender="p_dom", text="运费 1000", mid="q1"),
|
||||
flow=self.flow,
|
||||
ledger=self.ledger,
|
||||
injected_intent=INTENT_QUOTE,
|
||||
injected_quote={"total": "1000", "source_label": "产品报价"},
|
||||
)
|
||||
self.assertIn(phase, {"product_quote_sent", "wait_tms_choice"})
|
||||
ticket = self.ledger.get_ticket(work_order_no=wo)
|
||||
self.assertTrue(ticket.quote)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
Reference in New Issue
Block a user