Files
wecom_it_smart_desk/backend/app/services/dify_triage_service.py
T

285 lines
10 KiB
Python
Raw Normal View History

# =============================================================================
# 企微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