Files
wecom_it_smart_desk/backend/app/api/ragflow_ingestion.py
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

217 lines
7.6 KiB
Python
Raw Permalink 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智能服务台 — RAGFlow 文档摄入 APITier1 新增 / 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,
}