141 lines
5.2 KiB
Python
141 lines
5.2 KiB
Python
|
|
# =============================================================================
|
|||
|
|
# 企微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)
|