Files
rag/exam_pkg/api.py
lacerate551 100d1a06eb init: RAG 知识库服务初始提交
- 后端 API(Flask + Gunicorn)
- RAG 引擎(混合检索 + 云端 Reranker + 引用溯源)
- 文档解析(MinerU + 多格式支持)
- Docker 生产部署配置
- 排除前端项目、敏感配置、模型文件
2026-06-04 17:35:27 +08:00

275 lines
8.3 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.
"""
出题与批题系统 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_typesAI 自动分析文档后决定
请求体:
{
"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"
})