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.
Files changed (63) hide show
  1. aeval_framework-0.1.0.dist-info/METADATA +42 -0
  2. aeval_framework-0.1.0.dist-info/RECORD +63 -0
  3. aeval_framework-0.1.0.dist-info/WHEEL +4 -0
  4. aeval_framework-0.1.0.dist-info/entry_points.txt +2 -0
  5. agent_eval/__init__.py +14 -0
  6. agent_eval/api/__init__.py +14 -0
  7. agent_eval/api/app.py +82 -0
  8. agent_eval/api/events.py +96 -0
  9. agent_eval/api/routes/__init__.py +0 -0
  10. agent_eval/api/routes/datasets.py +441 -0
  11. agent_eval/api/routes/graders.py +19 -0
  12. agent_eval/api/routes/metrics.py +49 -0
  13. agent_eval/api/routes/runs.py +573 -0
  14. agent_eval/api/routes/suites.py +84 -0
  15. agent_eval/api/routes/tasks.py +114 -0
  16. agent_eval/api/standalone.py +105 -0
  17. agent_eval/cli.py +455 -0
  18. agent_eval/core/__init__.py +48 -0
  19. agent_eval/core/contract.py +296 -0
  20. agent_eval/core/metrics.py +184 -0
  21. agent_eval/core/runner.py +868 -0
  22. agent_eval/core/suite.py +60 -0
  23. agent_eval/core/types.py +227 -0
  24. agent_eval/dataset/__init__.py +31 -0
  25. agent_eval/dataset/models.py +199 -0
  26. agent_eval/dataset/quality.py +194 -0
  27. agent_eval/dataset/sources/__init__.py +45 -0
  28. agent_eval/dataset/sources/llm_generator.py +219 -0
  29. agent_eval/dataset/sources/manual.py +172 -0
  30. agent_eval/dataset/sources/regression.py +201 -0
  31. agent_eval/dataset/sources/trace_mining.py +277 -0
  32. agent_eval/dataset/storage.py +342 -0
  33. agent_eval/dataset/version.py +72 -0
  34. agent_eval/examples/__init__.py +0 -0
  35. agent_eval/examples/basic_usage.py +175 -0
  36. agent_eval/examples/mock_runner.py +195 -0
  37. agent_eval/graders/__init__.py +91 -0
  38. agent_eval/graders/artifact_check.py +114 -0
  39. agent_eval/graders/code_based.py +101 -0
  40. agent_eval/graders/human.py +77 -0
  41. agent_eval/graders/metric.py +142 -0
  42. agent_eval/graders/model_based.py +179 -0
  43. agent_eval/graders/state_check.py +106 -0
  44. agent_eval/graders/step_level.py +116 -0
  45. agent_eval/graders/tool_calls.py +102 -0
  46. agent_eval/graders/transcript.py +86 -0
  47. agent_eval/metrics/__init__.py +110 -0
  48. agent_eval/metrics/answer_relevancy.py +57 -0
  49. agent_eval/metrics/base.py +155 -0
  50. agent_eval/metrics/batch_evaluation.py +267 -0
  51. agent_eval/metrics/context_precision.py +62 -0
  52. agent_eval/metrics/context_recall.py +71 -0
  53. agent_eval/metrics/faithfulness.py +72 -0
  54. agent_eval/metrics/llm_judge.py +100 -0
  55. agent_eval/metrics/prompt_metric.py +150 -0
  56. agent_eval/metrics/pytest_plugin.py +308 -0
  57. agent_eval/metrics/report.py +149 -0
  58. agent_eval/metrics/synthetic_data.py +203 -0
  59. agent_eval/storage/__init__.py +17 -0
  60. agent_eval/storage/memory.py +95 -0
  61. agent_eval/storage/sqlite.py +240 -0
  62. agent_eval/trace/__init__.py +16 -0
  63. agent_eval/trace/phoenix.py +144 -0
@@ -0,0 +1,573 @@
1
+ """
2
+ Run management routes.
3
+
4
+ GET /runs — List run history
5
+ POST /runs — Start a new run
6
+ GET /runs/{run_id} — Get run details
7
+ DELETE /runs/{run_id} — Delete a run
8
+ GET /runs/{run_id}/trials — Get trials for a run
9
+ GET /runs/{run_id}/stream — SSE event stream (realtime progress)
10
+ POST /runs/{run_id}/cancel — Cancel a running run
11
+ POST /runs/{run_id}/human-scores — Submit a human score for a pending trial
12
+
13
+ Separate router (mounted at the API root, no /runs prefix):
14
+ POST /compare — Compare two runs
15
+ """
16
+
17
+ from __future__ import annotations
18
+
19
+ import asyncio
20
+ import json
21
+ import logging
22
+ import time
23
+ import uuid
24
+ from contextlib import suppress
25
+ from typing import Any
26
+
27
+ from fastapi import APIRouter, HTTPException
28
+ from pydantic import BaseModel, Field
29
+ from sse_starlette.sse import EventSourceResponse
30
+
31
+ from agent_eval.api.events import run_event_bus
32
+
33
+ router = APIRouter()
34
+
35
+ logger = logging.getLogger(__name__)
36
+
37
+ # run_id → 后台 asyncio task 注册表 (取消运行用)
38
+ _background_tasks: dict[str, asyncio.Task] = {}
39
+
40
+ # SSE 心跳间隔 (秒) — 防代理空闲断连 (§17.3)
41
+ _STREAM_HEARTBEAT_SECONDS = 15.0
42
+
43
+
44
+ @router.get("")
45
+ async def list_runs(suite_name: str | None = None, limit: int = 50):
46
+ """列出运行历史"""
47
+ from agent_eval.api.app import _get_runner
48
+
49
+ runner = _get_runner()
50
+ if runner is None:
51
+ raise HTTPException(status_code=503, detail="EvalRunner not configured")
52
+
53
+ runs = await runner.storage.list_runs(suite_name=suite_name, limit=limit)
54
+ return {
55
+ "runs": [
56
+ {
57
+ "run_id": r.run_id,
58
+ "suite_name": r.suite_name,
59
+ "status": r.status,
60
+ "started_at": r.started_at,
61
+ "completed_at": r.completed_at,
62
+ "duration_ms": r.duration_ms,
63
+ "task_count": len(r.trials),
64
+ "summary": r.summary.model_dump() if r.summary else None,
65
+ }
66
+ for r in runs
67
+ ]
68
+ }
69
+
70
+
71
+ class CreateRunRequest(BaseModel):
72
+ """启动运行的请求体"""
73
+ suite_name: str
74
+ config: dict[str, Any] = {}
75
+
76
+
77
+ @router.post("")
78
+ async def create_run(request: CreateRunRequest):
79
+ """启动一次 suite 运行 (立即返回 run_id, 后台执行)"""
80
+ from agent_eval.api.app import _get_runner
81
+
82
+ runner = _get_runner()
83
+ if runner is None:
84
+ raise HTTPException(status_code=503, detail="EvalRunner not configured")
85
+
86
+ # Load suite
87
+ suite = await runner.storage.get_suite(request.suite_name)
88
+ if suite is None:
89
+ raise HTTPException(
90
+ status_code=404,
91
+ detail=f"Suite '{request.suite_name}' not found. Create it first.",
92
+ )
93
+
94
+ # 预生成 run_id, 便于客户端立即轮询
95
+ run_id = f"run_{uuid.uuid4().hex[:12]}"
96
+
97
+ # 先落一条 pending 记录, 保证 POST 返回后 GET /runs/{run_id} 立即可查
98
+ from agent_eval.core.types import RunResult
99
+
100
+ await runner.storage.save_run(
101
+ RunResult(run_id=run_id, suite_name=suite.name, status="pending")
102
+ )
103
+
104
+ async def _run():
105
+ # 事件总线: runner 进度回调 → per-run fan-out (SSE 增量, 不落库)
106
+ async def _emit(event: str, data: dict[str, Any]) -> None:
107
+ run_event_bus.publish(run_id, event, data)
108
+
109
+ try:
110
+ result = await runner.run_suite(suite, callback=_emit, run_id=run_id)
111
+ if result.status == "failed":
112
+ run_event_bus.publish(run_id, "error", {"error": result.error or "run failed"})
113
+ run_event_bus.publish(
114
+ run_id,
115
+ "run_complete",
116
+ {
117
+ "status": result.status,
118
+ "summary": result.summary.model_dump() if result.summary else None,
119
+ "error": result.error,
120
+ },
121
+ )
122
+ except asyncio.CancelledError:
123
+ # run_suite 已将状态置为 cancelled 并保存; 观察端也需要终态
124
+ run_event_bus.publish(run_id, "run_complete", {"status": "cancelled"})
125
+ except Exception as e:
126
+ logger.warning("Run %s failed: %s", run_id, e)
127
+ run_event_bus.publish(run_id, "error", {"error": str(e)})
128
+ run_event_bus.publish(run_id, "run_complete", {"status": "failed", "error": str(e)})
129
+ finally:
130
+ _background_tasks.pop(run_id, None)
131
+
132
+ # Create task (runs in background)
133
+ task = asyncio.create_task(_run())
134
+ _background_tasks[run_id] = task
135
+
136
+ # Return immediately with run info
137
+ return {
138
+ "run_id": run_id,
139
+ "message": "Run started",
140
+ "suite_name": request.suite_name,
141
+ "task_count": len(suite.tasks),
142
+ "status": "running",
143
+ }
144
+
145
+
146
+ @router.get("/{run_id}")
147
+ async def get_run(run_id: str):
148
+ """获取运行详情"""
149
+ from agent_eval.api.app import _get_runner
150
+
151
+ runner = _get_runner()
152
+ if runner is None:
153
+ raise HTTPException(status_code=503, detail="EvalRunner not configured")
154
+
155
+ run = await runner.storage.get_run(run_id)
156
+ if run is None:
157
+ raise HTTPException(status_code=404, detail=f"Run '{run_id}' not found")
158
+
159
+ return {
160
+ "run_id": run.run_id,
161
+ "suite_name": run.suite_name,
162
+ "status": run.status,
163
+ "started_at": run.started_at,
164
+ "completed_at": run.completed_at,
165
+ "duration_ms": run.duration_ms,
166
+ "error": run.error,
167
+ "trials": {
168
+ task_id: [
169
+ {
170
+ "trial_index": t.trial_index,
171
+ "trace_id": t.trace_id,
172
+ "success": t.success,
173
+ "score": t.avg_score(),
174
+ "duration_ms": t.duration_ms,
175
+ "error": t.error,
176
+ "grader_results": [
177
+ {
178
+ "grader_name": gr.grader_name,
179
+ "score": gr.score,
180
+ "passed": gr.passed,
181
+ "explanation": gr.explanation,
182
+ }
183
+ for gr in t.grader_results
184
+ ],
185
+ }
186
+ for t in trials
187
+ ]
188
+ for task_id, trials in run.trials.items()
189
+ },
190
+ "summary": run.summary.model_dump() if run.summary else None,
191
+ }
192
+
193
+
194
+ @router.delete("/{run_id}")
195
+ async def delete_run(run_id: str):
196
+ """删除运行"""
197
+ from agent_eval.api.app import _get_runner
198
+
199
+ runner = _get_runner()
200
+ if runner is None:
201
+ raise HTTPException(status_code=503, detail="EvalRunner not configured")
202
+
203
+ deleted = await runner.storage.delete_run(run_id)
204
+ if not deleted:
205
+ raise HTTPException(status_code=404, detail=f"Run '{run_id}' not found")
206
+
207
+ return {"deleted": True}
208
+
209
+
210
+ @router.get("/{run_id}/trials")
211
+ async def get_trials(run_id: str, task_id: str | None = None):
212
+ """获取 trial 列表"""
213
+ from agent_eval.api.app import _get_runner
214
+
215
+ runner = _get_runner()
216
+ if runner is None:
217
+ raise HTTPException(status_code=503, detail="EvalRunner not configured")
218
+
219
+ run = await runner.storage.get_run(run_id)
220
+ if run is None:
221
+ raise HTTPException(status_code=404, detail=f"Run '{run_id}' not found")
222
+
223
+ if task_id:
224
+ trials = run.trials.get(task_id, [])
225
+ return {
226
+ "task_id": task_id,
227
+ "trials": [t.model_dump() for t in trials],
228
+ }
229
+
230
+ # Return all trials
231
+ all_trials = {}
232
+ for tid, trials in run.trials.items():
233
+ all_trials[tid] = [t.model_dump() for t in trials]
234
+ return {"trials": all_trials}
235
+
236
+
237
+ # ─── SSE 事件流 (任务 3.2, 协议 §17.3) ───────────────────────────────────────
238
+
239
+
240
+ def _terminal_event_from_run(run) -> dict[str, Any]:
241
+ """从存储的 run 记录合成 run_complete 终态事件 (进程重启后 bus 无缓存)。"""
242
+ return {
243
+ "type": "run_complete",
244
+ "run_id": run.run_id,
245
+ "timestamp": time.time() * 1000,
246
+ "status": run.status,
247
+ "summary": run.summary.model_dump() if run.summary else None,
248
+ "error": run.error,
249
+ }
250
+
251
+
252
+ async def _stream_events(run_id: str):
253
+ """SSE 事件生成器: 订阅即推增量; run_complete 收尾关流; 心跳注释行。
254
+
255
+ 断线恢复不在此处理 — 客户端按快照+增量协议重拉快照后重新订阅。
256
+ """
257
+ queue = run_event_bus.subscribe(run_id)
258
+ try:
259
+ while True:
260
+ try:
261
+ event = await asyncio.wait_for(
262
+ queue.get(), timeout=_STREAM_HEARTBEAT_SECONDS
263
+ )
264
+ except TimeoutError:
265
+ yield {"comment": "heartbeat"}
266
+ continue
267
+ yield {"data": json.dumps(event, default=str)}
268
+ if event.get("type") == "run_complete":
269
+ return
270
+ finally:
271
+ run_event_bus.unsubscribe(run_id, queue)
272
+
273
+
274
+ @router.get("/{run_id}/stream")
275
+ async def stream_run(run_id: str):
276
+ """SSE 推送运行事件 (task/trial 生命周期 + 终态)。
277
+
278
+ - 运行中 run: 订阅即推增量, run_complete 后关流
279
+ - 已完成 run: 立即推 run_complete 后关流 (bus 终态缓存或由存储合成)
280
+ - 连接只是观察窗口: 断开不影响 run 执行 (后台任务持有生命周期)
281
+ """
282
+ from agent_eval.api.app import _get_runner
283
+
284
+ runner = _get_runner()
285
+ if runner is None:
286
+ raise HTTPException(status_code=503, detail="EvalRunner not configured")
287
+
288
+ run = await runner.storage.get_run(run_id)
289
+ if run is None:
290
+ raise HTTPException(status_code=404, detail=f"Run '{run_id}' not found")
291
+
292
+ if run.status in ("completed", "failed", "cancelled"):
293
+ cached = run_event_bus.terminal_event(run_id)
294
+
295
+ async def _terminal_only():
296
+ yield {"data": json.dumps(cached or _terminal_event_from_run(run), default=str)}
297
+
298
+ return EventSourceResponse(
299
+ _terminal_only(),
300
+ headers={"Cache-Control": "no-cache, no-transform"},
301
+ )
302
+
303
+ return EventSourceResponse(
304
+ _stream_events(run_id),
305
+ headers={"Cache-Control": "no-cache, no-transform", "X-Accel-Buffering": "no"},
306
+ )
307
+
308
+
309
+ @router.post("/{run_id}/cancel")
310
+ async def cancel_run(run_id: str):
311
+ """取消一个运行中的 run (已完成 trial 保留)"""
312
+ import time
313
+
314
+ from agent_eval.api.app import _get_runner
315
+
316
+ runner = _get_runner()
317
+ if runner is None:
318
+ raise HTTPException(status_code=503, detail="EvalRunner not configured")
319
+
320
+ task = _background_tasks.get(run_id)
321
+ if task is None or task.done():
322
+ run = await runner.storage.get_run(run_id)
323
+ if run is None:
324
+ raise HTTPException(status_code=404, detail=f"Run '{run_id}' not found")
325
+ raise HTTPException(
326
+ status_code=409, detail=f"Run '{run_id}' is not running"
327
+ )
328
+
329
+ task.cancel()
330
+ # 等后台任务收尾: run_suite 捕获 CancelledError 后保存 cancelled 状态;
331
+ # 若任务尚未启动就被取消 (协程未执行), 这里兜底更新 run 记录。
332
+ with suppress(asyncio.CancelledError):
333
+ await asyncio.wait({task})
334
+ run = await runner.storage.get_run(run_id)
335
+ if run is not None and run.status not in ("completed", "failed", "cancelled"):
336
+ run.status = "cancelled"
337
+ run.completed_at = time.time() * 1000
338
+ await runner.storage.save_run(run)
339
+
340
+ return {"run_id": run_id, "cancelled": True}
341
+
342
+
343
+ class HumanScoreRequest(BaseModel):
344
+ """人工评分回传请求体"""
345
+
346
+ task_id: str
347
+ trial_index: int = Field(..., ge=0)
348
+ score: float = Field(..., ge=0.0, le=1.0, description="人工评分 0.0-1.0")
349
+ explanation: str = Field("", description="评分理由/反馈")
350
+ grader_name: str = Field("human", description="人工评分器名称")
351
+
352
+
353
+ @router.post("/{run_id}/human-scores")
354
+ async def submit_human_score(run_id: str, request: HumanScoreRequest):
355
+ """人工评分回传: 更新已存 GraderResult 并重算该 task 汇总"""
356
+ from agent_eval.api.app import _get_runner
357
+
358
+ runner = _get_runner()
359
+ if runner is None:
360
+ raise HTTPException(status_code=503, detail="EvalRunner not configured")
361
+
362
+ run = await runner.storage.get_run(run_id)
363
+ if run is None:
364
+ raise HTTPException(status_code=404, detail=f"Run '{run_id}' not found")
365
+
366
+ trials = run.trials.get(request.task_id)
367
+ if not trials:
368
+ raise HTTPException(
369
+ status_code=404,
370
+ detail=f"Task '{request.task_id}' not found in run '{run_id}'",
371
+ )
372
+ trial = next(
373
+ (t for t in trials if t.trial_index == request.trial_index), None
374
+ )
375
+ if trial is None:
376
+ raise HTTPException(
377
+ status_code=404,
378
+ detail=(
379
+ f"Trial {request.trial_index} not found for task "
380
+ f"'{request.task_id}'"
381
+ ),
382
+ )
383
+
384
+ grader_result = next(
385
+ (
386
+ gr
387
+ for gr in trial.grader_results
388
+ if gr.grader_name == request.grader_name
389
+ ),
390
+ None,
391
+ )
392
+ if grader_result is None:
393
+ raise HTTPException(
394
+ status_code=404,
395
+ detail=(
396
+ f"No grader result '{request.grader_name}' on task "
397
+ f"'{request.task_id}' trial {request.trial_index}"
398
+ ),
399
+ )
400
+
401
+ # 从 suite 中取 task 定义 (评分策略/阈值); suite 缺失时用默认阈值
402
+ suite = await runner.storage.get_suite(run.suite_name)
403
+ task = None
404
+ if suite is not None:
405
+ task = next((t for t in suite.tasks if t.id == request.task_id), None)
406
+
407
+ threshold = task.score_threshold if task is not None else 0.7
408
+ if task is not None:
409
+ threshold = task.get_grader_config(request.grader_name).get(
410
+ "threshold", threshold
411
+ )
412
+
413
+ # 更新已存的 GraderResult
414
+ grader_result.score = request.score
415
+ grader_result.passed = request.score >= threshold
416
+ grader_result.explanation = request.explanation or "人工评分"
417
+ grader_result.confidence = 1.0
418
+ grader_result.details = {
419
+ **grader_result.details,
420
+ "status": "scored",
421
+ "human_score": request.score,
422
+ "human_explanation": request.explanation,
423
+ }
424
+
425
+ # 重算 trial 成功状态与 run 汇总 (含该 task 汇总)
426
+ if task is not None:
427
+ trial.success = runner._compute_trial_success(task, trial.grader_results)
428
+ run.summary = runner._compute_summary(run, suite) if suite is not None else run.summary
429
+ await runner.storage.save_run(run)
430
+
431
+ return {
432
+ "run_id": run_id,
433
+ "task_id": request.task_id,
434
+ "trial_index": request.trial_index,
435
+ "grader_name": request.grader_name,
436
+ "score": grader_result.score,
437
+ "passed": grader_result.passed,
438
+ "trial_success": trial.success,
439
+ "summary": run.summary.model_dump() if run.summary else None,
440
+ }
441
+
442
+
443
+ class CompareRequest(BaseModel):
444
+ """对比请求体"""
445
+ run_id_a: str
446
+ run_id_b: str
447
+
448
+
449
+ # compare 挂载在 /api/eval/compare (spec §REST API), 不带 /runs 前缀
450
+ compare_router = APIRouter()
451
+
452
+
453
+ @compare_router.post("/compare")
454
+ async def compare_runs(request: CompareRequest):
455
+ """对比两次运行"""
456
+ from agent_eval.api.app import _get_runner
457
+
458
+ runner = _get_runner()
459
+ if runner is None:
460
+ raise HTTPException(status_code=503, detail="EvalRunner not configured")
461
+
462
+ run_a = await runner.storage.get_run(request.run_id_a)
463
+ run_b = await runner.storage.get_run(request.run_id_b)
464
+
465
+ if run_a is None:
466
+ raise HTTPException(status_code=404, detail=f"Run '{request.run_id_a}' not found")
467
+ if run_b is None:
468
+ raise HTTPException(status_code=404, detail=f"Run '{request.run_id_b}' not found")
469
+
470
+ if not run_a.summary or not run_b.summary:
471
+ raise HTTPException(status_code=400, detail="Both runs must be completed")
472
+
473
+ # Build comparison
474
+ comparison = _build_comparison(run_a, run_b)
475
+
476
+ return {
477
+ "run_a": {
478
+ "run_id": run_a.run_id,
479
+ "suite_name": run_a.suite_name,
480
+ "started_at": run_a.started_at,
481
+ },
482
+ "run_b": {
483
+ "run_id": run_b.run_id,
484
+ "suite_name": run_b.suite_name,
485
+ "started_at": run_b.started_at,
486
+ },
487
+ "comparison": comparison,
488
+ }
489
+
490
+
491
+ def _build_comparison(run_a, run_b) -> dict[str, Any]:
492
+ """构建两次运行的对比"""
493
+ summary_a = run_a.summary
494
+ summary_b = run_b.summary
495
+
496
+ # 全局指标对比
497
+ all_k_values = set()
498
+ if summary_a.pass_at_k:
499
+ all_k_values.update(summary_a.pass_at_k.keys())
500
+ if summary_b.pass_at_k:
501
+ all_k_values.update(summary_b.pass_at_k.keys())
502
+
503
+ pass_at_k_comparison = {}
504
+ for k in sorted(all_k_values):
505
+ a_val = summary_a.pass_at_k.get(k, 0.0)
506
+ b_val = summary_b.pass_at_k.get(k, 0.0)
507
+ pass_at_k_comparison[f"pass_at_{k}"] = {
508
+ "a": a_val,
509
+ "b": b_val,
510
+ "delta": round(b_val - a_val, 4),
511
+ }
512
+
513
+ pass_power_k_comparison = {}
514
+ for k in sorted(all_k_values):
515
+ a_val = summary_a.pass_power_k.get(k, 0.0)
516
+ b_val = summary_b.pass_power_k.get(k, 0.0)
517
+ pass_power_k_comparison[f"pass_power_{k}"] = {
518
+ "a": a_val,
519
+ "b": b_val,
520
+ "delta": round(b_val - a_val, 4),
521
+ }
522
+
523
+ # 逐 task 对比
524
+ task_a_map = {ts.task_id: ts for ts in summary_a.task_summaries}
525
+ task_b_map = {ts.task_id: ts for ts in summary_b.task_summaries}
526
+
527
+ all_task_ids = set(task_a_map.keys()) | set(task_b_map.keys())
528
+ regressions = []
529
+ improvements = []
530
+ task_comparisons = {}
531
+
532
+ for task_id in sorted(all_task_ids):
533
+ ts_a = task_a_map.get(task_id)
534
+ ts_b = task_b_map.get(task_id)
535
+
536
+ if ts_a and ts_b:
537
+ a_score = ts_a.avg_score
538
+ b_score = ts_b.avg_score
539
+ delta = round(b_score - a_score, 4)
540
+
541
+ task_comparisons[task_id] = {
542
+ "a": a_score,
543
+ "b": b_score,
544
+ "delta": delta,
545
+ }
546
+
547
+ if delta < -0.1:
548
+ regressions.append({
549
+ "task_id": task_id,
550
+ "a": a_score,
551
+ "b": b_score,
552
+ "delta": delta,
553
+ })
554
+ elif delta > 0.1:
555
+ improvements.append({
556
+ "task_id": task_id,
557
+ "a": a_score,
558
+ "b": b_score,
559
+ "delta": delta,
560
+ })
561
+
562
+ return {
563
+ "pass_at_k": pass_at_k_comparison,
564
+ "pass_power_k": pass_power_k_comparison,
565
+ "avg_score": {
566
+ "a": summary_a.avg_score,
567
+ "b": summary_b.avg_score,
568
+ "delta": round(summary_b.avg_score - summary_a.avg_score, 4),
569
+ },
570
+ "regressions": regressions,
571
+ "improvements": improvements,
572
+ "tasks": task_comparisons,
573
+ }
@@ -0,0 +1,84 @@
1
+ """
2
+ Suite management routes.
3
+
4
+ GET /suites — List all suites
5
+ POST /suites — Create a suite (JSON body)
6
+ GET /suites/{name} — Get suite details
7
+ DELETE /suites/{name} — Delete a suite
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ from fastapi import APIRouter, HTTPException
13
+
14
+ from agent_eval.core.types import EvalSuite
15
+
16
+ router = APIRouter()
17
+
18
+
19
+ @router.get("")
20
+ async def list_suites():
21
+ """列出所有评测套件"""
22
+ from agent_eval.api.app import _get_runner
23
+
24
+ runner = _get_runner()
25
+ if runner is None:
26
+ raise HTTPException(status_code=503, detail="EvalRunner not configured")
27
+
28
+ suites = await runner.storage.list_suites()
29
+ return {
30
+ "suites": [
31
+ {
32
+ "name": s.name,
33
+ "description": s.description,
34
+ "task_count": len(s.tasks),
35
+ "metadata": s.metadata,
36
+ }
37
+ for s in suites
38
+ ]
39
+ }
40
+
41
+
42
+ @router.post("")
43
+ async def create_suite(suite: EvalSuite):
44
+ """创建评测套件"""
45
+ from agent_eval.api.app import _get_runner
46
+
47
+ runner = _get_runner()
48
+ if runner is None:
49
+ raise HTTPException(status_code=503, detail="EvalRunner not configured")
50
+
51
+ await runner.storage.save_suite(suite)
52
+ return {"name": suite.name, "task_count": len(suite.tasks)}
53
+
54
+
55
+ @router.get("/{name}")
56
+ async def get_suite(name: str):
57
+ """获取评测套件详情"""
58
+ from agent_eval.api.app import _get_runner
59
+
60
+ runner = _get_runner()
61
+ if runner is None:
62
+ raise HTTPException(status_code=503, detail="EvalRunner not configured")
63
+
64
+ suite = await runner.storage.get_suite(name)
65
+ if suite is None:
66
+ raise HTTPException(status_code=404, detail=f"Suite '{name}' not found")
67
+
68
+ return suite.model_dump()
69
+
70
+
71
+ @router.delete("/{name}")
72
+ async def delete_suite(name: str):
73
+ """删除评测套件"""
74
+ from agent_eval.api.app import _get_runner
75
+
76
+ runner = _get_runner()
77
+ if runner is None:
78
+ raise HTTPException(status_code=503, detail="EvalRunner not configured")
79
+
80
+ deleted = await runner.storage.delete_suite(name)
81
+ if not deleted:
82
+ raise HTTPException(status_code=404, detail=f"Suite '{name}' not found")
83
+
84
+ return {"deleted": True}