ead5f83bee
dependencies.py 拆分为 dependencies/ 包; 新增 vision/ragflow_ingestion/neo4j 客户端与 h5_ai_task; alembic 045 图置信度迁移; 响应契约统一收尾。
217 lines
7.6 KiB
Python
217 lines
7.6 KiB
Python
# =============================================================================
|
||
# 企微IT智能服务台 — RAGFlow 文档摄入 API(Tier1 新增 / P1-5 / 通道 C)
|
||
# =============================================================================
|
||
# 说明:RAGFlow 文档摄入接口,训练师上传非标准格式文档,
|
||
# 经 RAGFlow ETL 整理/结构化后生成 KnowledgeSuggestion 进审批队列。
|
||
#
|
||
# 1. POST /api/ragflow/ingest — 上传文档触发 RAGFlow 处理
|
||
# 2. GET /api/ragflow/tasks/{task_id} — 查询处理任务状态
|
||
#
|
||
# P1-5 硬约束:
|
||
# - 触发方式:训练师手动上传(非定时扫描)
|
||
# - 支持格式:.docx/.pdf/.txt/.png/.jpg
|
||
# - source_type=document_ragflow, audience=engineer_workguide
|
||
# - 产出走 D7 审批流
|
||
# =============================================================================
|
||
|
||
import logging
|
||
import uuid
|
||
from typing import Optional
|
||
|
||
from fastapi import APIRouter, Depends, File, Form, Query, UploadFile
|
||
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.enums import (
|
||
AudienceEnum,
|
||
GraphSyncStatusEnum,
|
||
SourceTypeEnum,
|
||
SuggestionStatusEnum,
|
||
)
|
||
from app.services.ragflow_ingestion_service import RagflowIngestionService
|
||
|
||
logger = logging.getLogger(__name__)
|
||
|
||
router = APIRouter()
|
||
|
||
# 支持的文件格式
|
||
ALLOWED_EXTENSIONS = {".docx", ".pdf", ".txt", ".png", ".jpg", ".jpeg"}
|
||
ALLOWED_MIME_TYPES = {
|
||
"application/vnd.openxmlformats-officedocument.wordprocessingml.document", # .docx
|
||
"application/pdf", # .pdf
|
||
"text/plain", # .txt
|
||
"image/png", # .png
|
||
"image/jpeg", # .jpg/.jpeg
|
||
}
|
||
|
||
# 文件大小上限(20MB)
|
||
MAX_FILE_SIZE = 20 * 1024 * 1024
|
||
|
||
# 内存中的任务状态缓存(生产环境应迁移到 Redis)
|
||
_task_cache: dict = {}
|
||
|
||
|
||
# -----------------------------------------------------------------------------
|
||
# 上传文档触发 RAGFlow 处理(Tier1 新增)
|
||
# -----------------------------------------------------------------------------
|
||
# POST /api/ragflow/ingest
|
||
@router.post("/ingest")
|
||
@require_admin
|
||
async def ingest_document(
|
||
file: UploadFile = File(..., description="文档文件(.docx/.pdf/.txt/.png/.jpg)"),
|
||
category_hint: str = Form(
|
||
default="其他",
|
||
description="分类提示(可选,帮助RAGFlow归类):硬件/软件/网络/安全/账号/其他",
|
||
),
|
||
current_user: UserInfo = Depends(get_current_user),
|
||
db: AsyncSession = Depends(get_db),
|
||
):
|
||
"""上传非标准格式文档到 RAGFlow 进行 ETL 处理。
|
||
|
||
训练师上传文档后,RAGFlow 自动整理/筛选/结构化内容,
|
||
生成 KnowledgeSuggestion 提案进入 D7 审批队列。
|
||
|
||
**请求格式**: multipart/form-data
|
||
|
||
**字段说明**:
|
||
- **file**: 文档文件(必填,支持 .docx/.pdf/.txt/.png/.jpg)
|
||
- **category_hint**: 分类提示(可选,默认"其他")
|
||
|
||
**文件大小限制**: 最大 20MB
|
||
**处理时间**: 最长等待 5 分钟,超时返回 pending 状态
|
||
|
||
**需要管理员权限。**
|
||
"""
|
||
# 校验文件扩展名
|
||
file_name = file.filename or "unknown"
|
||
ext = "." + file_name.rsplit(".", 1)[-1].lower() if "." in file_name else ""
|
||
if ext not in ALLOWED_EXTENSIONS:
|
||
return {
|
||
"code": 400,
|
||
"message": f"不支持的文件格式: {ext},仅支持 {', '.join(ALLOWED_EXTENSIONS)}",
|
||
"data": None,
|
||
}
|
||
|
||
# 校验 MIME 类型(如可获取)
|
||
if file.content_type and file.content_type not in ALLOWED_MIME_TYPES:
|
||
logger.warning(
|
||
f"文件 MIME 类型不在白名单中: {file.content_type},仍允许上传"
|
||
)
|
||
|
||
# 读取文件内容
|
||
file_data = await file.read()
|
||
|
||
# 校验文件大小
|
||
if len(file_data) > MAX_FILE_SIZE:
|
||
return {
|
||
"code": 400,
|
||
"message": f"文件过大({len(file_data) / 1024 / 1024:.1f}MB),最大支持 20MB",
|
||
"data": None,
|
||
}
|
||
|
||
if len(file_data) == 0:
|
||
return {
|
||
"code": 400,
|
||
"message": "文件内容为空",
|
||
"data": None,
|
||
}
|
||
|
||
# 调用 RAGFlow Ingestion 服务
|
||
service = RagflowIngestionService()
|
||
|
||
logger.info(
|
||
f"管理员 {current_user.name} 上传文档到 RAGFlow: "
|
||
f"file_name={file_name}, category_hint={category_hint}, size={len(file_data)}"
|
||
)
|
||
|
||
result = await service.upload_and_process(file_data, file_name, category_hint)
|
||
|
||
# 将生成的 suggestions 写入数据库(pending 状态)
|
||
saved_suggestions = []
|
||
if result.get("suggestions"):
|
||
for sug_data in result["suggestions"]:
|
||
suggestion = KnowledgeSuggestion(
|
||
suggestion_type=sug_data.get("suggestion_type", "new_faq"),
|
||
status=SuggestionStatusEnum.pending.value,
|
||
title=sug_data.get("title", ""),
|
||
content=sug_data.get("content", ""),
|
||
category=sug_data.get("category", category_hint),
|
||
tags=sug_data.get("tags", []),
|
||
source_type=SourceTypeEnum.document_ragflow.value,
|
||
source_data=sug_data.get("source_data", []),
|
||
reason=sug_data.get("reason", ""),
|
||
confidence=sug_data.get("confidence", 0.85),
|
||
audience=AudienceEnum.engineer_workguide.value, # 通道 C 默认
|
||
issue=sug_data.get("issue", ""),
|
||
action=sug_data.get("action", ""),
|
||
relation_type=sug_data.get("relation_type", "LEADS_TO"),
|
||
parent_issue=sug_data.get("parent_issue", ""),
|
||
graph_meta=sug_data.get("graph_meta", {}),
|
||
graph_sync_status=GraphSyncStatusEnum.pending.value,
|
||
source_failed=sug_data.get("source_failed", False),
|
||
)
|
||
db.add(suggestion)
|
||
saved_suggestions.append({
|
||
"title": suggestion.title,
|
||
"category": suggestion.category,
|
||
"confidence": suggestion.confidence,
|
||
})
|
||
|
||
await db.commit()
|
||
logger.info(f"RAGFlow 生成 {len(saved_suggestions)} 条 KnowledgeSuggestion 待审批")
|
||
|
||
# 缓存任务状态
|
||
task_id = result["task_id"]
|
||
_task_cache[task_id] = {
|
||
"task_id": task_id,
|
||
"status": result["status"],
|
||
"file_name": file_name,
|
||
"created_at": __import__("datetime").datetime.now().isoformat(),
|
||
"suggestions_count": len(saved_suggestions),
|
||
}
|
||
|
||
return {
|
||
"code": 0,
|
||
"message": "文档已提交 RAGFlow 处理",
|
||
"data": {
|
||
"task_id": task_id,
|
||
"status": result["status"],
|
||
"file_name": file_name,
|
||
"suggestions_count": len(saved_suggestions),
|
||
"suggestions": saved_suggestions,
|
||
},
|
||
}
|
||
|
||
|
||
# -----------------------------------------------------------------------------
|
||
# 查询处理任务状态(Tier1 新增)
|
||
# -----------------------------------------------------------------------------
|
||
# GET /api/ragflow/tasks/{task_id}
|
||
@router.get("/tasks/{task_id}")
|
||
@require_admin
|
||
async def get_ingestion_task_status(
|
||
task_id: str,
|
||
current_user: UserInfo = Depends(get_current_user),
|
||
):
|
||
"""查询 RAGFlow 文档处理任务状态。
|
||
|
||
- **task_id**: 任务ID(来自 ingest 接口返回值)
|
||
|
||
**需要管理员权限。**
|
||
"""
|
||
task = _task_cache.get(task_id)
|
||
if not task:
|
||
return {
|
||
"code": 404,
|
||
"message": "任务不存在或已过期",
|
||
"data": None,
|
||
}
|
||
|
||
return {
|
||
"code": 0,
|
||
"message": "success",
|
||
"data": task,
|
||
}
|