Files
group_fqcd_jr/app/service/agent/customer_service_agent.py
T

145 lines
6.5 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 app.core.config import get_settings
from app.core.contracts import AgentDefinition, AgentRequest, CoreResult, RequestContext
from app.core.errors import RecoverableAgentError
from app.core.knowledge_contracts import KnowledgeSearchResult
from app.service.agent.base import BaseAgent
from app.service.agent.customer_service_routing import CustomerServiceIntentRouter
def _human_service_phone() -> str:
return get_settings().customer_service_phone
class CustomerServiceAgent(BaseAgent):
# 客服澄清严格限制两轮;第三次未确认即走人工,避免用户陷入循环追问。
MAX_CLARIFICATION_ROUNDS = 2
definition = AgentDefinition(
agent_type="customer_service",
version="1.0.0",
allowed_tools=("query_knowledge",),
allowed_roles=("visitor", "customer"),
allowed_portals=("api",),
requires_model_intent_classification=False,
# 客服只处理公开知识;会话短期记忆将在独立的受控 Redis 链路中实现。
recalls_customer_memory=False,
supported_intents=(
"security_notice",
"account_entry",
"human_transfer",
"compliance_refusal",
"chitchat",
"public_knowledge",
"faq",
"product_inquiry",
"policy_explain",
),
)
def __init__(self) -> None:
super().__init__(self.definition)
async def handle(self, request: AgentRequest, context: RequestContext) -> CoreResult:
message = request.message.strip()
route = CustomerServiceIntentRouter.classify(message)
if route.intent == "security_notice":
return CoreResult(
text=(
"请立即停止操作,不要继续提供验证码、密码或身份证件信息,并尽快联系"
f"人工客服 {_human_service_phone()}。"
),
transfer_required=True,
transfer_reason="security_notice",
)
if route.intent == "compliance_refusal":
return CoreResult(
text=(
"我不能推荐具体产品、承诺收益或代您交易;如需进一步了解,请联系"
f"人工客服 {_human_service_phone()}。"
),
transfer_required=True,
transfer_reason="compliance_refusal",
)
if route.intent == "account_entry":
if "visitor" in context.roles:
return CoreResult(text="我无法查询账户数据,请先登录后前往“我的账户”查看相关状态。")
return CoreResult(text="我无法查询账户数据,请前往“我的账户”查看相关状态。")
if route.intent == "human_transfer":
return CoreResult(
text=(
f"您可以联系人工客服 {_human_service_phone()}(工作日 09:00-18:00)"
"进一步核实。"
),
transfer_required=True,
transfer_reason="human_transfer",
)
if route.is_chitchat:
if request.metadata.chitchat_streak == 4:
return CoreResult(text="和您聊天很开心呀。您是想了解相关的基金业务或公开信息吗?")
return CoreResult(text="您好呀,我是奶龙基金智能助手,很高兴和您聊天。")
if route.requires_context and not request.metadata.session_context:
return self._clarify_or_transfer(
request.metadata.clarification_round,
"我还不确定您说的是哪只基金或产品。请提供产品名称或代码,我再为您查询公开资料。",
)
# `KnowledgeQuery.query` 的契约上限是 2000 字符;上下文只是一种补充,不能挤掉
# 当前问题,也不能因异常超长输入让客服运行失败。
knowledge_query = message[:2000]
if route.requires_context:
# 短期上下文仅用于补全“它的费率”等指代;不传给模型生成,也不参与账户查询。
knowledge_query = "\n".join((
f"当前问题:{message}",
"当前会话已脱敏上下文(仅用于指代消解):",
*request.metadata.session_context,
))[:2000]
try:
output = await self.call_tool(
"query_knowledge",
{"query": knowledge_query, "intents": route.knowledge_intents, "top_k": 5},
intent="public_knowledge",
context=context,
)
result = (
output
if isinstance(output, KnowledgeSearchResult)
else KnowledgeSearchResult.model_validate(output)
)
except RecoverableAgentError:
return self._knowledge_transfer("knowledge_unavailable")
if result.degraded:
return self._knowledge_transfer("knowledge_unavailable")
answers = [
hit.answer.strip()
for hit in result.hits
if isinstance(hit.answer, str) and hit.answer.strip()
]
if not answers:
return self._clarify_or_transfer(
request.metadata.clarification_round,
"为了准确查询公开资料,您想了解哪一类信息?例如产品信息、交易规则或政策说明。",
)
return CoreResult(text=answers[0])
@classmethod
def _clarify_or_transfer(cls, clarification_round: int, clarification_text: str) -> CoreResult:
"""每次只补充一个关键信息;超过两轮仍无法确认时再转人工。"""
if clarification_round >= cls.MAX_CLARIFICATION_ROUNDS:
return cls._knowledge_transfer("knowledge_clarification_exhausted")
return CoreResult(text=clarification_text, clarification_required=True)
@staticmethod
def _knowledge_transfer(reason: str) -> CoreResult:
phone = _human_service_phone()
if reason == "knowledge_unavailable":
text = (
"您好呀,我暂时无法从公开资料中确认这个问题,请联系"
f"人工客服 {phone} 进一步核实。"
)
else:
text = (
"您好呀,我暂时没有查到匹配的公开资料,请联系"
f"人工客服 {phone} 进一步核实。"
)
return CoreResult(text=text, transfer_required=True, transfer_reason=reason)