Files
wecom_it_smart_desk/backend/app/services/dify_triage_service.py
T
Simon 449c6d4875 feat: 2026-07-12~13 全量更新 - AI对话链路改造+H5 v4/v5+坐席端v5+上下文感知诊断+知识库迭代3
## H5 员工端 v4 (2026-07-13 00:48 已部署)
- 人工按钮三态文案统一为"人工坐席"
- 按钮位置移至发送键和语音按钮上方(垂直堆叠)
- 点按钮直接调 store.shakeAgent(),删除 CallAgentModal 弹窗动画
- 截图快捷键提示改为"截图->粘贴:Alt+Shift+A-Ctrl+V ---> Ctrl+V"
- 移动端隐藏截图提示(CSS 媒体查询)
- AI转人工提示改为"已为您呼叫人工坐席,请稍等!"
- 坐席接入提示改为"坐席正在查看您的信息,请等待处理回复!"
- 删除"摇铃呼叫坐席"入口和文案
- 删除孤儿组件 MessageList.vue + shake 动画 CSS

## H5 员工端 v5 (2026-07-13 02:08 已部署)
- RightPanel v2.1:删除"软件安装"和"资源权限"标签页
- 移除标签栏,智能推荐(DynamicRecommend)直接展示
- 删除 SoftwareDownloads/ApprovalLinks 引用和相关 CSS

## AI 对话链路全栈改造 Phase 1-6 (已部署)
- Phase 1: Dify JSON输出 + 后端blocking解析 + 双WS推送 + 错误降级
- Phase 2: 关键词收窄(~25强意图词) + 两级分类Prompt + 删除前端checkApprovalIntent
- Phase 3: WS扩展(ai_thinking+dynamic_recommend) + ai_structured气泡 + RightPanel v2 + 选项回传
- Phase 4: VisionService接入 + 图片消息融合(5秒窗口) + 降级策略
- Phase 5: 坐席端ai_thinking指示器 + ai_structured/byod_card渲染 + handleNewMessage修复
- Phase 6: diagnosis_stage(6值) + response_time_ms计时 + 慢响应告警(>10s)

## 坐席端 v5 (2026-07-13 01:38 已部署)
- ai_structured/byod_card 只读渲染
- AI思考指示器 UI
- handleNewMessage 透传 msg_type/extra_data 修复
- 布局优化v2.0: QuickReplyBar L1+L2悬浮 + ReplyBox左右分区 + 右栏260/560px切换
- 键盘快捷键v2.3: 纯数字路由 + ESC分层撤销 + Shift+Space用event.code

## 上下文感知智能诊断闭环 (2026-07-12 已部署)
- 三层诊断(API→Script→AI) + 三段排队(VIP→info_locked→not locked)
- 答题插队 + 五场景关闭
- 迁移052(6表+6列) + queue_service + quiz_service + closing_service
- H5前端: QueueWaiting + RightPanel双Tab + InputBar三态 + ResolveConfirmCard
- 坐席前端: pending_close结单流程 + 信息锁定(Dify步骤完成+有效回答率≥70%)

## 知识库迭代3 (2026-07-12 已部署)
- 分诊交互(H5+坐席+Dify独立应用)
- 拓扑预览(ECharts只读)
- 代答排除(4种匹配器: keyword/regex/intent/category)
- 迁移051 + 44文件43测试通过

## 后端变更
- 6个Python文件改造(h5_ai_task.py/h5.py/ai_service.py/closing_service.py等)
- funny_phrase_service.py: shake/connected/keyword 默认文案更新
- session_service.py: 企微消息文案同步
- 新增: queue.py/quiz.py/triage.py/exclusion_rules.py 等API端点
- 新增: diagnostic.py/quiz.py/triage_session.py 等模型
- 新增: closing_service/queue_service/quiz_service/triage_service 等服务

## 文档更新
- CHANGELOG.md: 新增 [未发布] 区全部变更记录
- 项目管理主文档 v2.5: 新增v0.7.3版本 + 已完成看板 + 最近搞定
- 版本记录: 新增v0.7.3条目
- AI对话链路实施计划: Phase 1-6 全部标记已实施
- 新增架构图/时序图/类图(mermaid)

## 部署路径修正
- 服务器项目根路径: /opt/wecom-it-desk/
- 所有前端dist均为ro bind mount,只能在宿主机源路径操作
- 服务器nginx /h5/ 是静态文件服务(非proxy_pass)
- elFinder上传二进制不可靠(MD5不匹配),改用base64分块上传
2026-07-13 02:17:03 +08:00

285 lines
10 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# =============================================================================
# 企微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