# ============================================================================= # 企微IT智能服务台 — 阶段5 自动化 Dify 客户端 # ============================================================================= # 说明:Dify 承担意图识别与 AI 编排。 # 1. 意图识别:根据员工消息判断命中哪个自动化场景 # (password_reset / software_install / virus_dispose / terminal_locate) # 2. AI 编排(可选):生成处置方案草案 # # 认证:Dify 开放 API 使用 Bearer Token(API Key)。 # 配置:AUTOMATION_DIFY_BASE_URL / AUTOMATION_DIFY_API_KEY(来自 settings)。 # # 容错:若 Dify 未配置或无结构化输出,detect_intent 走关键词兜底, # 保证 P0 四个场景在无真实 Dify 环境下也能演示闭环。 # ============================================================================= from __future__ import annotations import json import logging import re from typing import Any, Dict, Optional from app.config import settings from app.integrations.base import BaseClient, BaseClientError logger = logging.getLogger(__name__) class DifyClient(BaseClient): """Dify API 客户端(意图识别 / AI 编排)。""" system_name = "dify" def __init__( self, api_key: str, base_url: str, timeout: Optional[float] = None, audit=None, ): super().__init__(base_url=base_url, timeout=timeout, audit=audit) self.api_key = api_key def _headers(self) -> Dict[str, str]: return { "Authorization": f"Bearer {self.api_key}", "Content-Type": "application/json", } async def chat_completions( self, query: str, user: str = "automation", conversation_id: str = "", response_mode: str = "blocking", ) -> Dict[str, Any]: """调用 Dify Chat 补全(兼容 OpenAI Chat Completions 格式)。 Returns: Dict: {"answer": str, "conversation_id": str, ...} """ body = { "inputs": {}, "query": query, "user": user, "response_mode": response_mode, } if conversation_id: body["conversation_id"] = conversation_id return await self.request( "POST", "/v1/chat-messages", json_data=body, headers=self._headers(), event="dify.chat", ) async def detect_intent(self, message: str, employee_id: str = "") -> Dict[str, Any]: """意图识别:把员工消息发给 Dify,期望返回结构化场景意图。 解析策略: 1. 优先尝试从 answer 中解析 JSON(scenario_key + confidence) 2. 失败则走关键词兜底(无 Dify 结构化输出时也能跑通 P0) Returns: Dict: {"scenario_key": str|None, "confidence": float, "raw": str, "error": str} """ try: data = await self.chat_completions(query=message, user=employee_id or "automation") answer = data.get("answer", "") except BaseClientError as e: logger.warning(f"Dify 意图识别失败,转关键词兜底: {e}") fb = self._fallback_intent(message) fb["error"] = str(e) return fb intent = self._parse_intent(answer) if intent["scenario_key"] is None: # 关键词兜底 fb = self._fallback_intent(message) fb["raw"] = answer return fb return intent @staticmethod def _parse_intent(answer: str) -> Dict[str, Any]: """尝试从 Dify 返回中解析 JSON 意图。""" try: m = re.search(r"\{.*\}", answer, re.DOTALL) if m: obj = json.loads(m.group(0)) return { "scenario_key": obj.get("scenario_key"), "confidence": float(obj.get("confidence", 0.0)), "raw": answer, } except Exception: pass return {"scenario_key": None, "confidence": 0.0, "raw": answer} @staticmethod def _fallback_intent(message: str) -> Dict[str, Any]: """关键词兜底意图识别(无 Dify 结构化输出时使用)。""" text = (message or "").lower() rules = [ (("密码", "重置", "password", "忘密码", "修改密码"), "password_reset"), (("安装", "软件", "install", "software", "wps", "office", "下载"), "software_install"), (("病毒", "杀毒", "virus", "勒索", "木马", "火绒", "huorong"), "virus_dispose"), (("定位", "终端", "电脑在哪", "locate", "terminal", "找电脑"), "terminal_locate"), ] for keywords, key in rules: if any(k in text for k in keywords): return {"scenario_key": key, "confidence": 0.75, "raw": ""} return {"scenario_key": None, "confidence": 0.0, "raw": ""} async def get_dify_client(audit=None) -> Optional[DifyClient]: """从 settings 构建 Dify 客户端;未配置返回 None。""" base_url = settings.automation_dify_base_url api_key = settings.automation_dify_api_key if not base_url or not api_key: return None return DifyClient(api_key=api_key, base_url=base_url, audit=audit)