aeval-framework 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- aeval_framework-0.1.0.dist-info/METADATA +42 -0
- aeval_framework-0.1.0.dist-info/RECORD +63 -0
- aeval_framework-0.1.0.dist-info/WHEEL +4 -0
- aeval_framework-0.1.0.dist-info/entry_points.txt +2 -0
- agent_eval/__init__.py +14 -0
- agent_eval/api/__init__.py +14 -0
- agent_eval/api/app.py +82 -0
- agent_eval/api/events.py +96 -0
- agent_eval/api/routes/__init__.py +0 -0
- agent_eval/api/routes/datasets.py +441 -0
- agent_eval/api/routes/graders.py +19 -0
- agent_eval/api/routes/metrics.py +49 -0
- agent_eval/api/routes/runs.py +573 -0
- agent_eval/api/routes/suites.py +84 -0
- agent_eval/api/routes/tasks.py +114 -0
- agent_eval/api/standalone.py +105 -0
- agent_eval/cli.py +455 -0
- agent_eval/core/__init__.py +48 -0
- agent_eval/core/contract.py +296 -0
- agent_eval/core/metrics.py +184 -0
- agent_eval/core/runner.py +868 -0
- agent_eval/core/suite.py +60 -0
- agent_eval/core/types.py +227 -0
- agent_eval/dataset/__init__.py +31 -0
- agent_eval/dataset/models.py +199 -0
- agent_eval/dataset/quality.py +194 -0
- agent_eval/dataset/sources/__init__.py +45 -0
- agent_eval/dataset/sources/llm_generator.py +219 -0
- agent_eval/dataset/sources/manual.py +172 -0
- agent_eval/dataset/sources/regression.py +201 -0
- agent_eval/dataset/sources/trace_mining.py +277 -0
- agent_eval/dataset/storage.py +342 -0
- agent_eval/dataset/version.py +72 -0
- agent_eval/examples/__init__.py +0 -0
- agent_eval/examples/basic_usage.py +175 -0
- agent_eval/examples/mock_runner.py +195 -0
- agent_eval/graders/__init__.py +91 -0
- agent_eval/graders/artifact_check.py +114 -0
- agent_eval/graders/code_based.py +101 -0
- agent_eval/graders/human.py +77 -0
- agent_eval/graders/metric.py +142 -0
- agent_eval/graders/model_based.py +179 -0
- agent_eval/graders/state_check.py +106 -0
- agent_eval/graders/step_level.py +116 -0
- agent_eval/graders/tool_calls.py +102 -0
- agent_eval/graders/transcript.py +86 -0
- agent_eval/metrics/__init__.py +110 -0
- agent_eval/metrics/answer_relevancy.py +57 -0
- agent_eval/metrics/base.py +155 -0
- agent_eval/metrics/batch_evaluation.py +267 -0
- agent_eval/metrics/context_precision.py +62 -0
- agent_eval/metrics/context_recall.py +71 -0
- agent_eval/metrics/faithfulness.py +72 -0
- agent_eval/metrics/llm_judge.py +100 -0
- agent_eval/metrics/prompt_metric.py +150 -0
- agent_eval/metrics/pytest_plugin.py +308 -0
- agent_eval/metrics/report.py +149 -0
- agent_eval/metrics/synthetic_data.py +203 -0
- agent_eval/storage/__init__.py +17 -0
- agent_eval/storage/memory.py +95 -0
- agent_eval/storage/sqlite.py +240 -0
- agent_eval/trace/__init__.py +16 -0
- agent_eval/trace/phoenix.py +144 -0
|
@@ -0,0 +1,441 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Dataset management routes.
|
|
3
|
+
|
|
4
|
+
GET /datasets — List datasets (optional ?tags=a,b)
|
|
5
|
+
POST /datasets — Create a dataset (JSON body)
|
|
6
|
+
POST /datasets/import — Import a dataset from YAML/JSON content
|
|
7
|
+
GET /datasets/{ref} — Get dataset with items (id or name)
|
|
8
|
+
DELETE /datasets/{ref} — Delete dataset (cascades items)
|
|
9
|
+
GET /datasets/{ref}/items — List items
|
|
10
|
+
POST /datasets/{ref}/items — Add an item
|
|
11
|
+
PUT /datasets/{ref}/items/{item_id} — Update an item
|
|
12
|
+
DELETE /datasets/{ref}/items/{item_id} — Delete an item
|
|
13
|
+
POST /datasets/{ref}/from-trace — Mine traces into the dataset
|
|
14
|
+
POST /datasets/{ref}/from-llm — LLM-generate items into the dataset
|
|
15
|
+
POST /datasets/{ref}/regression-extract — Extract failed-trial samples from a run
|
|
16
|
+
GET /datasets/{ref}/quality-check — Quality report
|
|
17
|
+
GET /datasets/{ref}/coverage — Capability coverage report
|
|
18
|
+
POST /datasets/{ref}/to-suite — Convert to an executable EvalSuite
|
|
19
|
+
POST /datasets/{ref}/version — Bump semver version with a change note
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
from __future__ import annotations
|
|
23
|
+
|
|
24
|
+
import time
|
|
25
|
+
from typing import Any, Literal
|
|
26
|
+
|
|
27
|
+
from fastapi import APIRouter, HTTPException, Query
|
|
28
|
+
from pydantic import BaseModel, Field
|
|
29
|
+
|
|
30
|
+
from agent_eval.dataset.models import (
|
|
31
|
+
DatasetError,
|
|
32
|
+
EvalDataset,
|
|
33
|
+
EvalDatasetItem,
|
|
34
|
+
SourceType,
|
|
35
|
+
)
|
|
36
|
+
from agent_eval.dataset.quality import CoverageAnalyzer, DatasetQualityChecker
|
|
37
|
+
from agent_eval.dataset.sources.llm_generator import LLMDatasetGenerator
|
|
38
|
+
from agent_eval.dataset.sources.manual import import_from_content
|
|
39
|
+
from agent_eval.dataset.sources.regression import RegressionExtractor
|
|
40
|
+
from agent_eval.dataset.sources.trace_mining import TraceMiner
|
|
41
|
+
from agent_eval.dataset.version import DatasetVersionManager
|
|
42
|
+
|
|
43
|
+
router = APIRouter()
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
# ─── Request / response models ───────────────────────────────────────────────
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
class ImportRequest(BaseModel):
|
|
50
|
+
"""YAML/JSON 内容导入请求"""
|
|
51
|
+
|
|
52
|
+
content: str = Field(..., description="数据集定义 (YAML 或 JSON 文本)")
|
|
53
|
+
format: str = Field("yaml", description="内容格式: yaml | json")
|
|
54
|
+
source_type: str = Field("manual", description="条目默认来源: manual | adversarial")
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
class FromTraceRequest(BaseModel):
|
|
58
|
+
"""Trace 挖掘请求"""
|
|
59
|
+
|
|
60
|
+
strategy: str = Field("failed_tasks", description="failed_tasks | long_running | diverse_sampling")
|
|
61
|
+
filters: dict[str, Any] = Field(default_factory=dict, description="get_trace_ids 过滤条件")
|
|
62
|
+
limit: int = Field(20, ge=1, le=200, description="最多产出的条目数")
|
|
63
|
+
candidate_limit: int = Field(100, ge=1, le=1000, description="最多检查的候选 trace 数")
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
class FromLLMRequest(BaseModel):
|
|
67
|
+
"""LLM 生成请求"""
|
|
68
|
+
|
|
69
|
+
scenario: str = Field(..., min_length=1, description="场景描述")
|
|
70
|
+
capabilities: list[str] = Field(default_factory=list, description="能力维度标签")
|
|
71
|
+
count: int = Field(5, ge=1, le=50, description="请求生成的条目数")
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
class RegressionExtractRequest(BaseModel):
|
|
75
|
+
"""回归样本提取请求"""
|
|
76
|
+
|
|
77
|
+
run_id: str = Field(..., min_length=1, description="来源 run ID")
|
|
78
|
+
max_items: int = Field(50, ge=1, le=500, description="提取上限")
|
|
79
|
+
bump_version: Literal["major", "minor", "patch"] | None = Field(
|
|
80
|
+
None, description="合入非空时对数据集升版 (闭环惯例: minor)"
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
class ToSuiteRequest(BaseModel):
|
|
85
|
+
"""to-suite 转换请求"""
|
|
86
|
+
|
|
87
|
+
name: str | None = Field(None, description="生成的 suite 名称 (缺省用数据集名)")
|
|
88
|
+
save: bool = Field(True, description="是否保存到 suite 存储 (供 POST /runs 使用)")
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
class VersionBumpRequest(BaseModel):
|
|
92
|
+
"""版本升版请求"""
|
|
93
|
+
|
|
94
|
+
change_type: Literal["major", "minor", "patch"]
|
|
95
|
+
note: str = Field("", description="变更说明 (记入 change_log)")
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
# ─── Helpers ─────────────────────────────────────────────────────────────────
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def _require_runner():
|
|
102
|
+
from agent_eval.api.app import _get_runner
|
|
103
|
+
|
|
104
|
+
runner = _get_runner()
|
|
105
|
+
if runner is None:
|
|
106
|
+
raise HTTPException(status_code=503, detail="EvalRunner not configured")
|
|
107
|
+
return runner
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def _require_dataset_storage(runner):
|
|
111
|
+
datasets = getattr(runner.storage, "datasets", None)
|
|
112
|
+
if datasets is None:
|
|
113
|
+
raise HTTPException(
|
|
114
|
+
status_code=503,
|
|
115
|
+
detail="Dataset storage not available on this storage backend",
|
|
116
|
+
)
|
|
117
|
+
return datasets
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
async def _resolve_dataset_ref(runner, ref: str) -> EvalDataset:
|
|
121
|
+
"""按 ID 或名称定位数据集 (名称取最高版本)"""
|
|
122
|
+
datasets = _require_dataset_storage(runner)
|
|
123
|
+
dataset = await datasets.get_dataset(ref)
|
|
124
|
+
if dataset is None:
|
|
125
|
+
dataset = await datasets.get_dataset_by_name(ref)
|
|
126
|
+
if dataset is None:
|
|
127
|
+
raise HTTPException(status_code=404, detail=f"Dataset '{ref}' not found")
|
|
128
|
+
return dataset
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
async def _save_dataset(runner, dataset: EvalDataset) -> None:
|
|
132
|
+
datasets = _require_dataset_storage(runner)
|
|
133
|
+
await datasets.save_dataset(dataset)
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def _dataset_summary(dataset: EvalDataset) -> dict[str, Any]:
|
|
137
|
+
return {
|
|
138
|
+
"id": dataset.id,
|
|
139
|
+
"name": dataset.name,
|
|
140
|
+
"description": dataset.description,
|
|
141
|
+
"version": dataset.version,
|
|
142
|
+
"tags": dataset.tags,
|
|
143
|
+
"capability_map": dataset.capability_map,
|
|
144
|
+
"item_count": len(dataset.items),
|
|
145
|
+
"created_at": dataset.created_at,
|
|
146
|
+
"updated_at": dataset.updated_at,
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
# ─── Dataset CRUD ────────────────────────────────────────────────────────────
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
@router.get("")
|
|
154
|
+
async def list_datasets(tags: str | None = Query(None, description="逗号分隔标签过滤")):
|
|
155
|
+
"""列出数据集"""
|
|
156
|
+
runner = _require_runner()
|
|
157
|
+
tag_list = [t.strip() for t in tags.split(",") if t.strip()] if tags else None
|
|
158
|
+
datasets = await _require_dataset_storage(runner).list_datasets(tags=tag_list)
|
|
159
|
+
return {"datasets": [_dataset_summary(d) for d in datasets]}
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
@router.post("")
|
|
163
|
+
async def create_dataset(dataset: EvalDataset):
|
|
164
|
+
"""创建数据集 (JSON; 条目可同时携带)"""
|
|
165
|
+
runner = _require_runner()
|
|
166
|
+
await _save_dataset(runner, dataset)
|
|
167
|
+
return {"id": dataset.id, "name": dataset.name, "version": dataset.version,
|
|
168
|
+
"item_count": len(dataset.items)}
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
@router.post("/import")
|
|
172
|
+
async def import_dataset(request: ImportRequest):
|
|
173
|
+
"""从 YAML/JSON 内容导入数据集 (校验失败 422 并给出具体条目与字段)"""
|
|
174
|
+
runner = _require_runner()
|
|
175
|
+
try:
|
|
176
|
+
dataset = import_from_content(
|
|
177
|
+
request.content, format=request.format,
|
|
178
|
+
source_type=SourceType(request.source_type) if request.source_type else SourceType.MANUAL,
|
|
179
|
+
source_ref="api-import",
|
|
180
|
+
)
|
|
181
|
+
except ValueError as e:
|
|
182
|
+
raise HTTPException(
|
|
183
|
+
status_code=422,
|
|
184
|
+
detail=f"Invalid source_type '{request.source_type}' (valid: manual, adversarial)",
|
|
185
|
+
) from e
|
|
186
|
+
except DatasetError as e:
|
|
187
|
+
raise HTTPException(status_code=422, detail=str(e)) from e
|
|
188
|
+
await _save_dataset(runner, dataset)
|
|
189
|
+
return {"id": dataset.id, "name": dataset.name, "item_count": len(dataset.items)}
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
@router.get("/{ref}")
|
|
193
|
+
async def get_dataset(ref: str):
|
|
194
|
+
"""获取数据集详情 (含条目)"""
|
|
195
|
+
runner = _require_runner()
|
|
196
|
+
dataset = await _resolve_dataset_ref(runner, ref)
|
|
197
|
+
return dataset.model_dump()
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
@router.delete("/{ref}")
|
|
201
|
+
async def delete_dataset(ref: str):
|
|
202
|
+
"""删除数据集 (级联删除条目)"""
|
|
203
|
+
runner = _require_runner()
|
|
204
|
+
dataset = await _resolve_dataset_ref(runner, ref)
|
|
205
|
+
deleted = await _require_dataset_storage(runner).delete_dataset(dataset.id)
|
|
206
|
+
if not deleted:
|
|
207
|
+
raise HTTPException(status_code=404, detail=f"Dataset '{ref}' not found")
|
|
208
|
+
return {"deleted": True, "id": dataset.id}
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
# ─── Item CRUD ───────────────────────────────────────────────────────────────
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
@router.get("/{ref}/items")
|
|
215
|
+
async def list_items(ref: str):
|
|
216
|
+
"""列出数据集条目"""
|
|
217
|
+
runner = _require_runner()
|
|
218
|
+
dataset = await _resolve_dataset_ref(runner, ref)
|
|
219
|
+
items = await _require_dataset_storage(runner).get_dataset_items(dataset.id)
|
|
220
|
+
return {"items": [i.model_dump() for i in items]}
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
@router.post("/{ref}/items")
|
|
224
|
+
async def add_item(ref: str, item: EvalDatasetItem):
|
|
225
|
+
"""新增条目 (ID 冲突 409)"""
|
|
226
|
+
runner = _require_runner()
|
|
227
|
+
dataset = await _resolve_dataset_ref(runner, ref)
|
|
228
|
+
if dataset.get_item(item.id) is not None:
|
|
229
|
+
raise HTTPException(status_code=409, detail=f"Item '{item.id}' already exists")
|
|
230
|
+
try:
|
|
231
|
+
await _require_dataset_storage(runner).save_dataset_item(dataset.id, item)
|
|
232
|
+
except KeyError as e:
|
|
233
|
+
raise HTTPException(status_code=404, detail=str(e)) from e
|
|
234
|
+
return {"added": True, "item_id": item.id, "dataset_id": dataset.id}
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
@router.put("/{ref}/items/{item_id}")
|
|
238
|
+
async def update_item(ref: str, item_id: str, item: EvalDatasetItem):
|
|
239
|
+
"""更新条目 (路径 ID 与 body ID 不一致时 422)"""
|
|
240
|
+
if item.id != item_id:
|
|
241
|
+
raise HTTPException(
|
|
242
|
+
status_code=422, detail=f"Body item id '{item.id}' != path item_id '{item_id}'"
|
|
243
|
+
)
|
|
244
|
+
runner = _require_runner()
|
|
245
|
+
dataset = await _resolve_dataset_ref(runner, ref)
|
|
246
|
+
datasets = _require_dataset_storage(runner)
|
|
247
|
+
existing = await datasets.get_dataset_item(dataset.id, item_id)
|
|
248
|
+
if existing is None:
|
|
249
|
+
raise HTTPException(status_code=404, detail=f"Item '{item_id}' not found")
|
|
250
|
+
try:
|
|
251
|
+
await datasets.save_dataset_item(dataset.id, item)
|
|
252
|
+
except KeyError as e:
|
|
253
|
+
raise HTTPException(status_code=404, detail=str(e)) from e
|
|
254
|
+
return {"updated": True, "item_id": item_id}
|
|
255
|
+
|
|
256
|
+
|
|
257
|
+
@router.delete("/{ref}/items/{item_id}")
|
|
258
|
+
async def delete_item(ref: str, item_id: str):
|
|
259
|
+
"""删除条目"""
|
|
260
|
+
runner = _require_runner()
|
|
261
|
+
dataset = await _resolve_dataset_ref(runner, ref)
|
|
262
|
+
deleted = await _require_dataset_storage(runner).delete_dataset_item(dataset.id, item_id)
|
|
263
|
+
if not deleted:
|
|
264
|
+
raise HTTPException(status_code=404, detail=f"Item '{item_id}' not found")
|
|
265
|
+
return {"deleted": True, "item_id": item_id}
|
|
266
|
+
|
|
267
|
+
|
|
268
|
+
# ─── 数据源接入 ───────────────────────────────────────────────────────────────
|
|
269
|
+
|
|
270
|
+
|
|
271
|
+
@router.post("/{ref}/from-trace")
|
|
272
|
+
async def from_trace(ref: str, request: FromTraceRequest):
|
|
273
|
+
"""按策略从 trace 挖掘条目并入数据集"""
|
|
274
|
+
runner = _require_runner()
|
|
275
|
+
dataset = await _resolve_dataset_ref(runner, ref)
|
|
276
|
+
miner = TraceMiner(runner.trace_provider)
|
|
277
|
+
try:
|
|
278
|
+
report = await miner.mine(
|
|
279
|
+
request.strategy,
|
|
280
|
+
filters=request.filters or None,
|
|
281
|
+
limit=request.limit,
|
|
282
|
+
candidate_limit=request.candidate_limit,
|
|
283
|
+
)
|
|
284
|
+
except NotImplementedError as e:
|
|
285
|
+
raise HTTPException(status_code=422, detail=str(e)) from e
|
|
286
|
+
except RuntimeError as e:
|
|
287
|
+
# Phoenix SDK 缺失/不可达 — 明确报错而非静默空结果
|
|
288
|
+
raise HTTPException(status_code=503, detail=f"Trace provider unavailable: {e}") from e
|
|
289
|
+
|
|
290
|
+
# 挖掘条目合入: 跳过 prompt 与既有条目重复的 trace
|
|
291
|
+
extractor = RegressionExtractor()
|
|
292
|
+
dataset, merge_report = extractor.merge_into_dataset(dataset, report.items)
|
|
293
|
+
if report.items:
|
|
294
|
+
await _save_dataset(runner, dataset)
|
|
295
|
+
return {
|
|
296
|
+
"mining": report.to_dict(),
|
|
297
|
+
"merged": merge_report.merged,
|
|
298
|
+
"merged_skipped": merge_report.merged_skipped,
|
|
299
|
+
"dataset_id": dataset.id,
|
|
300
|
+
"item_count": len(dataset.items),
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
|
|
304
|
+
@router.post("/{ref}/from-llm")
|
|
305
|
+
async def from_llm(ref: str, request: FromLLMRequest):
|
|
306
|
+
"""LLM 按场景生成条目并入数据集 (产出经与手动导入相同的校验)"""
|
|
307
|
+
runner = _require_runner()
|
|
308
|
+
if runner.llm_fn is None:
|
|
309
|
+
raise HTTPException(
|
|
310
|
+
status_code=503,
|
|
311
|
+
detail="LLM function not configured (llm_fn) — cannot generate items",
|
|
312
|
+
)
|
|
313
|
+
dataset = await _resolve_dataset_ref(runner, ref)
|
|
314
|
+
generator = LLMDatasetGenerator(llm_fn=runner.llm_fn)
|
|
315
|
+
try:
|
|
316
|
+
report = await generator.generate(
|
|
317
|
+
request.scenario, request.capabilities, request.count
|
|
318
|
+
)
|
|
319
|
+
except DatasetError as e:
|
|
320
|
+
raise HTTPException(status_code=422, detail=str(e)) from e
|
|
321
|
+
|
|
322
|
+
# ID 与既有条目冲突的生成条目跳过 (报告到 invalid)
|
|
323
|
+
existing_ids = {i.id for i in dataset.items}
|
|
324
|
+
fresh = []
|
|
325
|
+
for item in report.items:
|
|
326
|
+
if item.id in existing_ids:
|
|
327
|
+
report.invalid.append({"index": -1, "error": f"duplicate id: {item.id}"})
|
|
328
|
+
else:
|
|
329
|
+
fresh.append(item)
|
|
330
|
+
existing_ids.add(item.id)
|
|
331
|
+
|
|
332
|
+
dataset = dataset.model_copy(update={
|
|
333
|
+
"items": [*dataset.items, *fresh],
|
|
334
|
+
"updated_at": time.time() * 1000,
|
|
335
|
+
})
|
|
336
|
+
if fresh:
|
|
337
|
+
await _save_dataset(runner, dataset)
|
|
338
|
+
return {
|
|
339
|
+
"generation": report.to_dict(),
|
|
340
|
+
"dataset_id": dataset.id,
|
|
341
|
+
"item_count": len(dataset.items),
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
|
|
345
|
+
@router.post("/{ref}/regression-extract")
|
|
346
|
+
async def regression_extract(ref: str, request: RegressionExtractRequest):
|
|
347
|
+
"""从 run 失败 trial 提取回归样本并入数据集 (质量闭环)"""
|
|
348
|
+
runner = _require_runner()
|
|
349
|
+
dataset = await _resolve_dataset_ref(runner, ref)
|
|
350
|
+
|
|
351
|
+
run = await runner.storage.get_run(request.run_id)
|
|
352
|
+
if run is None:
|
|
353
|
+
raise HTTPException(status_code=404, detail=f"Run '{request.run_id}' not found")
|
|
354
|
+
|
|
355
|
+
suite = None
|
|
356
|
+
if run.suite_name:
|
|
357
|
+
suite = await runner.storage.get_suite(run.suite_name)
|
|
358
|
+
|
|
359
|
+
items, report = RegressionExtractor(max_items=request.max_items).extract_from_run(run, suite)
|
|
360
|
+
dataset, merge_report = RegressionExtractor().merge_into_dataset(dataset, items)
|
|
361
|
+
|
|
362
|
+
version_note = f"regression merge from run {request.run_id}"
|
|
363
|
+
if request.bump_version and merge_report.merged > 0:
|
|
364
|
+
dataset = DatasetVersionManager.bump(dataset, request.bump_version, version_note)
|
|
365
|
+
|
|
366
|
+
if merge_report.merged:
|
|
367
|
+
await _save_dataset(runner, dataset)
|
|
368
|
+
|
|
369
|
+
return {
|
|
370
|
+
"extraction": report.to_dict(),
|
|
371
|
+
"merge": merge_report.to_dict(),
|
|
372
|
+
"version": dataset.version,
|
|
373
|
+
"bumped": bool(request.bump_version and merge_report.merged),
|
|
374
|
+
"dataset_id": dataset.id,
|
|
375
|
+
"item_count": len(dataset.items),
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
|
|
379
|
+
# ─── 质量 / 覆盖度 ───────────────────────────────────────────────────────────
|
|
380
|
+
|
|
381
|
+
|
|
382
|
+
@router.get("/{ref}/quality-check")
|
|
383
|
+
async def quality_check(ref: str):
|
|
384
|
+
"""数据集质量检查 (errors 阻塞 to-suite, warnings 仅提示)"""
|
|
385
|
+
runner = _require_runner()
|
|
386
|
+
dataset = await _resolve_dataset_ref(runner, ref)
|
|
387
|
+
return DatasetQualityChecker().check(dataset).to_dict()
|
|
388
|
+
|
|
389
|
+
|
|
390
|
+
@router.get("/{ref}/coverage")
|
|
391
|
+
async def coverage(ref: str, expected: str | None = Query(None, description="逗号分隔的期望能力维度")):
|
|
392
|
+
"""能力维度覆盖度分析"""
|
|
393
|
+
runner = _require_runner()
|
|
394
|
+
dataset = await _resolve_dataset_ref(runner, ref)
|
|
395
|
+
expected_list = (
|
|
396
|
+
[c.strip() for c in expected.split(",") if c.strip()] if expected else None
|
|
397
|
+
)
|
|
398
|
+
return CoverageAnalyzer().analyze(dataset, expected_capabilities=expected_list).to_dict()
|
|
399
|
+
|
|
400
|
+
|
|
401
|
+
# ─── 转换与版本 ───────────────────────────────────────────────────────────────
|
|
402
|
+
|
|
403
|
+
|
|
404
|
+
@router.post("/{ref}/to-suite")
|
|
405
|
+
async def to_suite(ref: str, request: ToSuiteRequest):
|
|
406
|
+
"""转换为可执行 Suite (非法条目 422 并给出明确错误)"""
|
|
407
|
+
runner = _require_runner()
|
|
408
|
+
dataset = await _resolve_dataset_ref(runner, ref)
|
|
409
|
+
try:
|
|
410
|
+
suite = dataset.to_suite(request.name)
|
|
411
|
+
except DatasetError as e:
|
|
412
|
+
raise HTTPException(status_code=422, detail=str(e)) from e
|
|
413
|
+
|
|
414
|
+
if request.save:
|
|
415
|
+
await runner.storage.save_suite(suite)
|
|
416
|
+
return {
|
|
417
|
+
"suite_name": suite.name,
|
|
418
|
+
"task_count": len(suite.tasks),
|
|
419
|
+
"metadata": suite.metadata,
|
|
420
|
+
"saved": request.save,
|
|
421
|
+
"dataset_id": dataset.id,
|
|
422
|
+
"dataset_version": dataset.version,
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
|
|
426
|
+
@router.post("/{ref}/version")
|
|
427
|
+
async def bump_version(ref: str, request: VersionBumpRequest):
|
|
428
|
+
"""语义化升版 (major/minor/patch) 并记录变更"""
|
|
429
|
+
runner = _require_runner()
|
|
430
|
+
dataset = await _resolve_dataset_ref(runner, ref)
|
|
431
|
+
try:
|
|
432
|
+
dataset = DatasetVersionManager.bump(dataset, request.change_type, request.note)
|
|
433
|
+
except DatasetError as e:
|
|
434
|
+
raise HTTPException(status_code=422, detail=str(e)) from e
|
|
435
|
+
await _save_dataset(runner, dataset)
|
|
436
|
+
return {
|
|
437
|
+
"id": dataset.id,
|
|
438
|
+
"version": dataset.version,
|
|
439
|
+
"change_log": dataset.change_log,
|
|
440
|
+
"item_count": len(dataset.items),
|
|
441
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Grader listing routes.
|
|
3
|
+
|
|
4
|
+
GET /graders — List available graders (name/type/description)
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from fastapi import APIRouter
|
|
10
|
+
|
|
11
|
+
router = APIRouter()
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@router.get("")
|
|
15
|
+
async def list_graders():
|
|
16
|
+
"""列出可用 grader (静态注册表, 不依赖 runner 注入)"""
|
|
17
|
+
from agent_eval.graders import get_grader_catalog
|
|
18
|
+
|
|
19
|
+
return {"graders": get_grader_catalog()}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Metric batch evaluation routes.
|
|
3
|
+
|
|
4
|
+
POST /metrics/batch — Batch-score pre-existing outputs (historical
|
|
5
|
+
conversations / logs) with the runner-injected metrics registry.
|
|
6
|
+
|
|
7
|
+
指标名经 runner.metrics_registry 解析 (与 metric grader 同源装配);
|
|
8
|
+
未注册指标名返回 422 并列出无效指标; runner 未装配返回 503。
|
|
9
|
+
成本提示: 每条用例 × 每指标一次 judge LLM 调用 (并发上限 4, 同 prompt
|
|
10
|
+
经 judge 层缓存命中则跳过)。
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
from fastapi import APIRouter, HTTPException
|
|
16
|
+
|
|
17
|
+
from agent_eval.metrics.batch_evaluation import (
|
|
18
|
+
BatchEvaluationRequest,
|
|
19
|
+
BatchEvaluator,
|
|
20
|
+
UnknownMetricsError,
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
router = APIRouter()
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
@router.post("/batch")
|
|
27
|
+
async def batch_evaluate(request: BatchEvaluationRequest):
|
|
28
|
+
"""批量评测已有输出 (不运行 Agent; 对历史对话/日志补测)。"""
|
|
29
|
+
from agent_eval.api.app import _get_runner
|
|
30
|
+
from agent_eval.metrics.llm_judge import LLMNotConfiguredError
|
|
31
|
+
|
|
32
|
+
runner = _get_runner()
|
|
33
|
+
if runner is None:
|
|
34
|
+
raise HTTPException(status_code=503, detail="EvalRunner not configured")
|
|
35
|
+
if not runner.metrics_registry:
|
|
36
|
+
raise HTTPException(
|
|
37
|
+
status_code=503, detail="Metrics registry not configured"
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
evaluator = BatchEvaluator(runner.metrics_registry, llm_fn=runner.llm_fn)
|
|
41
|
+
try:
|
|
42
|
+
return await evaluator.evaluate(request)
|
|
43
|
+
except UnknownMetricsError as e:
|
|
44
|
+
raise HTTPException(
|
|
45
|
+
status_code=422,
|
|
46
|
+
detail={"message": str(e), "unknown_metrics": e.unknown},
|
|
47
|
+
) from e
|
|
48
|
+
except LLMNotConfiguredError as e:
|
|
49
|
+
raise HTTPException(status_code=503, detail=str(e)) from e
|