Files
wecom_it_smart_desk/backend/app/api/triage.py
T

422 lines
12 KiB
Python
Raw Normal View History

# =============================================================================
# 企微IT智能服务台 — 分诊交互 API
# =============================================================================
# 说明:分诊交互相关接口,包括:
# H5 端(5个):
# POST /api/h5/triage/start — 发起分诊
# POST /api/h5/triage/step — 提交步骤选择
# POST /api/h5/triage/skip — 跳过步骤
# POST /api/h5/triage/transfer — 转人工
# POST /api/h5/triage/complete — 分诊完成
# 坐席端(7个):
# GET /api/agent/triage/pending — 待分诊列表
# GET /api/agent/triage/stats — 统计概要
# GET /api/agent/triage/history — 历史列表
# GET /api/agent/triage/export — 导出 xlsx
# GET /api/agent/triage/{triage_id} — 分诊详情
# POST /api/agent/triage/{triage_id}/route — 路由操作
# POST /api/agent/triage/{triage_id}/exclude-options — 排除选项
#
# 注意:固定路径路由(/history, /export)必须在参数路由(/{triage_id})之前注册,
# 否则 FastAPI 会将 "history"/"export" 误匹配为 triage_id。
# =============================================================================
import logging
from typing import Optional
from fastapi import APIRouter, Depends, Query, Response
from sqlalchemy.ext.asyncio import AsyncSession
from app.database import get_db
from app.dependencies import get_current_user, UserInfo
from app.schemas.triage import (
TriageStartRequest,
TriageStepRequest,
TriageSkipRequest,
TriageTransferRequest,
TriageCompleteRequest,
TriageRouteRequest,
TriageExcludeOptionsRequest,
)
from app.services.triage_service import get_triage_service
logger = logging.getLogger(__name__)
router = APIRouter()
# 坐席端认证依赖(延迟导入避免循环依赖)
def _get_current_agent():
from app.api.agents import get_current_agent
return get_current_agent
# =============================================================================
# H5 端接口(5个)
# =============================================================================
@router.post("/h5/triage/start")
async def start_triage(
body: TriageStartRequest,
current_user: UserInfo = Depends(get_current_user),
db: AsyncSession = Depends(get_db),
):
"""发起分诊。
员工在 H5 端发送问题后,调用此接口发起 AI 分诊。
后端创建分诊会话,调用 Dify 分诊应用分析问题并生成分步选择题。
5秒超时自动转人工。
- **conversation_id**: 会话ID
- **question**: 员工问题文本
"""
service = get_triage_service()
result = await service.start_triage(
db=db,
conversation_id=body.conversation_id,
question=body.question,
user_id=current_user.employee_id,
user_name=current_user.name,
user_dept=current_user.department,
)
if result.get("status") == "timeout":
return {
"code": 0,
"message": result.get("message", "分诊超时,已自动转人工"),
"data": result,
}
return {
"code": 0,
"message": "success",
"data": result,
}
@router.post("/h5/triage/step")
async def submit_step(
body: TriageStepRequest,
current_user: UserInfo = Depends(get_current_user),
db: AsyncSession = Depends(get_db),
):
"""提交步骤选择。
员工选择某个选项后,提交到后端记录上下文并获取下一步骤。
- **triage_id**: 分诊会话ID
- **step_index**: 当前步骤序号(0-based
- **selected_label**: 选择的选项标签
"""
service = get_triage_service()
result = await service.submit_step(
db=db,
triage_id=body.triage_id,
step_index=body.step_index,
selected_label=body.selected_label,
)
if "error" in result:
return {"code": 404, "message": result["error"], "data": None}
return {
"code": 0,
"message": "success",
"data": result,
}
@router.post("/h5/triage/skip")
async def skip_step(
body: TriageSkipRequest,
current_user: UserInfo = Depends(get_current_user),
db: AsyncSession = Depends(get_db),
):
"""跳过步骤。
员工跳过当前步骤,直接进入下一步。
- **triage_id**: 分诊会话ID
- **step_index**: 要跳过的步骤序号
"""
service = get_triage_service()
result = await service.skip_step(
db=db,
triage_id=body.triage_id,
step_index=body.step_index,
)
if "error" in result:
return {"code": 404, "message": result["error"], "data": None}
return {
"code": 0,
"message": "success",
"data": result,
}
@router.post("/h5/triage/transfer")
async def transfer_to_human(
body: TriageTransferRequest,
current_user: UserInfo = Depends(get_current_user),
db: AsyncSession = Depends(get_db),
):
"""转人工。
员工主动选择转人工,后端记录上下文并将会话状态改为转人工。
- **triage_id**: 分诊会话ID
- **context**: 已收集的上下文列表
"""
service = get_triage_service()
result = await service.transfer_to_human(
db=db,
triage_id=body.triage_id,
context=body.context,
)
if "error" in result:
return {"code": 404, "message": result["error"], "data": None}
return {
"code": 0,
"message": "已转接人工坐席",
"data": result,
}
@router.post("/h5/triage/complete")
async def complete_triage(
body: TriageCompleteRequest,
current_user: UserInfo = Depends(get_current_user),
db: AsyncSession = Depends(get_db),
):
"""分诊完成。
所有步骤完成后,调用 Dify 生成最终 AI 回复。
- **triage_id**: 分诊会话ID
- **context**: 已收集的上下文列表
"""
service = get_triage_service()
result = await service.complete_triage(
db=db,
triage_id=body.triage_id,
context=body.context,
)
if "error" in result:
return {"code": 404, "message": result["error"], "data": None}
return {
"code": 0,
"message": "success",
"data": result,
}
# =============================================================================
# 坐席端接口(7个)
# =============================================================================
@router.get("/agent/triage/pending")
async def list_pending(
urgency: Optional[str] = Query(default=None, description="紧急度筛选:high/medium/low"),
problem_type: Optional[str] = Query(default=None, description="问题类型筛选"),
page: int = Query(default=1, ge=1, description="页码"),
page_size: int = Query(default=20, ge=1, le=100, description="每页数量"),
agent=Depends(_get_current_agent()),
db: AsyncSession = Depends(get_db),
):
"""获取待分诊列表(按紧急度排序)。
返回状态为 pending/triaging 的分诊会话,按紧急度排序(high > medium > low)。
**需要坐席认证。**
"""
service = get_triage_service()
result = await service.list_pending(
db=db,
urgency=urgency,
problem_type=problem_type,
page=page,
page_size=page_size,
)
return {
"code": 0,
"message": "success",
"data": result,
}
@router.get("/agent/triage/stats")
async def get_stats(
agent=Depends(_get_current_agent()),
db: AsyncSession = Depends(get_db),
):
"""获取分诊看板统计概要。
返回6项统计指标:待分诊数/今日已分诊/AI自答/转人工/自动审批/平均耗时。
**需要坐席认证。**
"""
service = get_triage_service()
result = await service.get_stats(db=db)
return {
"code": 0,
"message": "success",
"data": result,
}
@router.get("/agent/triage/history")
async def get_history(
date_from: Optional[str] = Query(default=None, description="开始日期(ISO格式)"),
date_to: Optional[str] = Query(default=None, description="结束日期(ISO格式)"),
route_action: Optional[str] = Query(default=None, description="路由动作筛选"),
page: int = Query(default=1, ge=1, description="页码"),
page_size: int = Query(default=20, ge=1, le=100, description="每页数量"),
agent=Depends(_get_current_agent()),
db: AsyncSession = Depends(get_db),
):
"""获取已分诊历史列表。
返回状态为 routed/skipped/timeout 的分诊会话。
**需要坐席认证。**
"""
service = get_triage_service()
result = await service.get_history(
db=db,
date_from=date_from,
date_to=date_to,
route_action=route_action,
page=page,
page_size=page_size,
)
return {
"code": 0,
"message": "success",
"data": result,
}
@router.get("/agent/triage/export")
async def export_sessions(
date_from: Optional[str] = Query(default=None, description="开始日期(ISO格式)"),
date_to: Optional[str] = Query(default=None, description="结束日期(ISO格式)"),
agent=Depends(_get_current_agent()),
db: AsyncSession = Depends(get_db),
):
"""导出分诊记录为 xlsx。
导出基础字段 + 分诊步骤详情。
**需要坐席认证。**
"""
service = get_triage_service()
xlsx_data = await service.export_sessions(
db=db,
date_from=date_from,
date_to=date_to,
)
return Response(
content=xlsx_data,
media_type="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
headers={
"Content-Disposition": "attachment; filename=triage_sessions.xlsx",
},
)
@router.get("/agent/triage/{triage_id}")
async def get_detail(
triage_id: str,
agent=Depends(_get_current_agent()),
db: AsyncSession = Depends(get_db),
):
"""获取分诊详情。
返回分诊会话的完整数据,包括用户画像、问题描述、AI分析结果、已收集上下文等。
**需要坐席认证。**
"""
service = get_triage_service()
result = await service.get_detail(db=db, triage_id=triage_id)
if not result:
return {"code": 404, "message": "分诊会话不存在", "data": None}
return {
"code": 0,
"message": "success",
"data": result,
}
@router.post("/agent/triage/{triage_id}/route")
async def route_session(
triage_id: str,
body: TriageRouteRequest,
agent=Depends(_get_current_agent()),
db: AsyncSession = Depends(get_db),
):
"""坐席路由操作(覆盖 AI 建议)。
坐席可选择4种路由动作:ai_self(AI自答)/ human(转人工)/ auto_approval(自动审批)/ skip(跳过)。
**需要坐席认证。**
"""
service = get_triage_service()
result = await service.route_session(
db=db,
triage_id=triage_id,
route_action=body.route_action,
route_note=body.route_note,
operator_id=agent.user_id if hasattr(agent, "user_id") else str(agent.id),
)
if not result:
return {"code": 404, "message": "分诊会话不存在", "data": None}
return {
"code": 0,
"message": "路由操作成功",
"data": result,
}
@router.post("/agent/triage/{triage_id}/exclude-options")
async def exclude_options(
triage_id: str,
body: TriageExcludeOptionsRequest,
agent=Depends(_get_current_agent()),
db: AsyncSession = Depends(get_db),
):
"""坐席排除/推荐分诊选项(WS 推送到 H5)。
坐席可排除某些选项或推荐某个选项,通过 WebSocket 实时推送到 H5 端。
**需要坐席认证。**
"""
service = get_triage_service()
result = await service.exclude_options(
db=db,
triage_id=triage_id,
excluded_labels=body.excluded_labels,
recommended_label=body.recommended_label,
)
if "error" in result:
return {"code": 404, "message": result["error"], "data": None}
return {
"code": 0,
"message": "success",
"data": result,
}