"""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 = [] if intent == Intent.GUIDE_PURCHASE: answer = await _config( self.config_getter, "agent.customer.template.guide_purchase", "请前往开户页面办理。", ) elif intent == Intent.WANT_ADVISOR: answer = await _config( self.config_getter, "agent.customer.template.guide_advisor", "如需基金推荐,请联系投资顾问。", ) 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, } 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"(?