145 lines
6.5 KiB
Python
145 lines
6.5 KiB
Python
"""奶龙基金智能助手:只处理一期公开客服范围。"""
|
||
|
||
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)
|