From 1c1b870d2b1b2cf427c047f16ee716ab0cf2a731 Mon Sep 17 00:00:00 2001 From: jimi <1847930177@qq.com> Date: Sun, 1 Mar 2026 13:01:10 +0800 Subject: [PATCH] feat: enforce activity logs and tighten sizing/map reply policies --- core/pydantic_ai_agent.py | 122 ++++++++++++++++++++-- core/websocket_client.py | 112 +++++++++++++++++++- image/image_analyzer.py | 11 +- skills/customer-service/SKILL.md | 2 + skills/customer-service/customer-skill.md | 2 + skills/pre-sales-skill/SKILL.md | 3 +- skills/risk-skill/SKILL.md | 2 +- tests/test_regression_pipeline.py | 21 ++++ utils/content_filter.py | 4 + 9 files changed, 260 insertions(+), 19 deletions(-) diff --git a/core/pydantic_ai_agent.py b/core/pydantic_ai_agent.py index bf02ecf..a53e773 100755 --- a/core/pydantic_ai_agent.py +++ b/core/pydantic_ai_agent.py @@ -12,10 +12,11 @@ import random import hashlib import re import json +import logging from pathlib import Path from typing import Optional, Dict, List, Any, Tuple from datetime import datetime -from pydantic import BaseModel, Field +from pydantic import BaseModel, Field, model_validator from pydantic_ai import Agent, RunContext from pydantic_ai.models.openai import OpenAIChatModel from pydantic_ai.providers.openai import OpenAIProvider @@ -30,6 +31,7 @@ from core.workflow_router import get_workflow_router # ========== 企业微信通知 ========== _WECHAT_WEBHOOK = os.getenv("WECHAT_WEBHOOK", "") +logger = logging.getLogger("cs_agent") async def _notify_wechat(content: str, tag: str = "通知"): @@ -64,6 +66,21 @@ async def _notify_wechat_overdue(): # ========== 转接常量 ========== TRANSFER_MESSAGE = "话术|[转移会话],分组20252916034,无原因" CASE_LIBRARY_LINK = "https://www.yuque.com/zuowei-dfvpq/kge0in/mynala0g35b8cec5" +TAOBAO_REPLY_TAILS = ("嗯", "哦", "好的", "嗯咯", "嗯啦") + + +def _ensure_taobao_reply_tail(text: str) -> str: + """淘宝口吻收尾:最终回复结尾带简短口语尾词。""" + t = (text or "").strip() + if not t: + return "" + transfer_keywords = ("TRANSFER_REQUESTED", "[转移会话]", "转移会话") + if any(k in t for k in transfer_keywords): + return t + trimmed = t.rstrip(" \t\r\n。.!!?~~") + if any(trimmed.endswith(tail) for tail in TAOBAO_REPLY_TAILS): + return trimmed + return f"{trimmed} 好的" # ========== 数据模型 ========== @@ -117,6 +134,12 @@ class AgentResponse(BaseModel): need_transfer: bool = False # 是否需要转人工 transfer_msg: str = "" # 转接消息 + @model_validator(mode="after") + def _ensure_reply_tail(self): + if self.should_reply and self.reply: + self.reply = _ensure_taobao_reply_tail(self.reply) + return self + def _get_shop_type(acc_id: str = "", goods_name: str = "") -> str: """根据 acc_id 或 goods_name 判断店铺类型,返回 gemini_api / find_image / default""" @@ -173,6 +196,19 @@ class CustomerServiceAgent: C_MUTED = "\033[90m" # gray _DEFAULT_EVOLUTION_CANDIDATE = Path("config") / "evolution_candidate.json" + @staticmethod + def _activity_log(event: str, **kwargs): + safe = {} + for k, v in kwargs.items(): + if isinstance(v, str): + safe[k] = v[:240] + else: + safe[k] = v + try: + logger.info(f"[ACTIVITY] event={event} data={json.dumps(safe, ensure_ascii=False)}") + except Exception: + logger.info(f"[ACTIVITY] event={event} data={safe}") + def __init__(self, skills_dir: str = "skills"): self.api_key = os.getenv("OPENAI_API_KEY") self.base_url = os.getenv("OPENAI_BASE_URL", "https://api.openai.com/v1") @@ -1330,7 +1366,7 @@ class CustomerServiceAgent: def _get_risk_prompt(self) -> str: base = """你是淘宝客服的风控助手,负责敏感/违规内容的前置拦截与替代话术。 规则: -- 黄色/擦边/涉政/政治人物/政治事件/政治图片等不接单,礼貌拒绝 +- 黄色/擦边/涉政/政治人物/政治事件/政治图片/地图类内容等不接单,礼貌拒绝 - 输出不超过1句话""" return self._attach_skill_docs(base, self.skill_risk, self.skill_style) @@ -1353,6 +1389,19 @@ class CustomerServiceAgent: # 兜底:类似“有没有十大元帅的照片/图片” return bool(re.search(r"(元帅|将军|领导人|政治人物|政治事件).*(照片|图片|头像|原图)?", s)) + @staticmethod + def _is_map_inquiry(text: str) -> bool: + """地图类需求一律拒绝(按业务规则)。""" + s = (text or "").strip().lower() + if not s: + return False + kw = ( + "地图", "地形图", "行政区划图", "世界地图", "中国地图", + "卫星地图", "导航图", "航海图", "作战地图", "军事地图", + "map", "topographic map", "satellite map", + ) + return any(k in s for k in kw) + def _get_customer_profile_context(self, customer_id: str) -> str: """从数据库读取客户画像,注入给 AI。含个性化语气、报价策略、主动预测、近期对话。""" try: @@ -1601,6 +1650,13 @@ class CustomerServiceAgent: async def process_message(self, message: CustomerMessage) -> AgentResponse: """处理客户消息并生成回复""" + self._activity_log( + "agent_inbound", + acc_id=message.acc_id, + customer_id=message.from_id, + msg=message.msg, + msg_type=message.msg_type, + ) metrics_emit("inbound_msg", customer_id=message.from_id, acc_id=message.acc_id) # 获取或创建对话状态 state = self._get_conversation_state(message.from_id) @@ -1609,38 +1665,63 @@ class CustomerServiceAgent: if self._in_cooldown(state, message.msg): elapsed = int((datetime.now() - state.last_reply_at).total_seconds()) print(f"[Agent] 冷却期静默(距上次回复 {elapsed}s):{message.msg!r}") + self._activity_log("agent_cooldown_silent", customer_id=message.from_id, elapsed_s=elapsed) return AgentResponse(reply="", should_reply=False, need_transfer=False) # 前置风控:客户文本一旦命中政治/敏感询问,直接拒绝,避免“发图我看看”类答非所问 try: from utils.content_filter import should_block_customer - if should_block_customer(message.msg) or self._is_political_inquiry(message.msg): + map_hit = self._is_map_inquiry(message.msg) + political_hit = self._is_political_inquiry(message.msg) + if should_block_customer(message.msg) or political_hit or map_hit: # 命中敏感询问时清空待报价队列,避免旧图残留污染后续会话 state.pending_image_urls.clear() state.pending_requirements.clear() self._sync_pending_quote_state(message.from_id, state) + reject_text = "地图这类不做哈,这边不接地图相关需求。" + if political_hit and not map_hit: + reject_text = "这类不做哈,政治相关图片和人物都不接。" reply = await self._rewrite_reply_with_ai( message=message, state=state, - reply="这类不做哈,政治相关图片和人物都不接。", + reply=reject_text, scene="risk_reject", ) state.last_reply_at = datetime.now() print(f"{self.C_REPLY}[REPLY->CUSTOMER]{self.C_RESET} {reply}") + self._activity_log( + "agent_risk_reject", + customer_id=message.from_id, + map_hit=map_hit, + political_hit=political_hit, + reply=reply, + ) return AgentResponse(reply=reply, should_reply=True, need_transfer=False) except Exception: - if self._is_political_inquiry(message.msg): + map_hit = self._is_map_inquiry(message.msg) + political_hit = self._is_political_inquiry(message.msg) + if political_hit or map_hit: state.pending_image_urls.clear() state.pending_requirements.clear() self._sync_pending_quote_state(message.from_id, state) + reject_text = "地图这类不做哈,这边不接地图相关需求。" + if political_hit and not map_hit: + reject_text = "这类不做哈,政治相关图片和人物都不接。" reply = await self._rewrite_reply_with_ai( message=message, state=state, - reply="这类不做哈,政治相关图片和人物都不接。", + reply=reject_text, scene="risk_reject", ) state.last_reply_at = datetime.now() print(f"{self.C_REPLY}[REPLY->CUSTOMER]{self.C_RESET} {reply}") + self._activity_log( + "agent_risk_reject", + customer_id=message.from_id, + map_hit=map_hit, + political_hit=political_hit, + reply=reply, + ) return AgentResponse(reply=reply, should_reply=True, need_transfer=False) # 检测售前/售后 @@ -1932,9 +2013,10 @@ class CustomerServiceAgent: "天安门", "政治人物", "政治事件", "领导人", "党政", "习近平", "毛泽东", "邓小平", "江泽民", "胡锦涛", "特朗普", "拜登", "普京", "泽连斯基", + "地图", "地形图", "行政区划图", "卫星地图", ] target_agent = self.agent_after_sale if state.stage == "售后" else self.agent - risk_hit = any(k in msg_lower for k in risk_kw) or self._is_political_inquiry(message.msg) + risk_hit = any(k in msg_lower for k in risk_kw) or self._is_political_inquiry(message.msg) or self._is_map_inquiry(message.msg) if risk_hit: target_agent = self.agent_risk elif any(k in message.msg for k in order_markers): @@ -2011,6 +2093,7 @@ class CustomerServiceAgent: except Exception as e: err_str = str(e) print(f"[Agent] AI 调用失败: {e},使用兜底回复") + self._activity_log("agent_ai_error", customer_id=message.from_id, acc_id=message.acc_id, error=err_str) metrics_emit("ai_call_failed", customer_id=message.from_id, acc_id=message.acc_id) if "AccountOverdueError" in err_str or "overdue" in err_str.lower(): asyncio.create_task(_notify_wechat_overdue()) @@ -2115,6 +2198,14 @@ class CustomerServiceAgent: print(f"{self.C_REPLY}[REPLY->CUSTOMER]{self.C_RESET} {reply_text}") else: print(f"{self.C_MUTED}[REPLY->CUSTOMER]{self.C_RESET} <静默/不发送>") + self._activity_log( + "agent_outbound_decision", + customer_id=message.from_id, + should_reply=should_reply, + need_transfer=need_transfer, + reply=reply_text or "", + transfer_msg=transfer_msg, + ) return AgentResponse(reply=reply_text or "", should_reply=should_reply, need_transfer=need_transfer, transfer_msg=transfer_msg) @@ -3179,6 +3270,11 @@ class CustomerServiceAgent: prompt += f"\n客户说:{customer_text}\n" image_url = self._extract_image_url(customer_text) price_keywords = ["多少钱", "多少", "价格", "几块", "怎么收费", "报个价"] + size_keywords = [ + "尺寸", "比例", "宽", "高", "米", "厘米", "mm", "cm", + "横版", "竖版", "2米", "3米", "改成", "做成", + ] + has_size_change = any(kw in customer_text.lower() for kw in [k.lower() for k in size_keywords]) # gemini_api 店铺:不触发找图流程,按 API 客服回复 if shop_type == "gemini_api": @@ -3190,7 +3286,17 @@ class CustomerServiceAgent: if last_url: prompt += "\n客户在询问价格:若客户已确认发完,则给总报价;若还在发图,先引导发完后统一报价。" else: - prompt += "\n客户在询问价格但未发图,回复「发图来我看看」。" + prompt += "\n客户在询问价格但未发图:先简短承接(如“在看呢/收到”),不要机械连发;再自然引导对方发图。" + if has_size_change: + prompt += ( + "\n⚠️ 尺寸改动场景:优先判断图片主体是否会被拉伸变形," + "不是只看整张图宽高比。若会变形,要先提示“需要补图/扩边”,再给报价。" + ) + elif has_size_change: + prompt += ( + "\n客户在改尺寸/改比例:先按主体比例判断是否会变形," + "不是只看整图比例。若目标尺寸会拉伸主体,先明确说明要补图(如上下补图/扩边)再报价。" + ) elif any(kw in customer_text for kw in progress_keywords): # 客户问进度/催单,必须先核查付款状态 if order_unpaid: diff --git a/core/websocket_client.py b/core/websocket_client.py index 5ca9094..4595dcd 100755 --- a/core/websocket_client.py +++ b/core/websocket_client.py @@ -130,7 +130,20 @@ class QingjianAPIClient: if workflow: workflow.register_send_callback(self._workflow_send) workflow.register_agent_notify_callback(self._workflow_agent_notify) - + + def _activity_log(self, event: str, **kwargs): + """统一活动日志,便于按 event 检索完整链路。""" + safe = {} + for k, v in kwargs.items(): + if isinstance(v, str): + safe[k] = v[:200] + else: + safe[k] = v + try: + logger.info(f"[ACTIVITY] event={event} data={json.dumps(safe, ensure_ascii=False)}") + except Exception: + logger.info(f"[ACTIVITY] event={event} data={safe}") + async def connect(self): """连接WebSocket服务器""" @@ -417,12 +430,19 @@ class QingjianAPIClient: async def _debounce_agent_reply(self, data: dict): """ 消息防抖:同一客户在 _DEBOUNCE_SECONDS 内的连续消息合并后再处理。 - 订单通知、图片URL、付款相关消息不走防抖,立即处理。 + 订单通知、付款相关消息不走防抖,立即处理。 """ msg_body = data.get('msg', '') # 以下情况跳过防抖,立即处理(后台执行,不阻塞接收循环) immediate_keywords = ["买家已付款", "已付款", "[系统订单信息]"] - if any(kw in msg_body for kw in immediate_keywords) or self._msg_has_image_url(msg_body): + if any(kw in msg_body for kw in immediate_keywords): + self._activity_log( + "debounce_bypass_immediate", + acc_id=data.get("acc_id", ""), + customer_id=data.get("from_id", ""), + reason="payment_or_order", + msg=msg_body, + ) self._fire_and_forget(self._agent_reply_serialized(data)) return @@ -432,6 +452,12 @@ class QingjianAPIClient: if key not in self._pending_msgs: self._pending_msgs[key] = [] self._pending_msgs[key].append(msg_body) + self._activity_log( + "debounce_enqueue", + key=key, + queue_size=len(self._pending_msgs[key]), + msg=msg_body, + ) # 取消上一个等待任务(如果有) old_task = self._debounce_tasks.get(key) @@ -451,6 +477,12 @@ class QingjianAPIClient: else: merged_msg = "、".join(m for m in msgs if m.strip()) print(f"[{self.get_time()}] 防抖合并 {len(msgs)} 条消息: {merged_msg[:60]}") + self._activity_log( + "debounce_flush", + key=capture_key, + merged_count=len(msgs), + merged_msg=merged_msg, + ) merged_data = dict(capture_data) merged_data['msg'] = merged_msg await self._agent_reply_serialized(merged_data) @@ -513,7 +545,11 @@ class QingjianAPIClient: if intent == "打招呼": low, high = 1.0, min(3.0, base) elif intent in ("询价", "砍价"): - low, high = 2.0, min(5.0, base) + # 询价先略等一会,给客户补发图片/需求的窗口,减少机械两连回 + low, high = 4.0, min(7.0, max(base, 7.0)) + elif intent == "image": + # 文本里直接贴图链接:短等合并上下文,避免和上一条询价并发 + low, high = 2.2, 4.2 elif intent in ("修改", "批量"): low, high = max(3.0, base * 0.65), min(18.0, base + 2.0) elif intent == "转接": @@ -825,6 +861,12 @@ class QingjianAPIClient: return logger.info("Agent 正在处理消息...") + self._activity_log( + "agent_process_start", + acc_id=data.get("acc_id", ""), + customer_id=data.get("from_id", ""), + msg=msg_text, + ) # 调用 Agent response = await self.agent.process_message(customer_msg) @@ -832,6 +874,12 @@ class QingjianAPIClient: # 检查是否需要转接人工 if response.need_transfer: logger.info("Agent 决定转接人工") + self._activity_log( + "agent_transfer", + acc_id=data.get("acc_id", ""), + customer_id=data.get("from_id", ""), + transfer_msg=response.transfer_msg, + ) await self.transfer_to_human(data, response.transfer_msg) # 推送到企微:客户消息+转接回复成对 try: @@ -881,6 +929,12 @@ class QingjianAPIClient: # 模拟真人打字延迟,避免瞬间回复太机械 await asyncio.sleep(0.8) logger.info(f"Agent 回复: {response.reply}") + self._activity_log( + "agent_reply", + acc_id=data.get("acc_id", ""), + customer_id=data.get("from_id", ""), + reply=response.reply, + ) await self.send_reply(data, response.reply) # 推送到企微:客户消息+AI回复成对 try: @@ -897,9 +951,20 @@ class QingjianAPIClient: pass elif not response.need_transfer: logger.info("Agent 决定不回复此消息") - + self._activity_log( + "agent_no_reply", + acc_id=data.get("acc_id", ""), + customer_id=data.get("from_id", ""), + ) + except Exception as e: logger.error(f"Agent 处理失败: {e}") + self._activity_log( + "agent_process_error", + acc_id=data.get("acc_id", ""), + customer_id=data.get("from_id", ""), + error=str(e), + ) async def _analyze_multi_and_reply(self, data: dict, urls: list): try: @@ -1513,6 +1578,12 @@ class QingjianAPIClient: """ if not self.websocket: print(f"[{self.get_time()}] 错误: 未连接到服务器") + self._activity_log( + "send_reply_skipped", + reason="websocket_not_connected", + acc_id=original_msg.get("acc_id", ""), + customer_id=original_msg.get("from_id", ""), + ) return reply_content = self._colloquialize_outbound_reply(reply_content) @@ -1531,6 +1602,12 @@ class QingjianAPIClient: logger.info( f"外发限流命中,跳过发送 | 客户:{ckey} | cooldown:{cooldown}s | msg:{str(reply_content)[:40]}" ) + self._activity_log( + "send_reply_throttled", + key=ckey, + cooldown_s=cooldown, + msg=str(reply_content), + ) return self._last_reply_sent_at[ckey] = now_mono @@ -1554,6 +1631,12 @@ class QingjianAPIClient: "cy_name": customer_name } self._log_outbound_once(original_msg, str(reply_content)) + self._activity_log( + "send_reply_attempt", + acc_id=shop_id, + customer_id=customer_id, + msg=str(reply_content), + ) await self.send_message(reply) def _colloquialize_outbound_reply(self, text: Any) -> Any: @@ -1652,10 +1735,29 @@ class QingjianAPIClient: await self.websocket.send(msg_json) pretty = json.dumps(message, ensure_ascii=False, indent=2) print(f"[{self.get_time()}] 发送成功:\n{pretty}") + self._activity_log( + "send_message_success", + acc_id=message.get("acc_id", ""), + customer_id=message.get("from_id", ""), + msg_type=message.get("msg_type", 0), + msg=message.get("msg", ""), + ) except Exception as e: print(f"[{self.get_time()}] 发送失败: {e}") + self._activity_log( + "send_message_error", + acc_id=message.get("acc_id", ""), + customer_id=message.get("from_id", ""), + error=str(e), + ) else: print(f"[{self.get_time()}] 错误: 连接未打开") + self._activity_log( + "send_message_skipped", + reason="socket_not_open", + acc_id=message.get("acc_id", ""), + customer_id=message.get("from_id", ""), + ) async def auto_reply(self, data): """自动回复示例(已弃用,使用 agent_reply 替代)""" diff --git a/image/image_analyzer.py b/image/image_analyzer.py index 155d8b3..f9076b2 100755 --- a/image/image_analyzer.py +++ b/image/image_analyzer.py @@ -86,7 +86,7 @@ ANALYSIS_PROMPT = """你是一个电商图片处理评估专家,同时也是 G high情况下,可做改为partial,备注写明风险话术,谨慎接单 【敏感内容检测 - 必须严格判断!】 -- yes:含以下任一内容 → 色情/黄色/擦边/裸露/性暗示/大尺度/涉政/暴力/血腥/违禁品 +- yes:含以下任一内容 → 色情/黄色/擦边/裸露/性暗示/大尺度/涉政/暴力/血腥/违禁品/地图类 敏感内容=yes 时,可做必须填 no,直接拒绝不接单 - no:无上述敏感内容,可以正常接单处理 @@ -257,6 +257,9 @@ class ImageAnalyzer: "习近平", "毛泽东", "邓小平", "江泽民", "胡锦涛", "李克强", "周恩来", "中国共产党", "共产党", "中共", "党代会", "两会", "人大", "政协", "trump", "donald trump", "biden", "putin", "zelensky", "xi jinping", + # 地图类(业务规则:地图一律不接) + "地图", "地形图", "行政区划图", "世界地图", "中国地图", "卫星地图", "导航图", "航海图", + "map", "topographic map", "satellite map", "navigation map", # 黄暴血腥 "黄色", "擦边", "裸露", "色情", "性暗示", "暴力", "凶杀", "打斗", "枪击", "血腥", "尸体", "虐待", # 英文兜底 @@ -667,14 +670,14 @@ class ImageAnalyzer: reason = (reason or "多人脸") + " | 多人脸场景不接单" price_suggest = 0 - # 硬规则3:党政/涉黄/暴力/血腥内容不接单 + # 硬规则3:党政/涉黄/暴力/血腥/地图内容不接单 forbidden_scene = any(k in scene_text for k in self.FORBIDDEN_CONTENT_KEYWORDS) sensitive_hit = str(sensitive or "").strip().lower() in ("yes", "true", "1", "是") if forbidden_scene or sensitive_hit: feasibility = "no" risk = "high" - note = "含政治/党政/涉黄/暴力/血腥等敏感内容,不接单" - reason = (reason or "敏感内容") + " | 敏感内容不接单(政治类一律拒单)" + note = "含政治/党政/涉黄/暴力/血腥/地图等敏感内容,不接单" + reason = (reason or "敏感内容") + " | 敏感内容不接单(政治/地图类一律拒单)" price_suggest = 0 # 确保是 5 的倍数 diff --git a/skills/customer-service/SKILL.md b/skills/customer-service/SKILL.md index 8f69340..5b385ea 100755 --- a/skills/customer-service/SKILL.md +++ b/skills/customer-service/SKILL.md @@ -116,6 +116,8 @@ description: 找原图店客服 - 售前咨询、报价成交、售后处理 - 普通服务只发 **jpg** - 要分层(PSD)单独 35 元 - 改尺寸:调用 resize_image,常用 1920x1080 / 1080x1920 / 2000x2000 +- 客户改尺寸时,先判断**图片主体比例**会不会被拉伸变形(不是只看整图宽高比)。 +- 若主体会变形,先明确说明需要补图/扩边(如上下补图),再报价。 自然回复,不要固定句式。 diff --git a/skills/customer-service/customer-skill.md b/skills/customer-service/customer-skill.md index 48dc4f8..fc07ed9 100644 --- a/skills/customer-service/customer-skill.md +++ b/skills/customer-service/customer-skill.md @@ -88,6 +88,8 @@ description: 找原图店客服 - 售前咨询、报价成交、售后处理 - 普通服务只发 **jpg** - 要分层(PSD)单独 35 元 - 改尺寸:调用 resize_image,常用 1920x1080 / 1080x1920 / 2000x2000 +- 客户改尺寸时,先判断**图片主体比例**会不会被拉伸变形(不是只看整图宽高比)。 +- 若主体会变形,先明确说明需要补图/扩边(如上下补图),再报价。 自然回复,不要固定句式。 diff --git a/skills/pre-sales-skill/SKILL.md b/skills/pre-sales-skill/SKILL.md index f374e08..16b3894 100644 --- a/skills/pre-sales-skill/SKILL.md +++ b/skills/pre-sales-skill/SKILL.md @@ -18,5 +18,6 @@ description: 售前接待与收图阶段技能,强调上下文承接、短句 - 「就这一个/没有了/先这些/就这些」=> 视为发图完成,进入报价。 - 「高清/重新发/发我/?」=> 按跟进催办承接。 - 若客户说「上一张/截图/局部/细节/补图」,按同一需求补充处理,不当新单。 +- 客户提尺寸/比例需求时,优先看“主体是否会变形”,不是只看整图宽高比。 +- 若目标尺寸会拉伸主体,先说明需要补图/扩边,再继续报价推进。 - 输出 1-2 句,口语化,不官腔。 - diff --git a/skills/risk-skill/SKILL.md b/skills/risk-skill/SKILL.md index 0eade2a..198d56c 100644 --- a/skills/risk-skill/SKILL.md +++ b/skills/risk-skill/SKILL.md @@ -12,7 +12,7 @@ description: 风控拒绝技能,覆盖敏感内容拦截、拒绝边界和安 ## 执行规则 - 政治/色情/暴力/明显违规内容:直接拒绝,不报价。 +- 地图类需求(地图/地形图/行政区划图/卫星地图等):直接拒绝,不报价。 - 拒绝后若客户追问「能做吗/有吗」,保持一致,不反复改口。 - 不输出技术解释,不展开争论。 - 句子短、边界清晰、语气克制。 - diff --git a/tests/test_regression_pipeline.py b/tests/test_regression_pipeline.py index 4076ffa..19e73f5 100644 --- a/tests/test_regression_pipeline.py +++ b/tests/test_regression_pipeline.py @@ -364,6 +364,27 @@ class RegressionPipelineTest(unittest.IsolatedAsyncioTestCase): ) self.assertEqual(reply, "图片收到了,你继续发就行。") + async def test_map_inquiry_is_rejected(self): + agent = CustomerServiceAgent() + msg = CustomerMessage( + msg_id="m-map-reject", + 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("地图", resp.reply) + self.assertIn("不做", resp.reply) + self.assertTrue(resp.reply.endswith(("嗯", "哦", "好的", "嗯咯", "嗯啦"))) + def tearDown(self): db.clear_pending_quote_state(self.customer_id) diff --git a/utils/content_filter.py b/utils/content_filter.py index d39f79e..8fc055d 100755 --- a/utils/content_filter.py +++ b/utils/content_filter.py @@ -34,6 +34,10 @@ _SENSITIVE_PATTERNS = { "擦边": [ r"擦边", r"大尺度", r"性感图", r"露点", r"半裸", ], + "地图": [ + r"地图", r"地形图", r"行政区划图", r"世界地图", r"中国地图", + r"卫星地图", r"导航图", r"航海图", r"作战地图", r"军事地图", + ], } _COMPILED: dict = {}