from __future__ import annotations import random from datetime import datetime from typing import TYPE_CHECKING, Optional from core.rules import Rule, RuleContext, RuleEngine, RuleResult from services.risk_service import RiskService if TYPE_CHECKING: from core.pydantic_ai_agent import ( AgentResponse, ConversationState, CustomerMessage, CustomerServiceAgent, ) class AgentPreRuleService: """Pre-processing rule chain for short replies, cooldown, and text risk.""" def __init__(self, agent: "CustomerServiceAgent", risk_service: RiskService): self.agent = agent self.risk_service = risk_service self.engine = self._build_engine() async def run( self, *, message: "CustomerMessage", state: "ConversationState", trace_id: str, ) -> Optional["AgentResponse"]: ctx = RuleContext(data={"message": message, "state": state, "trace_id": trace_id}) result = await self.engine.run(ctx) if not result.stop: return None response = result.payload.get("response") return response def _build_engine(self) -> RuleEngine: return RuleEngine( rules=[ Rule( name="meaningless_short_text", priority=10, predicate=self._rule_pred_meaningless_short_text, action=self._rule_act_meaningless_short_text, ), Rule( name="cooldown_silent", priority=20, predicate=self._rule_pred_cooldown_silent, action=self._rule_act_cooldown_silent, ), Rule( name="manual_risk_block", priority=30, predicate=self._rule_pred_manual_risk_block, action=self._rule_act_manual_risk_block, ), Rule( name="text_risk_block", priority=40, predicate=self._rule_pred_text_risk_block, action=self._rule_act_text_risk_block, ), ] ) async def _rule_pred_meaningless_short_text(self, ctx: RuleContext) -> bool: from core.pydantic_ai_agent import _is_meaningless_short_text message = ctx.get("message") return _is_meaningless_short_text(message.msg) async def _rule_act_meaningless_short_text(self, ctx: RuleContext) -> RuleResult: from core.pydantic_ai_agent import AgentResponse message = ctx.get("message") state = ctx.get("state") trace_id = ctx.get("trace_id", "") ping = random.choice(("嗯咯", "嗯啦", "嗯", "哦")) state.last_reply_at = datetime.now() self.agent._activity_log( "agent_ping_reply", trace_id=trace_id, customer_id=message.from_id, msg=message.msg, reply=ping, ) return RuleResult( matched=True, stop=True, action="agent_ping_reply", payload={"response": AgentResponse(reply=ping, should_reply=True, need_transfer=False)}, ) async def _rule_pred_cooldown_silent(self, ctx: RuleContext) -> bool: message = ctx.get("message") state = ctx.get("state") return self.agent._in_cooldown(state, message.msg) async def _rule_act_cooldown_silent(self, ctx: RuleContext) -> RuleResult: from core.pydantic_ai_agent import AgentResponse message = ctx.get("message") state = ctx.get("state") trace_id = ctx.get("trace_id", "") elapsed = int((datetime.now() - state.last_reply_at).total_seconds()) if state.last_reply_at else 0 print(f"[Agent] 冷却期静默(距上次回复 {elapsed}s):{message.msg!r}") self.agent._activity_log( "agent_cooldown_silent", trace_id=trace_id, customer_id=message.from_id, elapsed_s=elapsed, ) return RuleResult( matched=True, stop=True, action="agent_cooldown_silent", payload={"response": AgentResponse(reply="", should_reply=False, need_transfer=False)}, ) async def _rule_pred_manual_risk_block(self, ctx: RuleContext) -> bool: message = ctx.get("message") decision = self.risk_service.check_manual_block(message.from_id) ctx.set("manual_risk_decision", decision) return decision.blocked async def _rule_act_manual_risk_block(self, ctx: RuleContext) -> RuleResult: from core.pydantic_ai_agent import AgentResponse, TRANSFER_MESSAGE message = ctx.get("message") trace_id = ctx.get("trace_id", "") decision = ctx.get("manual_risk_decision") self.agent._activity_log( "agent_manual_risk_reject", trace_id=trace_id, customer_id=message.from_id, risk=(decision.profile if decision else {}), ) return RuleResult( matched=True, stop=True, action="agent_manual_risk_reject", payload={ "response": AgentResponse( reply="这边无法继续为你处理该类需求,给你转人工专员对接。", should_reply=True, need_transfer=True, transfer_msg=TRANSFER_MESSAGE, ) }, ) async def _rule_pred_text_risk_block(self, ctx: RuleContext) -> bool: message = ctx.get("message") decision = await self.risk_service.check_text_block( message.msg, political_detector=self.agent._is_political_inquiry, map_detector=self.agent._is_map_inquiry, ) ctx.set("text_risk_decision", decision) return decision.blocked async def _rule_act_text_risk_block(self, ctx: RuleContext) -> RuleResult: from core.pydantic_ai_agent import AgentResponse message = ctx.get("message") state = ctx.get("state") trace_id = ctx.get("trace_id", "") decision = ctx.get("text_risk_decision") state.pending_image_urls.clear() state.pending_requirements.clear() self.agent._sync_pending_quote_state(message.from_id, state) reject_text = self.risk_service.build_reject_text(decision.category if decision else "other") reply = await self.agent._rewrite_reply_with_ai( message=message, state=state, reply=reject_text, scene="risk_reject", ) state.last_reply_at = datetime.now() print(f"{self.agent.C_REPLY}[REPLY->CUSTOMER]{self.agent.C_RESET} {reply}") self.agent._activity_log( "agent_risk_reject", trace_id=trace_id, customer_id=message.from_id, risk_category=(decision.category if decision else "other"), risk_source=(decision.source if decision else "unknown"), reply=reply, ) return RuleResult( matched=True, stop=True, action="agent_risk_reject", payload={"response": AgentResponse(reply=reply, should_reply=True, need_transfer=False)}, )