422 lines
12 KiB
Python
422 lines
12 KiB
Python
|
|
# =============================================================================
|
|||
|
|
# 企微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,
|
|||
|
|
}
|