# ============================================================================= # 企微IT智能服务台 — Dify 分诊服务 # ============================================================================= # 说明:对接 Dify OpenAI 兼容接口,调用独立分诊应用进行问题分析。 # 功能: # 1. analyze — 首次分诊分析,将问题拆分为分步选择题 # 2. get_next_step — 根据已选选项动态调整后续步骤 # 3. generate_reply — 根据收集的上下文生成最终回复 # 降级处理:Dify 不可用时返回友好错误,不中断主流程。 # ============================================================================= import json import logging from typing import Any, Dict, List, Optional import httpx from app.config import settings logger = logging.getLogger(__name__) # ============================================================================= # Dify 分诊 System Prompt # ============================================================================= TRIAGE_SYSTEM_PROMPT = """你是IT服务台智能分诊引擎,负责分析员工IT问题并拆分为分步选择题。 ## 任务目标 1. 识别问题类型(硬件/软件/网络/安全/账号/其他)和具体分类 2. 评估置信度(0.0-1.0)和紧急度(high/medium/low) 3. 将复杂问题拆分为分步选择题(简单问题1-2步,复杂问题3-5步) 4. 每步最多4个选项,每个选项分配概率(0-1),所有选项概率之和为1 5. 推荐路由渠道(ai_self/human/auto_approval) ## 紧急度规则 - 消息含"紧急/马上/宕机/无法工作/崩溃/死机/蓝屏"→high - 消息含"报错/失败/连不上/打不开/不能用"→medium - 其余→low ## 排除选项 excluded_options 中的选项不出现在后续步骤中。 ## 输出约束 必须输出合法JSON,不要输出解释性文字。JSON格式如下: { "triage_type": "confirm|transfer|approval", "confidence": 0.85, "urgency": "high|medium|low", "problem_type": "硬件|软件|网络|安全|账号|其他", "problem_category": "Outlook", "suggested_route": "ai_self|human|auto_approval", "matched_knowledge": "匹配到的知识条目描述", "match_score": 0.89, "context_tags": ["标签1", "标签2"], "triage_steps": [ { "question": "步骤问题文本", "options": [ {"label": "选项A", "probability": 0.68}, {"label": "选项B", "probability": 0.22} ] } ], "total_steps": 3, "reply": "AI回复文本(当triage_type=confirm时,引导员工选择)" } ## 置信度评估 基于知识库匹配度、问题清晰度、上下文完整度综合评估。""" class DifyTriageService: """Dify 分诊应用对接服务。 通过 OpenAI 兼容接口调用 Dify 独立分诊应用, 支持首次分析、动态步骤调整和最终回复生成。 Attributes: api_url: Dify OpenAI 兼容接口地址 api_key: Dify API Key timeout: 请求超时时间(秒) """ def __init__(self): """初始化 Dify 分诊服务。""" self.api_url = settings.dify_triage_api_url self.api_key = settings.dify_triage_api_key self.timeout = settings.dify_triage_timeout def is_available(self) -> bool: """检查 Dify 分诊服务是否可用。 Returns: bool: API URL 和 Key 均已配置时返回 True """ return bool(self.api_url and self.api_key) async def analyze( self, question: str, context: Optional[List[str]] = None, excluded_options: Optional[List[str]] = None, step_index: int = 0, ) -> Dict[str, Any]: """调用 Dify 分诊应用进行首次分析。 Args: question: 员工问题文本 context: 已收集的上下文标签(分步选择中累积) excluded_options: 坐席已排除的选项标签 step_index: 当前步骤序号(0=首次分诊) Returns: Dict[str, Any]: Dify 返回的分诊结果 JSON Raises: RuntimeError: Dify 不可用或返回格式错误 """ if not self.is_available(): logger.warning("Dify 分诊服务未配置,降级处理") raise RuntimeError("Dify 分诊服务未配置") # 构建用户消息内容(JSON 格式传入输入参数) user_content = json.dumps( { "question": question, "collected_context": context or [], "excluded_options": excluded_options or [], "step_index": step_index, }, ensure_ascii=False, ) try: async with httpx.AsyncClient(timeout=self.timeout) as client: resp = await client.post( f"{self.api_url}/v1/chat/completions", headers={ "Authorization": f"Bearer {self.api_key}", "Content-Type": "application/json", }, json={ "model": "triage-engine", "messages": [ { "role": "system", "content": TRIAGE_SYSTEM_PROMPT, }, {"role": "user", "content": user_content}, ], "temperature": 0.3, "max_tokens": 2000, }, ) resp.raise_for_status() # 解析 OpenAI 兼容响应格式 resp_data = resp.json() content = resp_data["choices"][0]["message"]["content"] # Dify 返回的是 JSON 字符串,需要解析 # 兼容 markdown 代码块包裹的 JSON content = content.strip() if content.startswith("```json"): content = content[7:] if content.startswith("```"): content = content[3:] if content.endswith("```"): content = content[:-3] content = content.strip() result = json.loads(content) logger.info( "Dify 分诊分析成功: problem_type=%s, confidence=%s, urgency=%s", result.get("problem_type"), result.get("confidence"), result.get("urgency"), ) return result except httpx.TimeoutException: logger.error("Dify 分诊请求超时(%s秒)", self.timeout) raise RuntimeError(f"Dify 分诊请求超时({self.timeout}秒)") except httpx.HTTPStatusError as e: logger.error("Dify 分诊 HTTP 错误: %s, status=%s", e, e.response.status_code) raise RuntimeError(f"Dify 分诊服务返回错误: {e.response.status_code}") except json.JSONDecodeError as e: logger.error("Dify 分诊返回 JSON 解析失败: %s", e) raise RuntimeError("Dify 分诊返回格式错误") except Exception as e: logger.error("Dify 分诊调用异常: %s", e, exc_info=True) raise RuntimeError(f"Dify 分诊调用异常: {e}") async def generate_reply( self, question: str, collected_context: List[str], ) -> Dict[str, Any]: """根据收集的上下文生成最终回复。 Args: question: 原始问题文本 collected_context: 分诊过程中收集的所有上下文 Returns: Dict[str, Any]: 包含 reply 和 confidence 的字典 Raises: RuntimeError: Dify 不可用或返回格式错误 """ if not self.is_available(): logger.warning("Dify 分诊服务未配置,降级处理(生成回复)") raise RuntimeError("Dify 分诊服务未配置") user_content = json.dumps( { "question": question, "collected_context": collected_context, "excluded_options": [], "step_index": -1, # -1 表示最终回复生成 }, ensure_ascii=False, ) try: async with httpx.AsyncClient(timeout=self.timeout) as client: resp = await client.post( f"{self.api_url}/v1/chat/completions", headers={ "Authorization": f"Bearer {self.api_key}", "Content-Type": "application/json", }, json={ "model": "triage-engine", "messages": [ { "role": "system", "content": TRIAGE_SYSTEM_PROMPT, }, {"role": "user", "content": user_content}, ], "temperature": 0.3, "max_tokens": 2000, }, ) resp.raise_for_status() resp_data = resp.json() content = resp_data["choices"][0]["message"]["content"] content = content.strip() if content.startswith("```json"): content = content[7:] if content.startswith("```"): content = content[3:] if content.endswith("```"): content = content[:-3] content = content.strip() result = json.loads(content) return { "reply": result.get("reply", ""), "confidence": result.get("confidence", 0.0), } except Exception as e: logger.error("Dify 分诊生成回复异常: %s", e, exc_info=True) raise RuntimeError(f"Dify 分诊生成回复异常: {e}") # 单例 _dify_triage_service: Optional[DifyTriageService] = None def get_dify_triage_service() -> DifyTriageService: """获取 DifyTriageService 单例。 Returns: DifyTriageService: 单例实例 """ global _dify_triage_service if _dify_triage_service is None: _dify_triage_service = DifyTriageService() return _dify_triage_service