feat(evaluation): 支持评估基准下载
This commit is contained in:
parent
5348106513
commit
360316963e
@ -1,6 +1,7 @@
|
|||||||
import traceback
|
import traceback
|
||||||
|
|
||||||
from fastapi import APIRouter, HTTPException, Depends, File, Form, Body, UploadFile
|
from fastapi import APIRouter, HTTPException, Depends, File, Form, Body, UploadFile
|
||||||
|
from fastapi.responses import FileResponse
|
||||||
from src.storage.postgres.models_business import User
|
from src.storage.postgres.models_business import User
|
||||||
from server.utils.auth_middleware import get_admin_user
|
from server.utils.auth_middleware import get_admin_user
|
||||||
from src.utils import logger
|
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)}")
|
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}")
|
@evaluation.get("/databases/{db_id}/results/{task_id}")
|
||||||
async def get_evaluation_results_by_db(
|
async def get_evaluation_results_by_db(
|
||||||
db_id: str,
|
db_id: str,
|
||||||
|
|||||||
@ -231,6 +231,29 @@ class EvaluationService:
|
|||||||
logger.error(f"获取评估基准详情失败: {e}")
|
logger.error(f"获取评估基准详情失败: {e}")
|
||||||
raise
|
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:
|
async def delete_benchmark(self, benchmark_id: str) -> None:
|
||||||
"""删除评估基准"""
|
"""删除评估基准"""
|
||||||
try:
|
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}`)
|
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
|
* @param {string} dbId - 知识库ID
|
||||||
|
|||||||
@ -48,6 +48,14 @@
|
|||||||
<a-button type="text" size="small" @click.stop="previewBenchmark(benchmark)">
|
<a-button type="text" size="small" @click.stop="previewBenchmark(benchmark)">
|
||||||
<EyeOutlined />
|
<EyeOutlined />
|
||||||
</a-button>
|
</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)">
|
<a-button type="text" size="small" danger @click.stop="deleteBenchmark(benchmark)">
|
||||||
<DeleteOutlined />
|
<DeleteOutlined />
|
||||||
</a-button>
|
</a-button>
|
||||||
@ -196,6 +204,7 @@ import {
|
|||||||
UploadOutlined,
|
UploadOutlined,
|
||||||
RobotOutlined,
|
RobotOutlined,
|
||||||
EyeOutlined,
|
EyeOutlined,
|
||||||
|
DownloadOutlined,
|
||||||
DeleteOutlined,
|
DeleteOutlined,
|
||||||
CheckCircleOutlined,
|
CheckCircleOutlined,
|
||||||
CloseCircleOutlined,
|
CloseCircleOutlined,
|
||||||
@ -225,6 +234,7 @@ const generateModalVisible = ref(false)
|
|||||||
const previewModalVisible = ref(false)
|
const previewModalVisible = ref(false)
|
||||||
const previewData = ref(null)
|
const previewData = ref(null)
|
||||||
const previewQuestions = ref([])
|
const previewQuestions = ref([])
|
||||||
|
const downloadingBenchmarkMap = reactive({})
|
||||||
const previewPagination = ref({
|
const previewPagination = ref({
|
||||||
current: 1,
|
current: 1,
|
||||||
pageSize: 10,
|
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) => {
|
const deleteBenchmark = (benchmark) => {
|
||||||
Modal.confirm({
|
Modal.confirm({
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user