201 lines
7.2 KiB
Python
201 lines
7.2 KiB
Python
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)},
|
||
)
|