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

444 lines
19 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.
"""
陆运群内协同:摘要只列本单协同项、补字段过滤、预填。
不连企微 / 真实 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_ADJUST, 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)
def test_sales_adjust_sends_excel_and_deal(self) -> None:
wo, _ = self._open(option_text="国内运输拼车 国内长途/零担 急件")
self.groups.sent.clear()
phase = handle_group_collab(
self._msg(sender="u1", text="空运费加 10%", mid="adj1"),
flow=self.flow,
ledger=self.ledger,
injected_intent=INTENT_ADJUST,
)
self.assertEqual(phase, "group_adjusted")
texts = self._group_texts()
self.assertIn("已识别调价指令", texts)
self.assertIn("调价后价格", texts)
self.assertTrue(any(x.get("kind") == "file" for x in self.groups.sent))
ticket = self.ledger.get_ticket(work_order_no=wo)
self.assertEqual(ticket.status, "已报价")
def test_product_fee_change_is_quote_not_adjust(self) -> None:
"""产品写「报关费改为300」是手工报价。已有报价后也不能被拦成只有销售能调价。"""
from agent.routing.group_intent import classify_group_text
self.assertEqual(classify_group_text("报关费改为300CNY", allow_network=False), INTENT_QUOTE)
self.assertEqual(classify_group_text("运费加 10%", allow_network=False), INTENT_ADJUST)
wo, _ = self._open(option_text="国内运输拼车 国内长途/零担 急件")
handle_group_collab(
self._msg(sender="p_dom", text="运费 1000", mid="q_fee0"),
flow=self.flow,
ledger=self.ledger,
injected_intent=INTENT_QUOTE,
injected_quote={"total": "1000", "source_label": "产品报价"},
)
self.groups.sent.clear()
phase = handle_group_collab(
self._msg(sender="p_dom", text="报关费改为300CNY", mid="q_fee1"),
flow=self.flow,
ledger=self.ledger,
)
self.assertNotEqual(phase, "adjust_not_sales")
self.assertNotIn("只有这张单的销售能调价", self._group_texts())
ticket = self.ledger.get_ticket(work_order_no=wo)
self.assertTrue(ticket.quote)
def test_product_cannot_adjust(self) -> None:
self._open(option_text="国内运输拼车 国内长途/零担 急件")
self.groups.sent.clear()
phase = handle_group_collab(
self._msg(sender="p_dom", text="空运费减 5%", mid="adj2"),
flow=self.flow,
ledger=self.ledger,
injected_intent=INTENT_ADJUST,
)
self.assertEqual(phase, "adjust_not_sales")
self.assertIn("只有这张单的销售能调价。产品请继续报价,价格未改。", self._group_texts())
def test_air_room_adjust_names_airline(self) -> None:
self.assertEqual(
copy.adjust_only_sales(air_room=True),
"只有这张单的销售能调价。航线请继续报价,价格未改。",
)
def test_land_bare_price_defaults_to_land_fee(self) -> None:
"""陆运「价格为1RMB/KG」无费用名时默认陆运费,禁止落成海运费(WO118)。"""
from agent.llm.extract_text import extract_product_quote
land = extract_product_quote("价格为1RMB/KG", business_line="LAND")
rows = list(land.get("fee_rows") or [])
self.assertTrue(rows)
self.assertEqual(str(rows[0].get("name") or ""), "陆运费")
self.assertEqual(str(land.get("total") or ""), "1")
sea = extract_product_quote("价格为1RMB/KG", business_line="SEA")
self.assertEqual(str((sea.get("fee_rows") or [{}])[0].get("name") or ""), "海运费")
air = extract_product_quote("价格为1RMB/KG", business_line="AIR")
self.assertEqual(str((air.get("fee_rows") or [{}])[0].get("name") or ""), "空运费")
def test_land_adjust_without_currency_inherits_tms_hkd(self) -> None:
"""WO202610090107:TMS 港币,产品只写「运输费调整为1100」时继承 HKD,禁止默认 USD。"""
from agent.llm.extract_text import extract_product_quote
from agent.policy.sea_group_ops import _quote_currency_hint, merge_quote_fees
tms = {
"currency": "HKD",
"cost": "HKD 1000",
"total": "1000",
"source": "TMS",
"fee_lines": [
{
"name": "运输费",
"amount": "1000",
"currency": "HKD",
}
],
"landSegments": [
{
"origin": "广东省-深圳市-福田区",
"destination": "香港特别行政区",
"currency": "HKD",
"costPrice": "1000",
}
],
}
self.assertEqual(_quote_currency_hint(tms), "HKD")
# 旧行为:无 baseline 会落到 USD
legacy = extract_product_quote("运输费调整为1100", business_line="LAND")
self.assertNotEqual(str(legacy.get("currency") or ""), "USD")
quote = extract_product_quote(
"运输费调整为1100",
business_line="LAND",
baseline_currency=_quote_currency_hint(tms),
)
self.assertEqual(str(quote.get("currency") or ""), "HKD")
self.assertEqual(str((quote.get("fee_lines") or [{}])[0].get("currency") or ""), "HKD")
self.assertEqual(str((quote.get("fee_lines") or [{}])[0].get("amount") or ""), "1100")
merged = merge_quote_fees(tms, quote)
self.assertEqual(str((merged.get("fee_lines") or [{}])[0].get("currency") or ""), "HKD")
self.assertEqual(str(merged.get("currency") or ""), "HKD")
def test_land_requote_replaces_wrong_sea_fee_name(self) -> None:
"""旧陆运产品价误落海运费时,重报陆运费应盖掉而不是并排两行。"""
from agent.policy.sea_group_ops import _align_wrong_default_freight, merge_quote_fees
base = {
"fee_rows": [{"name": "海运费", "amount": "1", "currency": "CNY"}],
"fee_lines": [{"name": "海运费", "amount": "1", "currency": "CNY"}],
"total": "1",
"source_label": "产品报价",
}
overlay = {
"fee_rows": [{"name": "陆运费", "amount": "2", "currency": "CNY"}],
"fee_lines": [{"name": "陆运费", "amount": "2", "currency": "CNY"}],
"total": "2",
"source_label": "产品报价",
}
aligned = _align_wrong_default_freight(base, overlay, business_line="LAND")
merged = merge_quote_fees(aligned, overlay)
names = [str(x.get("name") or "") for x in (merged.get("fee_rows") or [])]
self.assertEqual(names, ["陆运费"])
self.assertEqual(str((merged.get("fee_rows") or [{}])[0].get("amount") or ""), "2")
if __name__ == "__main__":
unittest.main()