285 lines
10 KiB
Python
285 lines
10 KiB
Python
|
|
# =============================================================================
|
|||
|
|
# 企微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
|