# ============================================================================= # 企微IT智能服务台 — AI Wingman 服务(坐席智能副驾驶) # ============================================================================= # 说明:复用 Dify 基础设施,使用独立的 Wingman Agent(Agent 2), # 与员工端 AI(Agent 1)共用知识库但 system prompt 不同。 # # 核心能力: # 1. 生成 AI 草稿回复 — 基于对话上下文为坐席生成专业回复 # 2. 生成会话自动摘要 — 结单时自动提取问题/原因/解决方案 # 3. 生成自动标签建议 — 基于对话内容建议标签分类 # # 降级策略:Wingman Agent 不可用时返回友好错误信息,不抛异常 # ============================================================================= import json import logging from typing import Any, Dict, List, Optional import httpx from app.config import settings logger = logging.getLogger(__name__) class WingmanService: """AI Wingman 服务 — 坐席智能副驾驶。 复用 Dify 基础设施,使用独立的 Wingman Agent(Agent 2), 与员工端 AI(Agent 1)共用知识库但 system prompt 不同。 三个核心方法使用不同的 system prompt: - 草稿生成:生成坐席可采纳的专业回复 - 摘要生成:提取结构化的会话摘要 - 标签建议:建议标签分类和优先级 """ # -------------------------------------------------------------------------- # System Prompt 定义 # -------------------------------------------------------------------------- _DRAFT_SYSTEM_PROMPT: str = ( "你是一个IT服务坐席助手,基于对话上下文为坐席生成专业、准确的回复草稿。" "直接输出回复内容,不要解释。" ) _SUMMARY_SYSTEM_PROMPT: str = ( "你是一个IT服务分析助手,基于完整对话生成结构化摘要," "包含:问题、原因、解决方案。以JSON格式输出。" "输出格式:{\"problem\": \"问题描述\", \"cause\": \"原因\", \"solution\": \"解决方案\"}" ) _TAGS_SYSTEM_PROMPT: str = ( "你是一个IT服务分类助手,基于对话内容建议标签分类。以JSON格式输出。" "输出格式:{\"suggested_tags\": [\"标签1\", \"标签2\"], \"category\": \"分类\", \"priority\": \"low/medium/high\"}" ) # -------------------------------------------------------------------------- # AI 辅助消息框 Prompt(4 个功能,与 PRD v1.0 对齐) # -------------------------------------------------------------------------- _AUTOCOMPLETE_SYSTEM_PROMPT: str = ( "你是一个IT服务坐席输入助手。根据坐席当前正在输入的内容和对话上下文,补齐下一句话。\n" "要求:\n" "1. 补齐内容自然衔接当前文字,不要重复已有内容\n" "2. 长度控制在1-2个短句,不超过80字\n" "3. 语气专业、简洁,符合IT服务规范\n" "4. 只返回补齐的文字,不要加引号或其他标记" ) _TONE_ADJUST_SYSTEM_PROMPT: str = ( "你是一个IT服务话术改写助手。将坐席选中的文字改写为指定风格。\n\n" "语气定义:\n" "- 专业(professional):使用准确的技术术语,结构化表达,去除口语化内容\n" "- 友好(friendly):适当增加问候和关心语,语气更亲和\n" "- 简洁(concise):去除冗余,直奔主题,控制字数\n\n" "要求:\n" "1. 保持原意不变,只调整语气和表达方式\n" "2. 改写后的文字长度与原文相近(±30%)\n" "3. 符合IT服务坐席的专业规范\n" "4. 以JSON格式输出,包含 rewritten_text、tone、changes_summary 三个字段\n" "5. changes_summary 简述所做的修改" ) _POLISH_SYSTEM_PROMPT: str = ( "你是一个IT服务文字精修助手。对坐席输入的文字进行指定操作的处理。\n\n" "操作定义:\n" "- 扩写(expand):在原文基础上增加操作步骤、注意事项、解释说明,使回复更完整\n" "- 压缩(compress):精简表达,去除重复和冗余,保留核心信息,控制字数\n" "- 纠错(correct):检查并修正错别字、语法错误、标点符号、格式问题\n\n" "要求:\n" "1. 保持原意不变\n" "2. 以JSON格式输出,包含 polished_text、action、changes_summary 三个字段\n" "3. changes_summary 简述所做的修改" ) _REWRITE_SYSTEM_PROMPT: str = ( "你是一个IT服务智能回复助手。基于对话上下文和知识库,为坐席生成3个不同风格的备选回复。\n\n" "版本要求:\n" "- 版本1:简洁直接,一句话说明问题和解决方案\n" "- 版本2:详细带步骤,包含操作步骤和注意事项\n" "- 版本3:带知识库引用,引用相关文档并给出权威解决方案\n\n" "要求:\n" "1. 3个版本内容不重复,各有侧重\n" "2. 每个版本独立成段,用 --- 分隔(三个减号,独占一行)\n" "3. 只返回回复内容,不要加额外解释\n\n" "输出格式:\n" "版本1简洁内容\n---\n版本2详细内容\n---\n版本3知识库引用内容" ) # -------------------------------------------------------------------------- # 知识建议生成专用 Prompt(Tier0 / T03 — 通道 A/B 复用) # -------------------------------------------------------------------------- _KNOWLEDGE_SUGGESTION_PROMPT: str = ( "你是一个IT知识库优化助手,基于对话上下文分析知识库的不足," "生成结构化的知识库优化建议。\n\n" "分析以下对话,判断是否需要新增FAQ或更新已有知识条目。\n" "如果AI回复被标记为无用,生成更新建议;如果AI无法解决需转人工,生成新增FAQ建议。\n\n" "必须以JSON格式输出,包含以下字段:\n" "- suggestion_type: 建议类型,\"new_faq\" 或 \"update\"\n" "- title: 问题标题(简洁明了)\n" "- content: 答案内容(分步骤、可操作)\n" "- category: 分类,只能是 硬件/软件/网络/安全/账号/其他 之一\n" "- tags: 标签列表,如 [\"VPN\", \"连接\"]\n" "- confidence: 你对这个建议的信心度,0.0-1.0之间\n" "- issue: 对应的Neo4j图问题节点名称,如\"VPN问题\"\n" "- action: 对应的Neo4j图动作节点名称,如\"VPN连接修复\"\n" "- relation_type: 图关系类型,\"LEADS_TO\" 或 \"RELATES_TO\"\n" "- parent_issue: 父问题名称(如果没有则为空字符串)\n\n" "输出格式示例:\n" "{\"suggestion_type\": \"new_faq\", \"title\": \"VPN连不上怎么办\", " "\"content\": \"1.检查网络连接 2.重启VPN客户端 3.联系IT支持\", " "\"category\": \"网络\", \"tags\": [\"VPN\", \"连接\"], " "\"confidence\": 0.86, \"issue\": \"VPN问题\", " "\"action\": \"VPN连接修复\", \"relation_type\": \"LEADS_TO\", " "\"parent_issue\": \"网络问题\"}" ) def __init__(self, redis_client=None): """初始化 Wingman 服务。 从配置读取 Wingman Agent 的 API 地址和认证信息。 独立于 AIService,使用自己的 httpx 客户端。 Args: redis_client: Redis 异步客户端(可选,用于补齐结果缓存) """ # Wingman Agent 专用 API 端点 self.api_url = settings.dify_wingman_api_url # Wingman Agent API Key self.api_key = settings.dify_wingman_api_key # 请求超时(秒) self.timeout = settings.dify_wingman_timeout # httpx 异步客户端(复用连接池) self._client: Optional[httpx.AsyncClient] = None # Redis 客户端(可选,用于补齐结果缓存) self._redis = redis_client async def _get_client(self) -> httpx.AsyncClient: """获取或创建 httpx 异步客户端(懒加载)。 复用连接池,避免每次请求都创建新连接。 Returns: httpx.AsyncClient: 异步 HTTP 客户端实例 """ if self._client is None or self._client.is_closed: self._client = httpx.AsyncClient( timeout=httpx.Timeout(self.timeout), headers={ "Authorization": f"Bearer {self.api_key}", "Content-Type": "application/json", } ) return self._client async def close(self): """关闭 httpx 客户端,释放连接池资源。""" if self._client and not self._client.is_closed: await self._client.aclose() self._client = None logger.debug("WingmanService httpx client closed") # -------------------------------------------------------------------------- # 核心方法 1:生成 AI 草稿回复 # -------------------------------------------------------------------------- async def generate_draft( self, conversation_id: str, messages: List[Dict[str, Any]], db: Any = None, ) -> Dict[str, Any]: """生成 AI 草稿回复。 传入当前会话的完整消息历史,让 Wingman Agent 基于上下文 生成坐席可以采纳的草稿回复。 Args: conversation_id: 会话ID messages: 会话消息历史列表,每条消息包含 sender_type/content 等 db: 数据库会话(可选,当前未使用) Returns: Dict: { "content": str, # 草稿内容 "confidence": float, # 置信度(0-1) "reasoning": str, # 生成推理说明 } """ # 构建对话上下文消息列表 context_messages = self._build_context_messages( messages, self._DRAFT_SYSTEM_PROMPT ) try: result = await self._call_wingman_api(context_messages) if result is None: return { "content": "", "confidence": 0.0, "reasoning": "Wingman 服务暂不可用", } reply_content = result # 基于回复长度和内容质量估算置信度 confidence = self._estimate_confidence(reply_content) return { "content": reply_content, "confidence": confidence, "reasoning": f"基于最近 {len(messages)} 条对话上下文生成", } except Exception as e: logger.error(f"Wingman 草稿生成失败: {e}") return { "content": "", "confidence": 0.0, "reasoning": f"AI 服务异常: {str(e)}", } # -------------------------------------------------------------------------- # 核心方法 2:生成会话自动摘要 # -------------------------------------------------------------------------- async def generate_summary( self, conversation_id: str, messages: List[Dict[str, Any]], ) -> Dict[str, Any]: """生成会话自动摘要。 基于完整对话生成结构化摘要,包含问题、原因、解决方案。 结单时自动调用。 Args: conversation_id: 会话ID messages: 会话消息历史列表 Returns: Dict: { "problem": str, # 问题描述 "cause": str, # 原因分析 "solution": str, # 解决方案 } """ context_messages = self._build_context_messages( messages, self._SUMMARY_SYSTEM_PROMPT ) # 默认摘要(降级时使用) default_summary = { "problem": "无法自动生成摘要", "cause": "", "solution": "", } try: result = await self._call_wingman_api(context_messages) if result is None: return default_summary # 尝试解析 JSON 格式的摘要 parsed = self._parse_json_response(result, default_summary) return { "problem": parsed.get("problem", default_summary["problem"]), "cause": parsed.get("cause", default_summary["cause"]), "solution": parsed.get("solution", default_summary["solution"]), } except Exception as e: logger.error(f"Wingman 摘要生成失败: {e}") return default_summary # -------------------------------------------------------------------------- # 核心方法 3:生成自动标签建议 # -------------------------------------------------------------------------- async def suggest_tags( self, conversation_id: str, messages: List[Dict[str, Any]], existing_tags: Dict[str, Any] = None, ) -> Dict[str, Any]: """生成自动标签建议。 基于对话内容建议标签分类,包含标签列表、分类和优先级。 Args: conversation_id: 会话ID messages: 会话消息历史列表 existing_tags: 已有标签(可选,用于避免重复建议) Returns: Dict: { "suggested_tags": list[str], # 建议标签列表 "category": str, # 分类 "priority": str, # 优先级: low/medium/high } """ context_messages = self._build_context_messages( messages, self._TAGS_SYSTEM_PROMPT ) # 默认标签建议(降级时使用) default_tags = { "suggested_tags": [], "category": "", "priority": "medium", } try: result = await self._call_wingman_api(context_messages) if result is None: return default_tags # 尝试解析 JSON 格式的标签建议 parsed = self._parse_json_response(result, default_tags) return { "suggested_tags": parsed.get("suggested_tags", []), "category": parsed.get("category", ""), "priority": parsed.get("priority", "medium"), } except Exception as e: logger.error(f"Wingman 标签建议失败: {e}") return default_tags # -------------------------------------------------------------------------- # AI 辅助消息框:4 个核心方法 # -------------------------------------------------------------------------- async def generate_completion( self, conversation_id: str, current_text: str, messages: List[Dict[str, Any]], max_length: int = 80, ) -> Dict[str, Any]: """自动补齐。 根据坐席当前输入和对话上下文,生成下一句的补齐建议。 使用 Redis 缓存(TTL=30s)减少重复请求。 Args: conversation_id: 会话ID current_text: 坐席当前输入的文本 messages: 会话消息历史列表 max_length: 补齐建议最大长度(默认 80 字符) Returns: Dict: {"completion": str, "confidence": float} """ # 1. 检查 Redis 缓存 cache_key = self._make_cache_key(current_text, conversation_id) cached = await self._get_cache(cache_key) if cached: logger.info(f"自动补齐缓存命中: key={cache_key}") return cached # 2. 构建上下文消息 context = self._build_context_messages( messages, self._AUTOCOMPLETE_SYSTEM_PROMPT ) context.append({ "role": "user", "content": ( f"坐席正在输入:{current_text}\n" f"请补齐下一句话(不超过{max_length}字)" ), }) # 3. 调用 Dify(降低 temperature 提高准确性) try: result = await self._call_wingman_api(context, temperature=0.2) if result is None: return {"completion": "", "confidence": 0.0} completion = result.strip() confidence = self._estimate_confidence(completion) response = {"completion": completion, "confidence": confidence} # 4. 写入 Redis 缓存(TTL=30s) if self._redis: await self._set_cache(cache_key, response, ttl=30) return response except Exception as e: logger.error(f"自动补齐失败: {e}") return {"completion": "", "confidence": 0.0} async def adjust_tone( self, conversation_id: str, selected_text: str, full_text: str, tone: str, messages: List[Dict[str, Any]], ) -> Dict[str, Any]: """语气调整。 将坐席选中的文字改写为指定语气风格(专业/友好/简洁)。 Args: conversation_id: 会话ID selected_text: 选中的文字 full_text: 输入框完整内容(供 AI 理解上下文) tone: 目标语气:professional/friendly/concise messages: 会话消息历史列表 Returns: Dict: {"rewritten_text": str, "tone": str, "changes_summary": str} """ tone_label = {"professional": "专业", "friendly": "友好", "concise": "简洁"} context = self._build_context_messages( messages, self._TONE_ADJUST_SYSTEM_PROMPT ) context.append({ "role": "user", "content": ( f"目标语气:{tone_label.get(tone, tone)}\n" f"原文:{selected_text}\n" f"完整输入框内容(供上下文参考):{full_text}\n" f"请将原文改写为{tone_label.get(tone, tone)}风格。" ), }) try: result = await self._call_wingman_api(context, temperature=0.3) if result is None: return { "rewritten_text": "", "tone": tone, "changes_summary": "AI 服务暂不可用", } parsed = self._parse_json_response(result, {}) return { "rewritten_text": parsed.get("rewritten_text", result.strip()), "tone": tone, "changes_summary": parsed.get("changes_summary", "已完成语气调整"), } except Exception as e: logger.error(f"语气调整失败: {e}") return { "rewritten_text": "", "tone": tone, "changes_summary": "AI 服务暂不可用", } async def polish_text( self, conversation_id: str, text: str, action: str, messages: List[Dict[str, Any]], ) -> Dict[str, Any]: """文字润色。 对坐席输入的文字进行扩写/压缩/纠错处理。 Args: conversation_id: 会话ID text: 待润色文字 action: 润色操作:expand(扩写)/compress(压缩)/correct(纠错) messages: 会话消息历史列表 Returns: Dict: {"polished_text": str, "action": str, "changes_summary": str} """ action_label = {"expand": "扩写", "compress": "压缩", "correct": "纠错"} context = self._build_context_messages( messages, self._POLISH_SYSTEM_PROMPT ) context.append({ "role": "user", "content": ( f"操作类型:{action_label.get(action, action)}\n" f"原文:{text}\n" f"请对原文进行{action_label.get(action, action)}处理。" ), }) try: result = await self._call_wingman_api(context, temperature=0.3) if result is None: return { "polished_text": "", "action": action, "changes_summary": "AI 服务暂不可用", } parsed = self._parse_json_response(result, {}) return { "polished_text": parsed.get("polished_text", result.strip()), "action": action, "changes_summary": parsed.get("changes_summary", "已完成润色"), } except Exception as e: logger.error(f"文字润色失败: {e}") return { "polished_text": "", "action": action, "changes_summary": "AI 服务暂不可用", } async def rewrite_versions( self, conversation_id: str, current_text: str, messages: List[Dict[str, Any]], generate_count: int = 3, include_knowledge: bool = True, ) -> Dict[str, Any]: """智能改写。 基于对话上下文和知识库,生成多个不同风格的备选回复。 版本1: 简洁直接 / 版本2: 详细带步骤 / 版本3: 带知识库引用。 Args: conversation_id: 会话ID current_text: 当前输入文本(可为空) messages: 会话消息历史列表 generate_count: 生成版本数(默认 3) include_knowledge: 是否包含知识库引用版本(默认 True) Returns: Dict: {"versions": [{"text": str, "style": str, "source": str}, ...]} """ context = self._build_context_messages( messages, self._REWRITE_SYSTEM_PROMPT ) # 知识库检索(版本3) knowledge_snippets = "" if include_knowledge: query = current_text or " ".join( [m.get("content", "") for m in messages[-3:] ]) kb_result = await self._search_knowledge(query) if kb_result: knowledge_snippets = kb_result logger.info(f"改写知识库检索成功: {len(kb_result)} 字符") else: logger.info("改写知识库检索为空,降级为仅对话上下文") user_content = ( f"当前输入:{current_text or '无(请基于对话上下文生成)'}\n" ) if knowledge_snippets: user_content += f"\n知识库参考资料:\n{knowledge_snippets}\n" user_content += "\n请生成3个不同风格的备选回复。" context.append({"role": "user", "content": user_content}) try: result = await self._call_wingman_api(context, temperature=0.6) if result is None: return {"versions": []} # 按 --- 分割版本 parts = [p.strip() for p in result.split("\n---\n") if p.strip()] style_map = [ ("简洁直接", "对话上下文"), ("详细带步骤", "对话上下文"), ( "带知识库引用", "RAGFlow知识库" if knowledge_snippets else "对话上下文", ), ] versions = [] for i, text in enumerate(parts[:generate_count]): if i < len(style_map): style, source = style_map[i] else: style, source = f"版本{i+1}", "对话上下文" versions.append({"text": text, "style": style, "source": source}) return {"versions": versions} except Exception as e: logger.error(f"智能改写失败: {e}") return {"versions": []} # -------------------------------------------------------------------------- # 核心方法 4:生成知识库优化建议(Tier0 / T03 — 复用现有范式) # -------------------------------------------------------------------------- async def generate_knowledge_suggestion( self, context_messages: List[Dict[str, Any]], ) -> Dict[str, Any]: """生成知识库优化建议(用于知识库自动迭代)。 传入对话上下文,让 Wingman Agent 分析并生成结构化的知识库优化建议。 复用 _build_context_messages + _call_wingman_api + _parse_json_response 范式。 Args: context_messages: 对话消息历史列表 Returns: Dict: { "suggestion_type": str, # "new_faq" / "update" "title": str, # 问题标题 "content": str, # 答案内容 "category": str, # 分类 "tags": list[str], # 标签列表 "confidence": float, # 置信度(0.0-1.0) "issue": str, # 图节点:问题名称 "action": str, # 图节点:动作名称 "relation_type": str, # 图关系类型 "parent_issue": str, # 父 Issue 名称 } """ # 构建对话上下文消息列表(使用知识建议专用 Prompt) context = self._build_context_messages( context_messages, self._KNOWLEDGE_SUGGESTION_PROMPT ) # 默认建议(降级时使用) default_suggestion: Dict[str, Any] = { "suggestion_type": "new_faq", "title": "", "content": "", "category": "其他", "tags": [], "confidence": 0.0, "issue": "", "action": "", "relation_type": "LEADS_TO", "parent_issue": "", } try: result = await self._call_wingman_api(context) if result is None: logger.warning("Wingman 知识建议生成失败(API 返回 None)") return default_suggestion # 尝试解析 JSON 格式的建议 parsed = self._parse_json_response(result, default_suggestion) # 规范化字段 suggestion: Dict[str, Any] = { "suggestion_type": parsed.get( "suggestion_type", default_suggestion["suggestion_type"] ), "title": parsed.get("title", default_suggestion["title"]), "content": parsed.get("content", default_suggestion["content"]), "category": parsed.get("category", default_suggestion["category"]), "tags": parsed.get("tags", default_suggestion["tags"]), "confidence": float(parsed.get("confidence", 0.0)), "issue": parsed.get("issue", default_suggestion["issue"]), "action": parsed.get("action", default_suggestion["action"]), "relation_type": parsed.get( "relation_type", default_suggestion["relation_type"] ), "parent_issue": parsed.get( "parent_issue", default_suggestion["parent_issue"] ), } # 如果 Dify 未返回 confidence,使用启发式估算 if suggestion["confidence"] == 0.0: suggestion["confidence"] = self._estimate_confidence( suggestion["content"] ) logger.info( f"知识建议生成完成: type={suggestion['suggestion_type']}, " f"title={suggestion['title'][:50]}, " f"confidence={suggestion['confidence']}" ) return suggestion except Exception as e: logger.error(f"Wingman 知识建议生成异常: {e}") return default_suggestion # -------------------------------------------------------------------------- # 内部方法 # -------------------------------------------------------------------------- def _build_context_messages( self, messages: List[Dict[str, Any]], system_prompt: str, ) -> List[Dict[str, str]]: """构建发送给 Wingman Agent 的消息列表。 将数据库中的消息历史转换为 OpenAI Chat Completions 格式的 messages 列表,包含 system prompt 和对话上下文。 Args: messages: 数据库消息列表 system_prompt: 当前场景的 system prompt Returns: List[Dict]: OpenAI 格式的消息列表 """ # 构建上下文消息列表 context: List[Dict[str, str]] = [ {"role": "system", "content": system_prompt} ] # 角色映射:数据库 sender_type → OpenAI role role_map = { "employee": "user", # 员工消息 → user "agent": "assistant", # 坐席消息 → assistant "ai": "assistant", # AI消息 → assistant "system": "system", # 系统消息 → system } for msg in messages: role = role_map.get(msg.get("sender_type", ""), "user") content = msg.get("content", "") if content: # 跳过系统消息(已有 system prompt) if msg.get("sender_type") == "system": continue context.append({"role": role, "content": content}) return context async def _call_wingman_api( self, context_messages: List[Dict[str, str]], temperature: float = 0.3, ) -> Optional[str]: """调用 Wingman Agent API(非流式)。 Args: context_messages: OpenAI 格式的消息列表 temperature: 温度参数(0-1),默认 0.3。不同功能使用不同值: 补齐 0.2 / 语气调整 0.3 / 润色 0.3 / 改写 0.6 Returns: Optional[str]: AI 回复内容,失败时返回 None """ payload = { "model": "Chat", "messages": context_messages, "stream": False, "temperature": temperature, # 参数化温度,保持向后兼容(默认 0.3) } try: client = await self._get_client() logger.info(f"调用 Wingman API: messages_count={len(context_messages)}") response = await client.post(self.api_url, json=payload) response.raise_for_status() data = response.json() # 解析 OpenAI 兼容格式的返回 choices = data.get("choices", []) if not choices: logger.warning("Wingman API 返回空 choices") return None reply_content = choices[0]["message"]["content"] logger.info(f"Wingman API 返回: content_length={len(reply_content)}") return reply_content except httpx.TimeoutException: logger.error("Wingman API 超时") return None except httpx.HTTPStatusError as e: logger.error(f"Wingman API HTTP 错误: status={e.response.status_code}") return None except Exception as e: logger.error(f"Wingman API 调用失败: {e}") return None def _parse_json_response( self, content: str, default: Dict[str, Any], ) -> Dict[str, Any]: """解析 AI 返回的 JSON 内容。 Wingman Agent 可能返回带 markdown 代码块的 JSON, 也可能返回纯 JSON。此方法尝试多种解析方式。 Args: content: AI 返回的原始文本 default: 解析失败时的默认值 Returns: Dict: 解析后的字典,失败时返回默认值 """ if not content: return default # 尝试 1:直接解析 try: return json.loads(content) except json.JSONDecodeError: pass # 尝试 2:提取 markdown 代码块中的 JSON # AI 可能返回 ```json ... ``` 格式 import re json_match = re.search(r'```(?:json)?\s*\n?(.*?)\n?```', content, re.DOTALL) if json_match: try: return json.loads(json_match.group(1).strip()) except json.JSONDecodeError: pass # 尝试 3:查找第一个 { 到最后一个 } 之间的内容 start = content.find('{') end = content.rfind('}') if start != -1 and end != -1 and end > start: try: return json.loads(content[start:end + 1]) except json.JSONDecodeError: pass logger.warning(f"Wingman JSON 解析失败,使用默认值: {content[:200]}") return default def _estimate_confidence(self, content: str) -> float: """估算 AI 草稿回复的置信度。 基于回复长度和内容特征估算一个粗略的置信度值。 - 回复过短(< 10 字符):低置信度 - 回复包含不确定措辞:降低置信度 - 回复长度适中、内容具体:高置信度 Args: content: AI 回复内容 Returns: float: 置信度(0.0 - 1.0) """ if not content or len(content.strip()) < 5: return 0.2 confidence = 0.8 # 基础置信度 # 回复过短降低置信度 if len(content) < 10: confidence -= 0.3 elif len(content) < 30: confidence -= 0.1 # 包含不确定措辞降低置信度 uncertain_phrases = ["可能", "大概", "也许", "不确定", "建议您"] for phrase in uncertain_phrases: if phrase in content: confidence -= 0.05 # 包含具体步骤或链接提高置信度 confident_phrases = ["步骤", "请按以下", "点击", "打开", "http"] for phrase in confident_phrases: if phrase in content: confidence += 0.05 # 限制在 0.0 - 1.0 范围内 return max(0.0, min(1.0, confidence)) # -------------------------------------------------------------------------- # AI 辅助消息框:私有辅助方法 # -------------------------------------------------------------------------- def _make_cache_key(self, text: str, conversation_id: str) -> str: """生成 Redis 缓存 key。 格式: wingman:autocomplete:{md5(text + conversation_id)} 对输入文本 + 会话ID 做 MD5 哈希,避免 key 过长。 Args: text: 输入文本 conversation_id: 会话ID Returns: str: Redis 缓存 key """ import hashlib raw = f"{text}:{conversation_id}" digest = hashlib.md5(raw.encode()).hexdigest() return f"wingman:autocomplete:{digest}" async def _get_cache(self, key: str) -> Optional[Dict[str, Any]]: """读取 Redis 缓存。 Redis 不可用时返回 None(降级),不阻塞主流程。 Args: key: 缓存 key Returns: Optional[Dict]: 缓存的补齐结果,不存在或失败时返回 None """ if self._redis is None: return None try: data = await self._redis.get(key) if data: return json.loads(data) except Exception as e: logger.warning(f"Redis 缓存读取失败: {e}") return None async def _set_cache( self, key: str, value: Dict[str, Any], ttl: int = 30 ) -> None: """写入 Redis 缓存。 Redis 不可用时静默跳过(降级),不阻塞主流程。 Args: key: 缓存 key value: 缓存值(会序列化为 JSON) ttl: 过期时间(秒),默认 30 秒 """ if self._redis is None: return try: await self._redis.setex( key, ttl, json.dumps(value, ensure_ascii=False) ) except Exception as e: logger.warning(f"Redis 缓存写入失败: {e}") async def _search_knowledge(self, query: str) -> Optional[str]: """调用 RAGFlow 检索知识库。 用于改写版本3,从知识库获取参考资料。 检索失败时返回 None(降级),不影响其他版本生成。 Args: query: 检索查询(基于对话上下文) Returns: Optional[str]: 合并后的知识片段文本,失败或无结果时返回 None """ try: from app.integrations.factory import build_ragflow_client client = await build_ragflow_client() if client is None: logger.warning("RAGFlow 客户端不可用") return None # 使用默认知识库检索(不指定 dataset_ids 则使用全局) result = await client.retrieval( question=query, similarity_threshold=0.2, top_k=3, ) if result and result.chunks: snippets = "\n\n".join([ f"[{c.document_keyword or '未知文档'}] {c.content[:500]}" for c in result.chunks[:3] ]) return snippets except Exception as e: logger.error(f"RAGFlow 检索失败: {e}") return None