init: RAG 知识库服务初始提交
- 后端 API(Flask + Gunicorn) - RAG 引擎(混合检索 + 云端 Reranker + 引用溯源) - 文档解析(MinerU + 多格式支持) - Docker 生产部署配置 - 排除前端项目、敏感配置、模型文件
This commit is contained in:
274
exam_pkg/api.py
Normal file
274
exam_pkg/api.py
Normal file
@@ -0,0 +1,274 @@
|
||||
"""
|
||||
出题与批题系统 API 蓝图
|
||||
|
||||
提供 REST API 接口:
|
||||
- 出题:生成题目(返回 JSON 给后端)
|
||||
- 批题:批阅答案(返回结果给后端)
|
||||
|
||||
职责边界:
|
||||
- RAG 服务负责:生成题目 + 批阅答案
|
||||
- 后端服务负责:审核入库 + 状态管理
|
||||
|
||||
使用方式:
|
||||
from exam_pkg.api import exam_bp
|
||||
app.register_blueprint(exam_bp, url_prefix='/exam')
|
||||
"""
|
||||
|
||||
from flask import Blueprint, request, jsonify
|
||||
import os
|
||||
|
||||
# 导入考试管理模块
|
||||
from exam_pkg.manager import (
|
||||
generate_questions_from_file,
|
||||
grade_answers,
|
||||
)
|
||||
|
||||
# 导入网关认证模块
|
||||
from auth.gateway import (
|
||||
require_gateway_auth, require_role, check_collection_permission, get_current_user
|
||||
)
|
||||
|
||||
# 导入统一响应格式
|
||||
from core.status_codes import EXAM_SUCCESS, GRADE_SUCCESS, EXAM_ERROR, GRADE_ERROR, BAD_REQUEST, NO_CONTENT, LLM_ERROR
|
||||
from api.response_utils import success_response, error_response
|
||||
|
||||
# 创建蓝图
|
||||
exam_bp = Blueprint('exam', __name__)
|
||||
|
||||
|
||||
# ==================== 出题 API ====================
|
||||
|
||||
@exam_bp.route('/generate', methods=['POST'])
|
||||
@require_gateway_auth
|
||||
def api_generate_questions():
|
||||
"""
|
||||
🔥 新版出题接口
|
||||
|
||||
请求体:
|
||||
{
|
||||
"request_id": "uuid-optional", // 幂等性支持
|
||||
"file_path": "public/产品手册.pdf",
|
||||
"collection": "public_kb",
|
||||
"question_types": {
|
||||
"single_choice": 3,
|
||||
"multiple_choice": 2,
|
||||
"true_false": 2,
|
||||
"fill_blank": 2,
|
||||
"subjective": 1
|
||||
},
|
||||
"difficulty": 3,
|
||||
"options": {
|
||||
"include_explanation": true,
|
||||
"max_source_chunks": 50
|
||||
}
|
||||
}
|
||||
|
||||
返回:
|
||||
{
|
||||
"success": true,
|
||||
"request_id": "uuid",
|
||||
"questions": [
|
||||
{
|
||||
"metadata": {"question_id": "...", "question_type": "...", ...},
|
||||
"source_trace": {"document_name": "...", "sources": [...], ...},
|
||||
"content": {"stem": "...", "data": {...}, "answer": "...", ...}
|
||||
}
|
||||
],
|
||||
"total": 10,
|
||||
"source_chunks_used": 15
|
||||
}
|
||||
"""
|
||||
try:
|
||||
data = request.json
|
||||
|
||||
file_path = data.get('file_path')
|
||||
collection = data.get('collection')
|
||||
question_types = data.get('question_types', {})
|
||||
|
||||
if not file_path or not collection:
|
||||
return error_response("MISSING_PARAMS", BAD_REQUEST, "缺少 file_path 或 collection 参数", http_status=400)
|
||||
|
||||
if not question_types:
|
||||
return error_response("MISSING_PARAMS", BAD_REQUEST, "缺少 question_types 参数", http_status=400)
|
||||
|
||||
# 获取当前用户
|
||||
user = get_current_user()
|
||||
if not user:
|
||||
return error_response("UNAUTHORIZED", BAD_REQUEST, "未认证", http_status=401)
|
||||
|
||||
# 检查向量库访问权限
|
||||
if not check_collection_permission(
|
||||
role=user['role'],
|
||||
department=user.get('department', ''),
|
||||
collection_name=collection,
|
||||
operation="read"
|
||||
):
|
||||
return error_response("FORBIDDEN", BAD_REQUEST, "权限不足", http_status=403)
|
||||
|
||||
# 调用新版出题接口
|
||||
result = generate_questions_from_file(
|
||||
file_path=file_path,
|
||||
collection=collection,
|
||||
question_types=question_types,
|
||||
difficulty=data.get('difficulty', 3),
|
||||
options=data.get('options', {}),
|
||||
request_id=data.get('request_id')
|
||||
)
|
||||
|
||||
return success_response(data=result, status_code=EXAM_SUCCESS, message="出题成功")
|
||||
|
||||
except Exception as e:
|
||||
return error_response("EXAM_ERROR", EXAM_ERROR, str(e), http_status=500)
|
||||
|
||||
|
||||
@exam_bp.route('/generate-smart', methods=['POST'])
|
||||
@require_gateway_auth
|
||||
def api_generate_smart():
|
||||
"""
|
||||
AI 智能出题 - 自动分析文件并决定题型和数量
|
||||
|
||||
与 /exam/generate 的区别:不需要传 question_types,AI 自动分析文档后决定
|
||||
|
||||
请求体:
|
||||
{
|
||||
"file_path": "public/产品手册.pdf",
|
||||
"collection": "public_kb",
|
||||
"difficulty": 3, // 可选,默认 3
|
||||
"options": {} // 可选
|
||||
}
|
||||
|
||||
返回:
|
||||
与 /exam/generate 相同格式,额外包含 ai_analysis 字段
|
||||
"""
|
||||
try:
|
||||
data = request.json
|
||||
|
||||
file_path = data.get('file_path')
|
||||
collection = data.get('collection')
|
||||
|
||||
if not file_path or not collection:
|
||||
return error_response("MISSING_PARAMS", BAD_REQUEST, "缺少 file_path 或 collection 参数", http_status=400)
|
||||
|
||||
# 获取当前用户
|
||||
user = get_current_user()
|
||||
if not user:
|
||||
return error_response("UNAUTHORIZED", BAD_REQUEST, "未认证", http_status=401)
|
||||
|
||||
# 检查向量库访问权限
|
||||
if not check_collection_permission(
|
||||
role=user['role'],
|
||||
department=user.get('department', ''),
|
||||
collection_name=collection,
|
||||
operation="read"
|
||||
):
|
||||
return error_response("FORBIDDEN", BAD_REQUEST, "权限不足", http_status=403)
|
||||
|
||||
# 1. 调用 AI 分析文件,获取推荐的题型和数量
|
||||
from exam_pkg.manager import analyze_file_for_exam
|
||||
ai_analysis = analyze_file_for_exam(
|
||||
file_path=file_path,
|
||||
collection=collection
|
||||
)
|
||||
|
||||
question_types = ai_analysis.get('question_types', {})
|
||||
if not question_types or sum(question_types.values()) == 0:
|
||||
return error_response("EXAM_ERROR", EXAM_ERROR, "AI 分析后未生成有效题型配置", http_status=500)
|
||||
|
||||
# 2. 使用 AI 推荐的题型调用出题接口
|
||||
result = generate_questions_from_file(
|
||||
file_path=file_path,
|
||||
collection=collection,
|
||||
question_types=question_types,
|
||||
difficulty=data.get('difficulty', 3),
|
||||
options=data.get('options', {}),
|
||||
request_id=data.get('request_id')
|
||||
)
|
||||
|
||||
# 3. 在返回结果中添加 AI 分析信息
|
||||
result['ai_analysis'] = ai_analysis
|
||||
|
||||
return success_response(data=result, status_code=EXAM_SUCCESS, message="AI 智能出题成功")
|
||||
|
||||
except Exception as e:
|
||||
return error_response("EXAM_ERROR", EXAM_ERROR, str(e), http_status=500)
|
||||
|
||||
|
||||
# ==================== 批题 API ====================
|
||||
|
||||
@exam_bp.route('/grade', methods=['POST'])
|
||||
@require_gateway_auth
|
||||
def api_grade_answers():
|
||||
"""
|
||||
🔥 新版批题接口
|
||||
|
||||
请求体:
|
||||
{
|
||||
"request_id": "uuid-optional",
|
||||
"answers": [
|
||||
{
|
||||
"question_id": "uuid",
|
||||
"question_type": "single_choice",
|
||||
"question_content": {"answer": "B"},
|
||||
"student_answer": "A",
|
||||
"max_score": 2
|
||||
},
|
||||
{
|
||||
"question_id": "uuid",
|
||||
"question_type": "fill_blank",
|
||||
"question_content": {"answer": [["答案1"], ["答案2"]]},
|
||||
"student_answer": ["学生答案1", "学生答案2"],
|
||||
"max_score": 4
|
||||
},
|
||||
{
|
||||
"question_id": "uuid",
|
||||
"question_type": "subjective",
|
||||
"question_content": {
|
||||
"stem": "简述...",
|
||||
"data": {"scoring_points": [...]},
|
||||
"answer": "参考范文..."
|
||||
},
|
||||
"student_answer": "学生作答内容...",
|
||||
"max_score": 10
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
返回:
|
||||
{
|
||||
"success": true,
|
||||
"request_id": "uuid",
|
||||
"results": [...],
|
||||
"total_score": 10.5,
|
||||
"total_max_score": 16.0,
|
||||
"score_rate": 65.6
|
||||
}
|
||||
"""
|
||||
try:
|
||||
data = request.json
|
||||
|
||||
answers = data.get('answers', [])
|
||||
if not answers:
|
||||
return error_response("MISSING_PARAMS", BAD_REQUEST, "缺少答案数据", http_status=400)
|
||||
|
||||
# 调用新版批题接口
|
||||
result = grade_answers(
|
||||
answers=answers,
|
||||
request_id=data.get('request_id')
|
||||
)
|
||||
|
||||
return success_response(data=result, status_code=GRADE_SUCCESS, message="批阅完成")
|
||||
|
||||
except Exception as e:
|
||||
return error_response("GRADE_ERROR", GRADE_ERROR, str(e), http_status=500)
|
||||
|
||||
|
||||
# ==================== 健康检查 ====================
|
||||
|
||||
@exam_bp.route('/health', methods=['GET'])
|
||||
def api_health():
|
||||
"""健康检查"""
|
||||
return jsonify({
|
||||
"status": "ok",
|
||||
"service": "exam-api",
|
||||
"version": "2.0"
|
||||
})
|
||||
Reference in New Issue
Block a user