feat(evaluation): 支持评估基准下载
This commit is contained in:
parent
5348106513
commit
360316963e
@ -1,6 +1,7 @@
|
||||
import traceback
|
||||
|
||||
from fastapi import APIRouter, HTTPException, Depends, File, Form, Body, UploadFile
|
||||
from fastapi.responses import FileResponse
|
||||
from src.storage.postgres.models_business import User
|
||||
from server.utils.auth_middleware import get_admin_user
|
||||
from src.utils import logger
|
||||
@ -53,6 +54,29 @@ async def delete_evaluation_benchmark(benchmark_id: str, current_user: User = De
|
||||
raise HTTPException(status_code=500, detail=f"删除评估基准失败: {str(e)}")
|
||||
|
||||
|
||||
@evaluation.get("/benchmarks/{benchmark_id}/download")
|
||||
async def download_evaluation_benchmark(benchmark_id: str, current_user: User = Depends(get_admin_user)):
|
||||
"""下载评估基准文件"""
|
||||
from src.services.evaluation_service import EvaluationService
|
||||
|
||||
try:
|
||||
service = EvaluationService()
|
||||
download_info = await service.get_benchmark_download_info(benchmark_id)
|
||||
return FileResponse(
|
||||
path=download_info["file_path"],
|
||||
filename=download_info["filename"],
|
||||
media_type="application/x-ndjson",
|
||||
)
|
||||
except ValueError as e:
|
||||
if "not found" in str(e).lower():
|
||||
raise HTTPException(status_code=404, detail=str(e))
|
||||
logger.error(f"下载评估基准失败: {e}, {traceback.format_exc()}")
|
||||
raise HTTPException(status_code=500, detail=f"下载评估基准失败: {str(e)}")
|
||||
except Exception as e:
|
||||
logger.error(f"下载评估基准失败: {e}, {traceback.format_exc()}")
|
||||
raise HTTPException(status_code=500, detail=f"下载评估基准失败: {str(e)}")
|
||||
|
||||
|
||||
@evaluation.get("/databases/{db_id}/results/{task_id}")
|
||||
async def get_evaluation_results_by_db(
|
||||
db_id: str,
|
||||
|
||||
@ -231,6 +231,29 @@ class EvaluationService:
|
||||
logger.error(f"获取评估基准详情失败: {e}")
|
||||
raise
|
||||
|
||||
async def get_benchmark_download_info(self, benchmark_id: str) -> dict[str, str]:
|
||||
"""获取评估基准下载信息"""
|
||||
row = await self.eval_repo.get_benchmark(benchmark_id)
|
||||
if row is None:
|
||||
raise ValueError("Benchmark not found")
|
||||
|
||||
data_file_path = row.data_file_path or ""
|
||||
if not data_file_path or not os.path.exists(data_file_path):
|
||||
raise ValueError("Benchmark file not found")
|
||||
|
||||
filename_base = (row.name or "").strip()
|
||||
if not filename_base:
|
||||
filename_base = row.benchmark_id
|
||||
|
||||
filename_base = re.sub(r"[\\/:*?\"<>|]+", "_", filename_base).strip()
|
||||
if not filename_base or filename_base in {".", ".."}:
|
||||
filename_base = row.benchmark_id
|
||||
|
||||
if not filename_base.endswith(".jsonl"):
|
||||
filename_base = f"{filename_base}.jsonl"
|
||||
|
||||
return {"file_path": data_file_path, "filename": filename_base}
|
||||
|
||||
async def delete_benchmark(self, benchmark_id: str) -> None:
|
||||
"""删除评估基准"""
|
||||
try:
|
||||
|
||||
62
test/api/test_evaluation_router.py
Normal file
62
test/api/test_evaluation_router.py
Normal file
@ -0,0 +1,62 @@
|
||||
"""
|
||||
Integration tests for evaluation router endpoints.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import uuid
|
||||
|
||||
import pytest
|
||||
|
||||
pytestmark = [pytest.mark.asyncio, pytest.mark.integration]
|
||||
|
||||
|
||||
async def _upload_test_benchmark(test_client, admin_headers: dict[str, str], db_id: str) -> tuple[str, str]:
|
||||
benchmark_name = f"pytest_benchmark_{uuid.uuid4().hex[:8]}"
|
||||
line = '{"query":"什么是单元测试?","gold_answer":"用于验证代码行为的自动化测试"}\n'
|
||||
|
||||
response = await test_client.post(
|
||||
f"/api/evaluation/databases/{db_id}/benchmarks/upload",
|
||||
data={"name": benchmark_name, "description": "pytest benchmark for download"},
|
||||
files={"file": ("pytest_benchmark.jsonl", line.encode("utf-8"), "application/x-ndjson")},
|
||||
headers=admin_headers,
|
||||
)
|
||||
assert response.status_code == 200, response.text
|
||||
|
||||
payload = response.json()
|
||||
assert payload.get("message") == "success"
|
||||
benchmark_id = payload.get("data", {}).get("benchmark_id")
|
||||
assert benchmark_id
|
||||
return benchmark_id, line
|
||||
|
||||
|
||||
async def test_download_benchmark_requires_admin(test_client, standard_user):
|
||||
response = await test_client.get(
|
||||
"/api/evaluation/benchmarks/benchmark_fake/download",
|
||||
headers=standard_user["headers"],
|
||||
)
|
||||
assert response.status_code == 403
|
||||
|
||||
|
||||
async def test_admin_can_download_benchmark(test_client, admin_headers, knowledge_database):
|
||||
benchmark_id, expected_line = await _upload_test_benchmark(test_client, admin_headers, knowledge_database["db_id"])
|
||||
|
||||
response = await test_client.get(
|
||||
f"/api/evaluation/benchmarks/{benchmark_id}/download",
|
||||
headers=admin_headers,
|
||||
)
|
||||
assert response.status_code == 200, response.text
|
||||
assert "application/x-ndjson" in response.headers.get("content-type", "")
|
||||
assert "attachment" in response.headers.get("content-disposition", "").lower()
|
||||
|
||||
content = response.content.decode("utf-8")
|
||||
assert expected_line.strip() in content
|
||||
|
||||
|
||||
async def test_download_benchmark_not_found(test_client, admin_headers):
|
||||
response = await test_client.get(
|
||||
f"/api/evaluation/benchmarks/benchmark_not_found_{uuid.uuid4().hex[:8]}/download",
|
||||
headers=admin_headers,
|
||||
)
|
||||
assert response.status_code == 404, response.text
|
||||
|
||||
@ -453,6 +453,15 @@ export const evaluationApi = {
|
||||
return apiAdminDelete(`/api/evaluation/benchmarks/${benchmarkId}`)
|
||||
},
|
||||
|
||||
/**
|
||||
* 下载评估基准
|
||||
* @param {string} benchmarkId - 基准ID
|
||||
* @returns {Promise} - Response对象
|
||||
*/
|
||||
downloadBenchmark: async (benchmarkId) => {
|
||||
return apiAdminGet(`/api/evaluation/benchmarks/${benchmarkId}/download`, {}, 'blob')
|
||||
},
|
||||
|
||||
/**
|
||||
* 自动生成评估基准
|
||||
* @param {string} dbId - 知识库ID
|
||||
|
||||
@ -48,6 +48,14 @@
|
||||
<a-button type="text" size="small" @click.stop="previewBenchmark(benchmark)">
|
||||
<EyeOutlined />
|
||||
</a-button>
|
||||
<a-button
|
||||
type="text"
|
||||
size="small"
|
||||
:loading="!!downloadingBenchmarkMap[benchmark.benchmark_id]"
|
||||
@click.stop="downloadBenchmark(benchmark)"
|
||||
>
|
||||
<DownloadOutlined />
|
||||
</a-button>
|
||||
<a-button type="text" size="small" danger @click.stop="deleteBenchmark(benchmark)">
|
||||
<DeleteOutlined />
|
||||
</a-button>
|
||||
@ -196,6 +204,7 @@ import {
|
||||
UploadOutlined,
|
||||
RobotOutlined,
|
||||
EyeOutlined,
|
||||
DownloadOutlined,
|
||||
DeleteOutlined,
|
||||
CheckCircleOutlined,
|
||||
CloseCircleOutlined,
|
||||
@ -225,6 +234,7 @@ const generateModalVisible = ref(false)
|
||||
const previewModalVisible = ref(false)
|
||||
const previewData = ref(null)
|
||||
const previewQuestions = ref([])
|
||||
const downloadingBenchmarkMap = reactive({})
|
||||
const previewPagination = ref({
|
||||
current: 1,
|
||||
pageSize: 10,
|
||||
@ -407,6 +417,60 @@ const previewBenchmark = async (benchmark) => {
|
||||
}
|
||||
}
|
||||
|
||||
const parseDownloadFilename = (contentDisposition) => {
|
||||
if (!contentDisposition) return ''
|
||||
|
||||
const utf8Match = contentDisposition.match(/filename\*=UTF-8''([^;]+)/i)
|
||||
if (utf8Match && utf8Match[1]) {
|
||||
try {
|
||||
return decodeURIComponent(utf8Match[1])
|
||||
} catch (error) {
|
||||
console.warn('解析 UTF-8 文件名失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
const asciiMatch = contentDisposition.match(/filename=\"?([^\";]+)\"?/i)
|
||||
if (asciiMatch && asciiMatch[1]) {
|
||||
return asciiMatch[1]
|
||||
}
|
||||
|
||||
return ''
|
||||
}
|
||||
|
||||
// 下载基准
|
||||
const downloadBenchmark = async (benchmark) => {
|
||||
const benchmarkId = benchmark?.benchmark_id
|
||||
if (!benchmarkId) return
|
||||
if (downloadingBenchmarkMap[benchmarkId]) return
|
||||
|
||||
downloadingBenchmarkMap[benchmarkId] = true
|
||||
try {
|
||||
const response = await evaluationApi.downloadBenchmark(benchmarkId)
|
||||
const blob = await response.blob()
|
||||
const contentDisposition =
|
||||
response.headers.get('Content-Disposition') || response.headers.get('content-disposition')
|
||||
const headerFilename = parseDownloadFilename(contentDisposition)
|
||||
const fallbackFilename = `${benchmark.name || benchmarkId}.jsonl`
|
||||
const filename = headerFilename || fallbackFilename
|
||||
|
||||
const url = window.URL.createObjectURL(blob)
|
||||
const link = document.createElement('a')
|
||||
link.href = url
|
||||
link.download = filename
|
||||
document.body.appendChild(link)
|
||||
link.click()
|
||||
document.body.removeChild(link)
|
||||
window.URL.revokeObjectURL(url)
|
||||
|
||||
message.success('下载成功')
|
||||
} catch (error) {
|
||||
console.error('下载基准失败:', error)
|
||||
message.error(`下载失败: ${error.message || '未知错误'}`)
|
||||
} finally {
|
||||
delete downloadingBenchmarkMap[benchmarkId]
|
||||
}
|
||||
}
|
||||
|
||||
// 删除基准
|
||||
const deleteBenchmark = (benchmark) => {
|
||||
Modal.confirm({
|
||||
|
||||
Loading…
Reference in New Issue
Block a user