feat: upgrade客服多店铺分流、批量报价与稳定性防护
This commit is contained in:
66
tests/test_batch_quote_reply_format.py
Normal file
66
tests/test_batch_quote_reply_format.py
Normal file
@@ -0,0 +1,66 @@
|
||||
import unittest
|
||||
from unittest.mock import AsyncMock, patch
|
||||
|
||||
from core.pydantic_ai_agent import CustomerMessage, CustomerServiceAgent
|
||||
|
||||
|
||||
class BatchQuoteReplyFormatTest(unittest.IsolatedAsyncioTestCase):
|
||||
async def test_batch_reply_contains_per_image_and_options(self):
|
||||
agent = CustomerServiceAgent()
|
||||
cid = "__batch_quote_case__"
|
||||
st = agent._get_conversation_state(cid)
|
||||
st.pending_image_urls = ["https://img.alicdn.com/a.jpg", "https://img.alicdn.com/b.jpg"]
|
||||
st.pending_requirements = ["去背景", "加急"]
|
||||
|
||||
msg = CustomerMessage(
|
||||
msg_id="m-batch-1",
|
||||
acc_id="test_shop",
|
||||
msg="发完了,统一报价",
|
||||
from_id=cid,
|
||||
from_name="t",
|
||||
cy_id=cid,
|
||||
acc_type="AliWorkbench",
|
||||
msg_type=0,
|
||||
cy_name="t",
|
||||
goods_name="专业找图",
|
||||
goods_order="",
|
||||
)
|
||||
|
||||
fake_r1 = {
|
||||
"complexity": "normal",
|
||||
"reason": "常规处理",
|
||||
"price_min": 15,
|
||||
"price_max": 25,
|
||||
"price_suggest": 20,
|
||||
"feasibility": "yes",
|
||||
"risk": "low",
|
||||
"aspect_ratio": "1:1",
|
||||
"perspective": "no",
|
||||
}
|
||||
fake_r2 = {
|
||||
"complexity": "complex",
|
||||
"reason": "细节较多",
|
||||
"price_min": 20,
|
||||
"price_max": 30,
|
||||
"price_suggest": 25,
|
||||
"feasibility": "yes",
|
||||
"risk": "low",
|
||||
"aspect_ratio": "1:1",
|
||||
"perspective": "no",
|
||||
}
|
||||
|
||||
with patch("image.image_analyzer.image_analyzer.analyze", new=AsyncMock(side_effect=[fake_r1, fake_r2])):
|
||||
with patch("core.workflow.workflow.image_analysis_result", new=AsyncMock(return_value=None)):
|
||||
res = await agent._quote_pending_images(st, msg)
|
||||
|
||||
self.assertFalse(res.get("need_transfer", False))
|
||||
reply = res.get("reply", "")
|
||||
self.assertIn("图1", reply)
|
||||
self.assertIn("图2", reply)
|
||||
self.assertIn("可选", reply)
|
||||
self.assertIn("打包", reply)
|
||||
self.assertIn("共", reply)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main(verbosity=2)
|
||||
40
tests/test_outbound_cooldown.py
Normal file
40
tests/test_outbound_cooldown.py
Normal file
@@ -0,0 +1,40 @@
|
||||
import os
|
||||
import unittest
|
||||
|
||||
from websockets.protocol import State
|
||||
|
||||
from core.websocket_client import QingjianAPIClient
|
||||
|
||||
|
||||
class _DummyWS:
|
||||
def __init__(self):
|
||||
self.state = State.OPEN
|
||||
self.sent = []
|
||||
|
||||
async def send(self, msg_json: str):
|
||||
self.sent.append(msg_json)
|
||||
|
||||
|
||||
class OutboundCooldownTest(unittest.IsolatedAsyncioTestCase):
|
||||
def setUp(self):
|
||||
os.environ["OUTBOUND_PER_CUSTOMER_COOLDOWN_SECONDS"] = "5"
|
||||
|
||||
async def test_skip_second_reply_within_cooldown(self):
|
||||
c = QingjianAPIClient(enable_agent=False)
|
||||
c.websocket = _DummyWS()
|
||||
msg = {
|
||||
"acc_id": "shop_a",
|
||||
"from_id": "u001",
|
||||
"from_name": "u001",
|
||||
"acc_type": "AliWorkbench",
|
||||
}
|
||||
await c.send_reply(msg, "第一条")
|
||||
await c.send_reply(msg, "第二条")
|
||||
self.assertEqual(len(c.websocket.sent), 1)
|
||||
|
||||
def tearDown(self):
|
||||
os.environ.pop("OUTBOUND_PER_CUSTOMER_COOLDOWN_SECONDS", None)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main(verbosity=2)
|
||||
34
tests/test_oversize_guard.py
Normal file
34
tests/test_oversize_guard.py
Normal file
@@ -0,0 +1,34 @@
|
||||
import os
|
||||
import unittest
|
||||
|
||||
from core.websocket_client import QingjianAPIClient
|
||||
|
||||
|
||||
class OversizeGuardTest(unittest.TestCase):
|
||||
def setUp(self):
|
||||
os.environ["MAX_SERVICE_SIZE_LONGEST_METERS"] = "10"
|
||||
os.environ["MAX_SERVICE_SIZE_AREA_SQM"] = "20"
|
||||
|
||||
def test_extract_size_pairs(self):
|
||||
c = QingjianAPIClient(enable_agent=False)
|
||||
pairs = c._extract_size_pairs_m("15*6.4米 高度")
|
||||
self.assertTrue(len(pairs) >= 1)
|
||||
self.assertEqual(pairs[0], (15.0, 6.4))
|
||||
|
||||
def test_oversize_hits(self):
|
||||
c = QingjianAPIClient(enable_agent=False)
|
||||
r = c._oversize_reply_if_needed("15*6.4米")
|
||||
self.assertIn("做不了", r)
|
||||
|
||||
def test_normal_size_not_hit(self):
|
||||
c = QingjianAPIClient(enable_agent=False)
|
||||
r = c._oversize_reply_if_needed("2.4*1.2米")
|
||||
self.assertEqual(r, "")
|
||||
|
||||
def tearDown(self):
|
||||
os.environ.pop("MAX_SERVICE_SIZE_LONGEST_METERS", None)
|
||||
os.environ.pop("MAX_SERVICE_SIZE_AREA_SQM", None)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main(verbosity=2)
|
||||
76
tests/test_regression_pipeline.py
Normal file
76
tests/test_regression_pipeline.py
Normal file
@@ -0,0 +1,76 @@
|
||||
import os
|
||||
import unittest
|
||||
from unittest.mock import AsyncMock
|
||||
|
||||
from core.pydantic_ai_agent import CustomerServiceAgent, CustomerMessage
|
||||
from db.customer_db import db
|
||||
|
||||
|
||||
class RegressionPipelineTest(unittest.IsolatedAsyncioTestCase):
|
||||
def setUp(self):
|
||||
self.customer_id = "__regression_test_customer__"
|
||||
db.clear_pending_quote_state(self.customer_id)
|
||||
os.environ["FEATURE_BATCH_QUOTE_ENABLED"] = "true"
|
||||
os.environ["FEATURE_BATCH_QUOTE_PERCENT"] = "100"
|
||||
os.environ["FEATURE_BATCH_QUOTE_SHOPS"] = ""
|
||||
|
||||
async def test_collect_images_then_ack(self):
|
||||
agent = CustomerServiceAgent()
|
||||
msg = CustomerMessage(
|
||||
msg_id="m1",
|
||||
acc_id="test_shop",
|
||||
msg="https://img.alicdn.com/a.jpg#*#https://img.alicdn.com/b.jpg",
|
||||
from_id=self.customer_id,
|
||||
from_name="t",
|
||||
cy_id=self.customer_id,
|
||||
acc_type="AliWorkbench",
|
||||
msg_type=0,
|
||||
cy_name="t",
|
||||
goods_name="专业找图",
|
||||
goods_order="",
|
||||
)
|
||||
resp = await agent.process_message(msg)
|
||||
self.assertTrue(resp.should_reply)
|
||||
self.assertIn("张", resp.reply)
|
||||
st = agent._get_conversation_state(self.customer_id)
|
||||
self.assertEqual(len(st.pending_image_urls), 2)
|
||||
|
||||
async def test_finish_signal_triggers_batch_quote(self):
|
||||
agent = CustomerServiceAgent()
|
||||
st = agent._get_conversation_state(self.customer_id)
|
||||
st.pending_image_urls = ["https://img.alicdn.com/a.jpg"]
|
||||
st.pending_requirements = ["去背景"]
|
||||
agent._sync_pending_quote_state(self.customer_id, st)
|
||||
agent._quote_pending_images = AsyncMock(return_value={"reply": "打包15元,确认我就安排", "need_transfer": False})
|
||||
|
||||
msg = CustomerMessage(
|
||||
msg_id="m2",
|
||||
acc_id="test_shop",
|
||||
msg="发完了,报价吧",
|
||||
from_id=self.customer_id,
|
||||
from_name="t",
|
||||
cy_id=self.customer_id,
|
||||
acc_type="AliWorkbench",
|
||||
msg_type=0,
|
||||
cy_name="t",
|
||||
goods_name="专业找图",
|
||||
goods_order="",
|
||||
)
|
||||
resp = await agent.process_message(msg)
|
||||
self.assertTrue(resp.should_reply)
|
||||
self.assertIn("15", resp.reply)
|
||||
agent._quote_pending_images.assert_awaited()
|
||||
|
||||
async def test_pending_state_restore(self):
|
||||
db.update_pending_quote_state(self.customer_id, ["u1", "u2"], ["r1"])
|
||||
agent = CustomerServiceAgent()
|
||||
st = agent._get_conversation_state(self.customer_id)
|
||||
self.assertEqual(st.pending_image_urls, ["u1", "u2"])
|
||||
self.assertEqual(st.pending_requirements, ["r1"])
|
||||
|
||||
def tearDown(self):
|
||||
db.clear_pending_quote_state(self.customer_id)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main(verbosity=2)
|
||||
70
tests/test_system_inquiry_rules.py
Normal file
70
tests/test_system_inquiry_rules.py
Normal file
@@ -0,0 +1,70 @@
|
||||
import os
|
||||
import unittest
|
||||
from unittest.mock import AsyncMock
|
||||
|
||||
from core.websocket_client import QingjianAPIClient
|
||||
|
||||
|
||||
class SystemInquiryRulesTest(unittest.IsolatedAsyncioTestCase):
|
||||
def setUp(self):
|
||||
self.rules = {
|
||||
"enabled": True,
|
||||
"default_action": "silent",
|
||||
"default_reply": "已收到",
|
||||
"sender_keywords": ["系统客服", "官方客服"],
|
||||
"message_keywords": ["系统询单", "代客咨询"],
|
||||
"shops": {
|
||||
"shop_reply": {
|
||||
"enabled": True,
|
||||
"action": "reply",
|
||||
"reply": "店铺回复模板",
|
||||
"sender_keywords": ["机器人客服"],
|
||||
"message_keywords": ["询单"],
|
||||
}
|
||||
},
|
||||
}
|
||||
os.environ["SYSTEM_INQUIRY_ENABLED"] = "true"
|
||||
os.environ["SYSTEM_INQUIRY_SHOPS"] = ""
|
||||
|
||||
async def test_detect_by_sender_keyword(self):
|
||||
client = QingjianAPIClient(enable_agent=False)
|
||||
client._system_inquiry_rules = self.rules
|
||||
policy = client._resolve_system_inquiry_policy("shop_a")
|
||||
data = {"acc_id": "shop_a", "from_name": "平台系统客服", "from_id": "kefu001", "msg": "你好"}
|
||||
self.assertTrue(client._match_system_inquiry(data, policy))
|
||||
|
||||
async def test_shop_rule_reply_action(self):
|
||||
client = QingjianAPIClient(enable_agent=False)
|
||||
client._system_inquiry_rules = self.rules
|
||||
client.send_reply = AsyncMock()
|
||||
client.transfer_to_human = AsyncMock()
|
||||
|
||||
data = {
|
||||
"acc_id": "shop_reply",
|
||||
"from_name": "机器人客服A",
|
||||
"from_id": "robot_01",
|
||||
"msg": "有个询单请处理",
|
||||
"acc_type": "AliWorkbench",
|
||||
}
|
||||
handled = await client._handle_system_inquiry(data)
|
||||
self.assertTrue(handled)
|
||||
client.send_reply.assert_awaited_once()
|
||||
client.transfer_to_human.assert_not_awaited()
|
||||
|
||||
async def test_shop_whitelist_blocks_other_shops(self):
|
||||
os.environ["SYSTEM_INQUIRY_SHOPS"] = "shop_only"
|
||||
client = QingjianAPIClient(enable_agent=False)
|
||||
client._system_inquiry_rules = self.rules
|
||||
client.send_reply = AsyncMock()
|
||||
data = {"acc_id": "shop_other", "from_name": "系统客服", "from_id": "sys_1", "msg": "系统询单"}
|
||||
handled = await client._handle_system_inquiry(data)
|
||||
self.assertFalse(handled)
|
||||
client.send_reply.assert_not_awaited()
|
||||
|
||||
def tearDown(self):
|
||||
for k in ("SYSTEM_INQUIRY_ENABLED", "SYSTEM_INQUIRY_SHOPS"):
|
||||
os.environ.pop(k, None)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main(verbosity=2)
|
||||
20
tests/test_transfer_greeting_context.py
Normal file
20
tests/test_transfer_greeting_context.py
Normal file
@@ -0,0 +1,20 @@
|
||||
import unittest
|
||||
|
||||
from core.websocket_client import QingjianAPIClient
|
||||
|
||||
|
||||
class TransferGreetingContextTest(unittest.TestCase):
|
||||
def test_transfer_greeting_is_non_empty(self):
|
||||
c = QingjianAPIClient(enable_agent=False)
|
||||
text = c._pick_transfer_greeting()
|
||||
self.assertTrue(isinstance(text, str) and len(text) > 0)
|
||||
|
||||
def test_transfer_greeting_contains_presence_phrase(self):
|
||||
c = QingjianAPIClient(enable_agent=False)
|
||||
for _ in range(10):
|
||||
text = c._pick_transfer_greeting()
|
||||
self.assertTrue(("在" in text) or ("我在" in text))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main(verbosity=2)
|
||||
Reference in New Issue
Block a user