Files

155 lines
5.7 KiB
Python
Raw Permalink Normal View History

"""Anonymous customer-service orchestration without private customer access."""
from __future__ import annotations
import json
import re
from inspect import isawaitable
from rag.intent import Intent
class QueryTooLongError(ValueError):
pass
async def _config(config_getter, key: str, default: str):
value = config_getter(key, default)
if isawaitable(value):
value = await value
return value or default
async def _maybe_await(value):
return await value if isawaitable(value) else value
class AnonymousCustomerAgent:
def __init__(
self,
*,
context,
rag_retrieve,
intent_recognize,
generate_answer,
audit_writer,
config_getter,
):
self.context = context
self.rag_retrieve = rag_retrieve
self.intent_recognize = intent_recognize
self.generate_answer = generate_answer
self.audit_writer = audit_writer
self.config_getter = config_getter
async def handle(self, session_id: str, query: str, *, trace_id: str) -> dict:
if len(query) > 2000:
raise QueryTooLongError("query长度不能超过2000字符")
await self.context.append(session_id, "user", query)
if self._contains_sensitive_input(query):
await _maybe_await(self.audit_writer(
action="anon_sensitive_input",
trace_id=trace_id,
session_id=session_id,
))
intent = await _maybe_await(self.intent_recognize(query))
sources = []
2026-09-12 15:06:34 +08:00
if intent == Intent.GUIDE_PURCHASE:
answer = await _config(
self.config_getter,
"agent.customer.template.guide_purchase",
"请前往开户页面办理。",
)
2026-09-12 15:06:34 +08:00
elif intent == Intent.WANT_ADVISOR:
answer = await _config(
self.config_getter,
"agent.customer.template.guide_advisor",
"如需基金推荐,请联系投资顾问。",
)
2026-09-12 15:06:34 +08:00
elif intent == Intent.OFF_TOPIC:
answer = await _config(
self.config_getter,
"agent.customer.template.off_topic",
"我是华夏科技的智能客服,只能解答基金与公司业务相关的问题,"
"您可以问我基金知识、开户流程或公司信息~",
)
elif intent == Intent.CHITCHAT:
answer = await self._chitchat(session_id)
elif intent in (Intent.KNOWLEDGE_QA, Intent.COMPANY_INFO):
try:
sources = await _maybe_await(self.rag_retrieve(query, None))
except Exception:
sources = []
if not sources:
answer = await _config(
self.config_getter,
"agent.customer.template.fallback_human",
"当前未找到匹配信息,请转人工客服。",
)
else:
messages = await self.context.get(session_id)
prompt = messages + [
{
"role": "system",
"content": "仅根据提供的知识来源回答,不得编造基金推荐。",
},
{
"role": "system",
"content": f"知识来源:{json.dumps(sources, ensure_ascii=False)}",
},
]
try:
answer = await _maybe_await(self.generate_answer(prompt))
except Exception:
answer = await _config(
self.config_getter,
"agent.customer.template.fallback_human",
"当前服务繁忙,请转人工客服。",
)
else:
answer = await _config(
self.config_getter,
"agent.customer.template.fallback_human",
"当前未找到匹配信息,请转人工客服。",
)
await self.context.append(session_id, "assistant", answer)
return {
"answer": answer,
"sources": sources,
"intent": intent.value,
"trace_id": trace_id,
}
2026-09-12 15:06:34 +08:00
async def _chitchat(self, session_id: str) -> str:
"""带对话历史调用 LLM 做受限闲聊,失败时退回固定话术。"""
messages = await self.context.get(session_id)
prompt = [
{
"role": "system",
"content": (
"你是华夏科技(基金代销金融机构)的智能客服助手。"
"用户正在与你寒暄,请用一两句话简短、友好地回应,"
"并自然地引导用户咨询基金知识、开户流程或公司信息。"
"不得推荐任何基金产品,不得谈论具体收益,不得回答金融之外的实质性问题。"
),
},
*messages,
]
try:
return await _maybe_await(self.generate_answer(prompt))
except Exception:
return await _config(
self.config_getter,
"agent.customer.template.chitchat_fallback",
"您好,我是华夏科技的智能客服,很高兴为您服务!"
"您可以问我基金知识、开户流程或公司信息~",
)
@staticmethod
def _contains_sensitive_input(query: str) -> bool:
return bool(
re.search(r"(?<!\d)1[3-9]\d{9}(?!\d)", query)
or re.search(r"(?:客户|customer)[_ -]?(?:id|号)?\s*[::]?\s*[A-Za-z0-9_-]{4,}", query, re.I)
)