Files
tw/core/agent_pre_rules.py

202 lines
7.3 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.
from __future__ import annotations
import logging
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:
message = ctx.get("message")
state = ctx.get("state")
return self.agent._should_handle_as_meaningless_short_text(state, 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
logger.info("[Agent] 冷却期静默(距上次回复 %ss%r", elapsed, message.msg)
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()
logger.info("[REPLY->CUSTOMER] %s", 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)},
)
logger = logging.getLogger("cs_agent")