Files
wecom_it_smart_desk/backend/app/api/knowledge_iteration.py
T
Simon ead5f83bee feat(backend): knowledge iteration + vision + neo4j + response contract source
dependencies.py 拆分为 dependencies/ 包; 新增 vision/ragflow_ingestion/neo4j 客户端与 h5_ai_task; alembic 045 图置信度迁移; 响应契约统一收尾。
2026-07-09 11:47:16 +08:00

568 lines
20 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智能服务台 — 知识库自动迭代 API(Tier1 扩展)
# =============================================================================
# 说明:知识库自动迭代相关接口(扩展版)。
# 1. POST /api/admin/knowledge-iteration/analyze - 触发分析并生成建议
# 2. GET /api/admin/knowledge-iteration/suggestions - 获取建议列表(支持 audience/confidence 筛选)
# 3. GET /api/admin/knowledge-iteration/suggestions/{id} - 获取建议详情
# 4. POST /api/admin/knowledge-iteration/suggestions/{id}/approve - 审核通过(触发Neo4j写图)
# 5. POST /api/admin/knowledge-iteration/suggestions/{id}/reject - 审核拒绝
# 6. POST /api/admin/knowledge-iteration/suggestions/{id}/rewrite - 改写提案(Tier1新增)
# 7. POST /api/admin/knowledge-iteration/suggestions/{id}/queue - 放入独立队列(Tier1新增)
# 8. POST /api/admin/knowledge-iteration/suggestions/{id}/dequeue-approve - 队列中审批(Tier1新增)
# 9. GET /api/admin/knowledge-iteration/stats - 获取统计
# =============================================================================
import logging
from typing import Optional
from fastapi import APIRouter, Depends, Query
from sqlalchemy.ext.asyncio import AsyncSession
from app.database import get_db
from app.dependencies import get_current_user, require_admin, UserInfo
from app.models.knowledge_suggestion import KnowledgeSuggestion
from app.schemas.knowledge_suggestion import (
KnowledgeSuggestionListResponse,
KnowledgeSuggestionResponse,
KnowledgeSuggestionStatsResponse,
KnowledgeSuggestionApprove,
KnowledgeSuggestionReject,
KnowledgeSuggestionRewrite,
KnowledgeSuggestionMerge,
)
from app.services.knowledge_iteration_service import (
KnowledgeIterationService,
dep_knowledge_iteration_service,
)
from app.services.neo4j_client import get_neo4j_client
logger = logging.getLogger(__name__)
router = APIRouter()
# -----------------------------------------------------------------------------
# 触发分析
# -----------------------------------------------------------------------------
# POST /api/admin/knowledge-iteration/analyze
@router.post("/analyze")
@require_admin
async def trigger_analysis(
days: int = Query(default=7, ge=1, le=90, description="分析过去N天的数据"),
current_user: UserInfo = Depends(get_current_user),
db: AsyncSession = Depends(get_db),
service: KnowledgeIterationService = Depends(dep_knowledge_iteration_service),
):
"""触发知识库迭代分析。
分析过去N天的标注数据和会话数据,调用 Dify AI 自动生成优化建议。
- **days**: 分析过去N天的数据(默认7天,最大90天)
**需要管理员权限。**
"""
logger.info(f"管理员 {current_user.name} 触发了知识库迭代分析, days={days}")
result = await service.analyze_and_generate_suggestions(db, days=days)
return {
"code": 0,
"message": "分析完成",
"data": result,
}
# -----------------------------------------------------------------------------
# 获取建议列表(Tier1 扩展:audience/confidence 筛选)
# -----------------------------------------------------------------------------
# GET /api/admin/knowledge-iteration/suggestions
@router.get("/suggestions")
@require_admin
async def list_suggestions(
status: Optional[str] = Query(default=None, description="筛选状态:pending/queued/approved/rejected/applied/graph_synced/expired"),
suggestion_type: Optional[str] = Query(default=None, description="筛选类型:new_faq/update/outdated"),
audience: Optional[str] = Query(default=None, description="筛选受众:employee_quick_reply/engineer_workguide"),
confidence_min: Optional[float] = Query(default=None, ge=0.0, le=1.0, description="置信度下限"),
confidence_max: Optional[float] = Query(default=None, ge=0.0, le=1.0, description="置信度上限"),
page: int = Query(default=1, ge=1, description="页码"),
page_size: int = Query(default=20, ge=1, le=100, description="每页数量"),
current_user: UserInfo = Depends(get_current_user),
db: AsyncSession = Depends(get_db),
service: KnowledgeIterationService = Depends(dep_knowledge_iteration_service),
):
"""获取知识库优化建议列表(Tier1 扩展:支持 audience/confidence 筛选)。
- **status**: 筛选状态
- **suggestion_type**: 筛选类型
- **audience**: 按受众类型筛选(Tier1 新增)
- **confidence_min**: 置信度下限(Tier1 新增)
- **confidence_max**: 置信度上限(Tier1 新增)
- **page**: 页码
- **page_size**: 每页数量
**需要管理员权限。**
"""
from sqlalchemy import select, func
# 构建查询
stmt = select(KnowledgeSuggestion).order_by(
KnowledgeSuggestion.created_at.desc()
)
if status:
stmt = stmt.where(KnowledgeSuggestion.status == status)
if suggestion_type:
stmt = stmt.where(KnowledgeSuggestion.suggestion_type == suggestion_type)
if audience:
stmt = stmt.where(KnowledgeSuggestion.audience == audience)
if confidence_min is not None:
stmt = stmt.where(KnowledgeSuggestion.confidence >= confidence_min)
if confidence_max is not None:
stmt = stmt.where(KnowledgeSuggestion.confidence <= confidence_max)
# 分页
offset = (page - 1) * page_size
stmt = stmt.offset(offset).limit(page_size)
result = await db.execute(stmt)
suggestions = result.scalars().all()
# 统计总数
count_stmt = select(func.count()).select_from(KnowledgeSuggestion)
if status:
count_stmt = count_stmt.where(KnowledgeSuggestion.status == status)
if suggestion_type:
count_stmt = count_stmt.where(
KnowledgeSuggestion.suggestion_type == suggestion_type
)
if audience:
count_stmt = count_stmt.where(KnowledgeSuggestion.audience == audience)
if confidence_min is not None:
count_stmt = count_stmt.where(KnowledgeSuggestion.confidence >= confidence_min)
if confidence_max is not None:
count_stmt = count_stmt.where(KnowledgeSuggestion.confidence <= confidence_max)
total_result = await db.execute(count_stmt)
total = total_result.scalar()
return {
"code": 0,
"message": "success",
"data": {
"total": total,
"items": [
KnowledgeSuggestionResponse.model_validate(s) for s in suggestions
],
},
}
# -----------------------------------------------------------------------------
# 获取建议详情
# -----------------------------------------------------------------------------
# GET /api/admin/knowledge-iteration/suggestions/{id}
@router.get("/suggestions/{suggestion_id}")
@require_admin
async def get_suggestion(
suggestion_id: str,
current_user: UserInfo = Depends(get_current_user),
db: AsyncSession = Depends(get_db),
):
"""获取知识库优化建议详情。
- **suggestion_id**: 建议ID
**需要管理员权限。**
"""
from sqlalchemy import select
stmt = select(KnowledgeSuggestion).where(
KnowledgeSuggestion.id == suggestion_id
)
result = await db.execute(stmt)
suggestion = result.scalar_one_or_none()
if not suggestion:
return {"code": 404, "message": "建议不存在", "data": None}
return {
"code": 0,
"message": "success",
"data": KnowledgeSuggestionResponse.model_validate(suggestion),
}
# -----------------------------------------------------------------------------
# 审核通过(Tier1 扩展:串联 Neo4j 写图)
# -----------------------------------------------------------------------------
# POST /api/admin/knowledge-iteration/suggestions/{id}/approve
@router.post("/suggestions/{suggestion_id}/approve")
@require_admin
async def approve_suggestion(
suggestion_id: str,
body: KnowledgeSuggestionApprove,
current_user: UserInfo = Depends(get_current_user),
db: AsyncSession = Depends(get_db),
service: KnowledgeIterationService = Depends(dep_knowledge_iteration_service),
):
"""审核通过知识库优化建议(Tier1:串联 Neo4j 写图 + 五态流转)。
审核通过后:
1. 状态 pending/queued → approved → applied → graph_synced
2. 自动创建 KnowledgeBase 条目(派生视图)
3. 触发 Neo4j 图写入(D1 解读2 合一)
- **suggestion_id**: 建议ID
**需要管理员权限。**
"""
logger.info(
f"管理员 {current_user.name} 审核通过建议: {suggestion_id}"
)
# 尝试获取 Neo4j 客户端(可选,不影响审批主流程)
neo4j_client = await get_neo4j_client()
suggestion = await service.approve_suggestion(
db, suggestion_id, current_user.employee_id,
neo4j_client=neo4j_client,
)
if not suggestion:
return {"code": 404, "message": "建议不存在或状态转换无效", "data": None}
return {
"code": 0,
"message": "审核通过,建议已应用到知识库并同步至知识图谱",
"data": KnowledgeSuggestionResponse.model_validate(suggestion),
}
# -----------------------------------------------------------------------------
# 审核拒绝
# -----------------------------------------------------------------------------
# POST /api/admin/knowledge-iteration/suggestions/{id}/reject
@router.post("/suggestions/{suggestion_id}/reject")
@require_admin
async def reject_suggestion(
suggestion_id: str,
body: KnowledgeSuggestionReject,
current_user: UserInfo = Depends(get_current_user),
db: AsyncSession = Depends(get_db),
service: KnowledgeIterationService = Depends(dep_knowledge_iteration_service),
):
"""拒绝知识库优化建议。
- **suggestion_id**: 建议ID
**需要管理员权限。**
"""
logger.info(
f"管理员 {current_user.name} 拒绝建议: {suggestion_id}, "
f"理由: {body.reject_reason}"
)
suggestion = await service.reject_suggestion(
db, suggestion_id, current_user.employee_id, body.reject_reason
)
if not suggestion:
return {"code": 404, "message": "建议不存在", "data": None}
return {
"code": 0,
"message": "已拒绝该建议",
"data": KnowledgeSuggestionResponse.model_validate(suggestion),
}
# -----------------------------------------------------------------------------
# 改写提案(Tier1 新增 — D7 内联审批改写)
# -----------------------------------------------------------------------------
# POST /api/admin/knowledge-iteration/suggestions/{id}/rewrite
@router.post("/suggestions/{suggestion_id}/rewrite")
@require_admin
async def rewrite_suggestion(
suggestion_id: str,
body: KnowledgeSuggestionRewrite,
current_user: UserInfo = Depends(get_current_user),
db: AsyncSession = Depends(get_db),
service: KnowledgeIterationService = Depends(dep_knowledge_iteration_service),
):
"""训练师改写知识库优化建议(Tier1 新增)。
改写后提案状态重置为 pending,重新走审批流程。
可修改字段:title、content、category、tags、confidence、audience、
issue、action、relation_type、parent_issue。
- **suggestion_id**: 建议ID
**需要管理员权限。**
"""
logger.info(
f"管理员 {current_user.name} 改写建议: {suggestion_id}"
)
# 将非 None 的字段收集为改写数据
rewrite_data = body.model_dump(exclude_none=True, exclude_unset=True)
suggestion = await service.rewrite_suggestion(
db, suggestion_id, current_user.employee_id, rewrite_data
)
if not suggestion:
return {"code": 404, "message": "建议不存在", "data": None}
return {
"code": 0,
"message": "提案已改写,等待重新审批",
"data": KnowledgeSuggestionResponse.model_validate(suggestion),
}
# -----------------------------------------------------------------------------
# 放入独立队列(Tier1 新增 — D7 独立队列)
# -----------------------------------------------------------------------------
# POST /api/admin/knowledge-iteration/suggestions/{id}/queue
@router.post("/suggestions/{suggestion_id}/queue")
@require_admin
async def queue_suggestion(
suggestion_id: str,
current_user: UserInfo = Depends(get_current_user),
db: AsyncSession = Depends(get_db),
service: KnowledgeIterationService = Depends(dep_knowledge_iteration_service),
):
"""将建议放入独立审批队列(Tier1 新增)。
当会话关闭且提案仍处于 pending 时调用,将提案状态改为 queued。
- **suggestion_id**: 建议ID
**需要管理员权限。**
"""
logger.info(
f"管理员 {current_user.name} 将建议放入独立队列: {suggestion_id}"
)
suggestion = await service.queue_suggestion(db, suggestion_id)
if not suggestion:
return {"code": 404, "message": "建议不存在或状态转换无效", "data": None}
return {
"code": 0,
"message": "建议已放入独立审批队列",
"data": KnowledgeSuggestionResponse.model_validate(suggestion),
}
# -----------------------------------------------------------------------------
# 队列中审批通过(Tier1 新增 — D7 独立队列审批)
# -----------------------------------------------------------------------------
# POST /api/admin/knowledge-iteration/suggestions/{id}/dequeue-approve
@router.post("/suggestions/{suggestion_id}/dequeue-approve")
@require_admin
async def dequeue_approve_suggestion(
suggestion_id: str,
current_user: UserInfo = Depends(get_current_user),
db: AsyncSession = Depends(get_db),
service: KnowledgeIterationService = Depends(dep_knowledge_iteration_service),
):
"""从独立队列中审批通过建议(Tier1 新增)。
流程与 approve 一致:状态流转 + KB 落库 + Neo4j 写图。
- **suggestion_id**: 建议ID
**需要管理员权限。**
"""
logger.info(
f"管理员 {current_user.name} 从队列中审批通过建议: {suggestion_id}"
)
neo4j_client = await get_neo4j_client()
suggestion = await service.dequeue_approve(
db, suggestion_id, current_user.employee_id,
neo4j_client=neo4j_client,
)
if not suggestion:
return {"code": 404, "message": "建议不存在或状态转换无效", "data": None}
return {
"code": 0,
"message": "队列审批通过,建议已应用到知识库并同步至知识图谱",
"data": KnowledgeSuggestionResponse.model_validate(suggestion),
}
# -----------------------------------------------------------------------------
# 获取统计
# -----------------------------------------------------------------------------
# GET /api/admin/knowledge-iteration/stats
@router.get("/stats")
@require_admin
async def get_stats(
current_user: UserInfo = Depends(get_current_user),
db: AsyncSession = Depends(get_db),
service: KnowledgeIterationService = Depends(dep_knowledge_iteration_service),
):
"""获取知识库优化建议统计。
返回各状态的建议数量统计(含 queued/graph_synced/expired)。
**需要管理员权限。**
"""
stats = await service.get_suggestion_stats(db)
return {
"code": 0,
"message": "success",
"data": KnowledgeSuggestionStatsResponse(**stats),
}
# -----------------------------------------------------------------------------
# 知识图谱可视化(任务2P2
# -----------------------------------------------------------------------------
# GET /api/admin/knowledge-iteration/graph
@router.get("/graph")
@require_admin
async def get_knowledge_graph(
limit: int = Query(default=100, ge=10, le=500, description="节点数量上限"),
issue_name: Optional[str] = Query(default=None, description="指定Issue名称查询子图"),
current_user: UserInfo = Depends(get_current_user),
service: KnowledgeIterationService = Depends(dep_knowledge_iteration_service),
):
"""获取知识图谱数据(Neo4j 节点+关系 JSON 格式)。
返回 ECharts 力导向图兼容的节点和关系数据。
支持全图查询(默认)和指定 Issue 的子图查询。
- **limit**: 节点数量上限(10-500
- **issue_name**: 指定 Issue 名称时查询子图(用于审批卡片预览)
**需要管理员权限。**
"""
neo4j_client = await get_neo4j_client()
if not neo4j_client:
return {
"code": 0,
"message": "Neo4j 不可用,图数据为空",
"data": {"nodes": [], "links": []},
}
if issue_name:
graph_data = await neo4j_client.query_issue_subgraph(
issue_name=issue_name, depth=1
)
else:
graph_data = await neo4j_client.query_full_graph(limit=limit)
return {
"code": 0,
"message": "success",
"data": graph_data,
}
# -----------------------------------------------------------------------------
# 检查重复建议(任务3:P2 知识去重)
# -----------------------------------------------------------------------------
# GET /api/admin/knowledge-iteration/suggestions/{id}/duplicates
@router.get("/suggestions/{suggestion_id}/duplicates")
@require_admin
async def check_duplicates(
suggestion_id: str,
current_user: UserInfo = Depends(get_current_user),
db: AsyncSession = Depends(get_db),
service: KnowledgeIterationService = Depends(dep_knowledge_iteration_service),
):
"""检查指定建议是否存在重复(利用 Neo4j 图结构 + SQL 文本相似)。
在采纳建议前调用,检测是否有同名 Issue 或相似标题的已有条目。
返回重复项列表供训练师参考。
- **suggestion_id**: 建议ID
**需要管理员权限。**
"""
from sqlalchemy import select
stmt = select(KnowledgeSuggestion).where(
KnowledgeSuggestion.id == suggestion_id
)
result = await db.execute(stmt)
suggestion = result.scalar_one_or_none()
if not suggestion:
return {"code": 404, "message": "建议不存在", "data": None}
neo4j_client = await get_neo4j_client()
duplicates = await service.find_duplicates(
db=db,
issue_name=suggestion.issue,
title=suggestion.title,
suggestion_id=suggestion_id,
neo4j_client=neo4j_client,
)
return {
"code": 0,
"message": "success",
"data": {
"suggestion_id": suggestion_id,
"has_duplicates": len(duplicates) > 0,
"duplicates": duplicates,
},
}
# -----------------------------------------------------------------------------
# 合并重复建议(任务3:P2 知识去重)
# -----------------------------------------------------------------------------
# POST /api/admin/knowledge-iteration/suggestions/{id}/merge
@router.post("/suggestions/{suggestion_id}/merge")
@require_admin
async def merge_suggestions(
suggestion_id: str,
body: KnowledgeSuggestionMerge,
current_user: UserInfo = Depends(get_current_user),
db: AsyncSession = Depends(get_db),
service: KnowledgeIterationService = Depends(dep_knowledge_iteration_service),
):
"""合并重复建议(去重操作)。
将 duplicate_id 的建议合并到当前建议(primary),
标签和元数据合并,重复建议标记为 rejected(合并归入)。
- **suggestion_id**: 主建议ID(保留)
- **duplicate_id**: 重复建议ID(将被合并)
**需要管理员权限。**
"""
logger.info(
f"管理员 {current_user.name} 合并建议: "
f"primary={suggestion_id}, duplicate={body.duplicate_id}"
)
merged = await service.merge_suggestions(
db=db,
primary_id=suggestion_id,
duplicate_id=body.duplicate_id,
reviewer_id=current_user.employee_id,
)
if not merged:
return {"code": 404, "message": "主建议不存在", "data": None}
return {
"code": 0,
"message": "建议合并完成,重复建议已标记为已驳回(合并归入)",
"data": KnowledgeSuggestionResponse.model_validate(merged),
}