feat(backend): knowledge iteration + vision + neo4j + response contract source

dependencies.py 拆分为 dependencies/ 包; 新增 vision/ragflow_ingestion/neo4j 客户端与 h5_ai_task; alembic 045 图置信度迁移; 响应契约统一收尾。
This commit is contained in:
Simon
2026-07-09 11:47:16 +08:00
parent f5374fce9b
commit ead5f83bee
39 changed files with 5276 additions and 545 deletions
+332 -31
View File
@@ -1,13 +1,16 @@
# =============================================================================
# 企微IT智能服务台 — 知识库自动迭代 API
# 企微IT智能服务台 — 知识库自动迭代 APITier1 扩展)
# =============================================================================
# 说明:知识库自动迭代相关接口
# 说明:知识库自动迭代相关接口(扩展版)。
# 1. POST /api/admin/knowledge-iteration/analyze - 触发分析并生成建议
# 2. GET /api/admin/knowledge-iteration/suggestions - 获取建议列表
# 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 - 审核通过
# 4. POST /api/admin/knowledge-iteration/suggestions/{id}/approve - 审核通过(触发Neo4j写图)
# 5. POST /api/admin/knowledge-iteration/suggestions/{id}/reject - 审核拒绝
# 6. GET /api/admin/knowledge-iteration/stats - 获取统计
# 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
@@ -17,19 +20,22 @@ from fastapi import APIRouter, Depends, Query
from sqlalchemy.ext.asyncio import AsyncSession
from app.database import get_db
from app.dependencies import require_admin
from app.models.user import User
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__)
@@ -41,21 +47,22 @@ 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: User = Depends(require_admin),
current_user: UserInfo = Depends(get_current_user),
db: AsyncSession = Depends(get_db),
service: KnowledgeIterationService = Depends(dep_knowledge_iteration_service),
):
"""触发知识库迭代分析。
分析过去N天的标注数据和会话数据,自动生成优化建议。
分析过去N天的标注数据和会话数据,调用 Dify AI 自动生成优化建议。
- **days**: 分析过去N天的数据(默认7天,最大90天)
**需要管理员权限。**
"""
logger.info(f"管理员 {current_user.username} 触发了知识库迭代分析, days={days}")
logger.info(f"管理员 {current_user.name} 触发了知识库迭代分析, days={days}")
result = await service.analyze_and_generate_suggestions(db, days=days)
@@ -67,23 +74,30 @@ async def trigger_analysis(
# -----------------------------------------------------------------------------
# 获取建议列表
# 获取建议列表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="筛选状态"),
suggestion_type: Optional[str] = Query(default=None, description="筛选类型"),
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: User = Depends(require_admin),
current_user: UserInfo = Depends(get_current_user),
db: AsyncSession = Depends(get_db),
service: KnowledgeIterationService = Depends(dep_knowledge_iteration_service),
):
"""获取知识库优化建议列表。
"""获取知识库优化建议列表Tier1 扩展:支持 audience/confidence 筛选)
- **status**: 筛选状态pending/approved/rejected/applied
- **suggestion_type**: 筛选类型new_faq/update/outdated
- **status**: 筛选状态
- **suggestion_type**: 筛选类型
- **audience**: 按受众类型筛选(Tier1 新增)
- **confidence_min**: 置信度下限(Tier1 新增)
- **confidence_max**: 置信度上限(Tier1 新增)
- **page**: 页码
- **page_size**: 每页数量
@@ -100,6 +114,12 @@ async def list_suggestions(
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
@@ -116,6 +136,12 @@ async def list_suggestions(
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()
@@ -137,9 +163,10 @@ async def list_suggestions(
# -----------------------------------------------------------------------------
# GET /api/admin/knowledge-iteration/suggestions/{id}
@router.get("/suggestions/{suggestion_id}")
@require_admin
async def get_suggestion(
suggestion_id: str,
current_user: User = Depends(require_admin),
current_user: UserInfo = Depends(get_current_user),
db: AsyncSession = Depends(get_db),
):
"""获取知识库优化建议详情。
@@ -167,39 +194,47 @@ async def get_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: User = Depends(require_admin),
current_user: UserInfo = Depends(get_current_user),
db: AsyncSession = Depends(get_db),
service: KnowledgeIterationService = Depends(dep_knowledge_iteration_service),
):
"""审核通过知识库优化建议。
"""审核通过知识库优化建议Tier1:串联 Neo4j 写图 + 五态流转)
审核通过后,如果是新FAQ或更新建议,将自动添加到知识库。
审核通过后
1. 状态 pending/queued → approved → applied → graph_synced
2. 自动创建 KnowledgeBase 条目(派生视图)
3. 触发 Neo4j 图写入(D1 解读2 合一)
- **suggestion_id**: 建议ID
**需要管理员权限。**
"""
logger.info(
f"管理员 {current_user.username} 审核通过建议: {suggestion_id}"
f"管理员 {current_user.name} 审核通过建议: {suggestion_id}"
)
# 尝试获取 Neo4j 客户端(可选,不影响审批主流程)
neo4j_client = await get_neo4j_client()
suggestion = await service.approve_suggestion(
db, suggestion_id, current_user.id
db, suggestion_id, current_user.employee_id,
neo4j_client=neo4j_client,
)
if not suggestion:
return {"code": 404, "message": "建议不存在", "data": None}
return {"code": 404, "message": "建议不存在或状态转换无效", "data": None}
return {
"code": 0,
"message": "审核通过,建议已应用到知识库",
"message": "审核通过,建议已应用到知识库并同步至知识图谱",
"data": KnowledgeSuggestionResponse.model_validate(suggestion),
}
@@ -209,10 +244,11 @@ async def approve_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: User = Depends(require_admin),
current_user: UserInfo = Depends(get_current_user),
db: AsyncSession = Depends(get_db),
service: KnowledgeIterationService = Depends(dep_knowledge_iteration_service),
):
@@ -223,12 +259,12 @@ async def reject_suggestion(
**需要管理员权限。**
"""
logger.info(
f"管理员 {current_user.username} 拒绝建议: {suggestion_id}, "
f"管理员 {current_user.name} 拒绝建议: {suggestion_id}, "
f"理由: {body.reject_reason}"
)
suggestion = await service.reject_suggestion(
db, suggestion_id, current_user.id, body.reject_reason
db, suggestion_id, current_user.employee_id, body.reject_reason
)
if not suggestion:
@@ -241,19 +277,141 @@ async def reject_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: User = Depends(require_admin),
current_user: UserInfo = Depends(get_current_user),
db: AsyncSession = Depends(get_db),
service: KnowledgeIterationService = Depends(dep_knowledge_iteration_service),
):
"""获取知识库优化建议统计。
返回各状态的建议数量统计。
返回各状态的建议数量统计(含 queued/graph_synced/expired
**需要管理员权限。**
"""
@@ -264,3 +422,146 @@ async def get_stats(
"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),
}