aeval-framework 0.1.0__tar.gz → 0.3.0__tar.gz

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 (149) hide show
  1. aeval_framework-0.3.0/.gitignore +39 -0
  2. {aeval_framework-0.1.0 → aeval_framework-0.3.0}/PKG-INFO +2 -2
  3. {aeval_framework-0.1.0 → aeval_framework-0.3.0}/pyproject.toml +5 -2
  4. {aeval_framework-0.1.0 → aeval_framework-0.3.0}/src/agent_eval/__init__.py +1 -1
  5. aeval_framework-0.3.0/src/agent_eval/api/app.py +218 -0
  6. {aeval_framework-0.1.0 → aeval_framework-0.3.0}/src/agent_eval/api/routes/runs.py +211 -57
  7. {aeval_framework-0.1.0 → aeval_framework-0.3.0}/src/agent_eval/api/routes/tasks.py +13 -3
  8. {aeval_framework-0.1.0 → aeval_framework-0.3.0}/src/agent_eval/api/standalone.py +13 -32
  9. aeval_framework-0.3.0/src/agent_eval/cli.py +1225 -0
  10. aeval_framework-0.3.0/src/agent_eval/core/__init__.py +99 -0
  11. aeval_framework-0.3.0/src/agent_eval/core/comparison.py +340 -0
  12. aeval_framework-0.3.0/src/agent_eval/core/contract.py +816 -0
  13. aeval_framework-0.3.0/src/agent_eval/core/discovery.py +149 -0
  14. aeval_framework-0.3.0/src/agent_eval/core/metrics.py +1060 -0
  15. aeval_framework-0.3.0/src/agent_eval/core/packaging.py +442 -0
  16. aeval_framework-0.3.0/src/agent_eval/core/pricing.py +152 -0
  17. aeval_framework-0.3.0/src/agent_eval/core/redaction.py +92 -0
  18. aeval_framework-0.3.0/src/agent_eval/core/runner.py +2579 -0
  19. aeval_framework-0.3.0/src/agent_eval/core/simulator.py +144 -0
  20. aeval_framework-0.3.0/src/agent_eval/core/types.py +1939 -0
  21. aeval_framework-0.3.0/src/agent_eval/examples/mock_runner.py +366 -0
  22. aeval_framework-0.3.0/src/agent_eval/graders/_evidence.py +243 -0
  23. aeval_framework-0.3.0/src/agent_eval/graders/_verdicts.py +37 -0
  24. {aeval_framework-0.1.0 → aeval_framework-0.3.0}/src/agent_eval/graders/artifact_check.py +59 -27
  25. {aeval_framework-0.1.0 → aeval_framework-0.3.0}/src/agent_eval/graders/code_based.py +46 -12
  26. {aeval_framework-0.1.0 → aeval_framework-0.3.0}/src/agent_eval/graders/human.py +25 -5
  27. aeval_framework-0.3.0/src/agent_eval/graders/metric.py +204 -0
  28. aeval_framework-0.3.0/src/agent_eval/graders/model_based.py +324 -0
  29. aeval_framework-0.3.0/src/agent_eval/graders/presentation_probes.py +392 -0
  30. aeval_framework-0.3.0/src/agent_eval/graders/state_check.py +280 -0
  31. {aeval_framework-0.1.0 → aeval_framework-0.3.0}/src/agent_eval/graders/step_level.py +46 -31
  32. aeval_framework-0.3.0/src/agent_eval/graders/tool_calls.py +161 -0
  33. aeval_framework-0.3.0/src/agent_eval/graders/transcript.py +166 -0
  34. {aeval_framework-0.1.0 → aeval_framework-0.3.0}/src/agent_eval/metrics/__init__.py +8 -0
  35. {aeval_framework-0.1.0 → aeval_framework-0.3.0}/src/agent_eval/metrics/answer_relevancy.py +4 -10
  36. aeval_framework-0.3.0/src/agent_eval/metrics/base.py +352 -0
  37. {aeval_framework-0.1.0 → aeval_framework-0.3.0}/src/agent_eval/metrics/batch_evaluation.py +12 -6
  38. {aeval_framework-0.1.0 → aeval_framework-0.3.0}/src/agent_eval/metrics/context_precision.py +4 -10
  39. {aeval_framework-0.1.0 → aeval_framework-0.3.0}/src/agent_eval/metrics/context_recall.py +4 -9
  40. {aeval_framework-0.1.0 → aeval_framework-0.3.0}/src/agent_eval/metrics/faithfulness.py +4 -10
  41. aeval_framework-0.3.0/src/agent_eval/metrics/llm_judge.py +179 -0
  42. {aeval_framework-0.1.0 → aeval_framework-0.3.0}/src/agent_eval/metrics/prompt_metric.py +6 -3
  43. aeval_framework-0.3.0/src/agent_eval/metrics/pytest_plugin.py +493 -0
  44. aeval_framework-0.3.0/src/agent_eval/metrics/report.py +251 -0
  45. aeval_framework-0.3.0/src/agent_eval/packs/__init__.py +8 -0
  46. aeval_framework-0.3.0/src/agent_eval/packs/starter/README.md +30 -0
  47. aeval_framework-0.3.0/src/agent_eval/packs/starter/manifest.json +8 -0
  48. aeval_framework-0.3.0/src/agent_eval/packs/starter/suite.yaml +29 -0
  49. {aeval_framework-0.1.0 → aeval_framework-0.3.0}/src/agent_eval/storage/memory.py +62 -6
  50. {aeval_framework-0.1.0 → aeval_framework-0.3.0}/src/agent_eval/storage/sqlite.py +135 -4
  51. aeval_framework-0.3.0/src/agent_eval/trace/__init__.py +56 -0
  52. aeval_framework-0.3.0/src/agent_eval/trace/mapping.py +243 -0
  53. aeval_framework-0.3.0/src/agent_eval/trace/normalize.py +572 -0
  54. aeval_framework-0.3.0/src/agent_eval/trace/observations.py +135 -0
  55. {aeval_framework-0.1.0 → aeval_framework-0.3.0}/src/agent_eval/trace/phoenix.py +57 -3
  56. aeval_framework-0.3.0/tests/conftest.py +225 -0
  57. aeval_framework-0.3.0/tests/smoke_achat_runner.py +94 -0
  58. aeval_framework-0.3.0/tests/test_agent_metric_catalog.py +474 -0
  59. {aeval_framework-0.1.0 → aeval_framework-0.3.0}/tests/test_api.py +165 -4
  60. aeval_framework-0.3.0/tests/test_baseline_comparison.py +250 -0
  61. aeval_framework-0.3.0/tests/test_builtin_graders.py +457 -0
  62. aeval_framework-0.3.0/tests/test_cli.py +706 -0
  63. aeval_framework-0.3.0/tests/test_collection_phases.py +644 -0
  64. aeval_framework-0.3.0/tests/test_conversation.py +772 -0
  65. aeval_framework-0.3.0/tests/test_discovery.py +267 -0
  66. aeval_framework-0.3.0/tests/test_e2e.py +437 -0
  67. aeval_framework-0.3.0/tests/test_environment_identity.py +339 -0
  68. {aeval_framework-0.1.0 → aeval_framework-0.3.0}/tests/test_eval_dataset_api.py +20 -14
  69. {aeval_framework-0.1.0 → aeval_framework-0.3.0}/tests/test_eval_metric_pipeline.py +77 -14
  70. {aeval_framework-0.1.0 → aeval_framework-0.3.0}/tests/test_eval_metrics_module.py +20 -14
  71. aeval_framework-0.3.0/tests/test_eval_pytest_plugin.py +540 -0
  72. {aeval_framework-0.1.0 → aeval_framework-0.3.0}/tests/test_eval_task_history.py +75 -0
  73. aeval_framework-0.3.0/tests/test_evidence_capture_and_storage.py +343 -0
  74. {aeval_framework-0.1.0 → aeval_framework-0.3.0}/tests/test_graders.py +27 -23
  75. aeval_framework-0.3.0/tests/test_holdout_and_canary.py +301 -0
  76. aeval_framework-0.3.0/tests/test_metric_roles_gates_agreement.py +734 -0
  77. aeval_framework-0.3.0/tests/test_metrics.py +699 -0
  78. aeval_framework-0.3.0/tests/test_packaging.py +436 -0
  79. aeval_framework-0.3.0/tests/test_power_analysis.py +133 -0
  80. aeval_framework-0.3.0/tests/test_presentation_probes.py +710 -0
  81. aeval_framework-0.3.0/tests/test_prompt_determinism.py +210 -0
  82. aeval_framework-0.3.0/tests/test_regrade_and_archive.py +792 -0
  83. aeval_framework-0.3.0/tests/test_run_storage_fields.py +261 -0
  84. {aeval_framework-0.1.0 → aeval_framework-0.3.0}/tests/test_runner.py +276 -28
  85. {aeval_framework-0.1.0 → aeval_framework-0.3.0}/tests/test_standalone_api.py +42 -3
  86. aeval_framework-0.3.0/tests/test_suite.py +438 -0
  87. aeval_framework-0.3.0/tests/test_termination_and_budgets.py +302 -0
  88. aeval_framework-0.3.0/tests/test_trace_normalization.py +427 -0
  89. aeval_framework-0.3.0/tests/test_trial_evidence.py +351 -0
  90. aeval_framework-0.3.0/tests/test_vocabulary_isolation.py +65 -0
  91. aeval_framework-0.1.0/.coverage +0 -0
  92. aeval_framework-0.1.0/.gitignore +0 -23
  93. aeval_framework-0.1.0/src/agent_eval/api/app.py +0 -82
  94. aeval_framework-0.1.0/src/agent_eval/cli.py +0 -455
  95. aeval_framework-0.1.0/src/agent_eval/core/__init__.py +0 -48
  96. aeval_framework-0.1.0/src/agent_eval/core/contract.py +0 -296
  97. aeval_framework-0.1.0/src/agent_eval/core/metrics.py +0 -184
  98. aeval_framework-0.1.0/src/agent_eval/core/runner.py +0 -868
  99. aeval_framework-0.1.0/src/agent_eval/core/types.py +0 -227
  100. aeval_framework-0.1.0/src/agent_eval/examples/mock_runner.py +0 -195
  101. aeval_framework-0.1.0/src/agent_eval/graders/metric.py +0 -142
  102. aeval_framework-0.1.0/src/agent_eval/graders/model_based.py +0 -179
  103. aeval_framework-0.1.0/src/agent_eval/graders/state_check.py +0 -106
  104. aeval_framework-0.1.0/src/agent_eval/graders/tool_calls.py +0 -102
  105. aeval_framework-0.1.0/src/agent_eval/graders/transcript.py +0 -86
  106. aeval_framework-0.1.0/src/agent_eval/metrics/base.py +0 -155
  107. aeval_framework-0.1.0/src/agent_eval/metrics/llm_judge.py +0 -100
  108. aeval_framework-0.1.0/src/agent_eval/metrics/pytest_plugin.py +0 -308
  109. aeval_framework-0.1.0/src/agent_eval/metrics/report.py +0 -149
  110. aeval_framework-0.1.0/src/agent_eval/trace/__init__.py +0 -16
  111. aeval_framework-0.1.0/tests/conftest.py +0 -9
  112. aeval_framework-0.1.0/tests/test_builtin_graders.py +0 -298
  113. aeval_framework-0.1.0/tests/test_cli.py +0 -280
  114. aeval_framework-0.1.0/tests/test_e2e.py +0 -243
  115. aeval_framework-0.1.0/tests/test_eval_pytest_plugin.py +0 -218
  116. aeval_framework-0.1.0/tests/test_metrics.py +0 -130
  117. aeval_framework-0.1.0/tests/test_suite.py +0 -209
  118. {aeval_framework-0.1.0 → aeval_framework-0.3.0}/README.md +0 -0
  119. {aeval_framework-0.1.0 → aeval_framework-0.3.0}/src/agent_eval/api/__init__.py +0 -0
  120. {aeval_framework-0.1.0 → aeval_framework-0.3.0}/src/agent_eval/api/events.py +0 -0
  121. {aeval_framework-0.1.0 → aeval_framework-0.3.0}/src/agent_eval/api/routes/__init__.py +0 -0
  122. {aeval_framework-0.1.0 → aeval_framework-0.3.0}/src/agent_eval/api/routes/datasets.py +0 -0
  123. {aeval_framework-0.1.0 → aeval_framework-0.3.0}/src/agent_eval/api/routes/graders.py +0 -0
  124. {aeval_framework-0.1.0 → aeval_framework-0.3.0}/src/agent_eval/api/routes/metrics.py +0 -0
  125. {aeval_framework-0.1.0 → aeval_framework-0.3.0}/src/agent_eval/api/routes/suites.py +0 -0
  126. {aeval_framework-0.1.0 → aeval_framework-0.3.0}/src/agent_eval/core/suite.py +0 -0
  127. {aeval_framework-0.1.0 → aeval_framework-0.3.0}/src/agent_eval/dataset/__init__.py +0 -0
  128. {aeval_framework-0.1.0 → aeval_framework-0.3.0}/src/agent_eval/dataset/models.py +0 -0
  129. {aeval_framework-0.1.0 → aeval_framework-0.3.0}/src/agent_eval/dataset/quality.py +0 -0
  130. {aeval_framework-0.1.0 → aeval_framework-0.3.0}/src/agent_eval/dataset/sources/__init__.py +0 -0
  131. {aeval_framework-0.1.0 → aeval_framework-0.3.0}/src/agent_eval/dataset/sources/llm_generator.py +0 -0
  132. {aeval_framework-0.1.0 → aeval_framework-0.3.0}/src/agent_eval/dataset/sources/manual.py +0 -0
  133. {aeval_framework-0.1.0 → aeval_framework-0.3.0}/src/agent_eval/dataset/sources/regression.py +0 -0
  134. {aeval_framework-0.1.0 → aeval_framework-0.3.0}/src/agent_eval/dataset/sources/trace_mining.py +0 -0
  135. {aeval_framework-0.1.0 → aeval_framework-0.3.0}/src/agent_eval/dataset/storage.py +0 -0
  136. {aeval_framework-0.1.0 → aeval_framework-0.3.0}/src/agent_eval/dataset/version.py +0 -0
  137. {aeval_framework-0.1.0 → aeval_framework-0.3.0}/src/agent_eval/examples/__init__.py +0 -0
  138. {aeval_framework-0.1.0 → aeval_framework-0.3.0}/src/agent_eval/examples/basic_usage.py +0 -0
  139. {aeval_framework-0.1.0 → aeval_framework-0.3.0}/src/agent_eval/graders/__init__.py +0 -0
  140. {aeval_framework-0.1.0 → aeval_framework-0.3.0}/src/agent_eval/metrics/synthetic_data.py +0 -0
  141. {aeval_framework-0.1.0 → aeval_framework-0.3.0}/src/agent_eval/storage/__init__.py +0 -0
  142. {aeval_framework-0.1.0 → aeval_framework-0.3.0}/tests/test_eval_batch_evaluation.py +0 -0
  143. {aeval_framework-0.1.0 → aeval_framework-0.3.0}/tests/test_eval_dataset_sources.py +0 -0
  144. {aeval_framework-0.1.0 → aeval_framework-0.3.0}/tests/test_eval_dataset_storage.py +0 -0
  145. {aeval_framework-0.1.0 → aeval_framework-0.3.0}/tests/test_eval_metrics_api.py +0 -0
  146. {aeval_framework-0.1.0 → aeval_framework-0.3.0}/tests/test_eval_prompt_metric.py +0 -0
  147. {aeval_framework-0.1.0 → aeval_framework-0.3.0}/tests/test_eval_report.py +0 -0
  148. {aeval_framework-0.1.0 → aeval_framework-0.3.0}/tests/test_import_isolation.py +0 -0
  149. {aeval_framework-0.1.0 → aeval_framework-0.3.0}/tests/test_sse.py +0 -0
@@ -0,0 +1,39 @@
1
+ # Python
2
+ __pycache__/
3
+ *.py[cod]
4
+ *.egg-info/
5
+ .venv/
6
+ dist/
7
+ build/
8
+ .pytest_cache/
9
+ .ruff_cache/
10
+ *.db
11
+ *.sqlite3
12
+ .coverage
13
+
14
+ # Node / Next.js (dashboard)
15
+ node_modules/
16
+ .next/
17
+ out/
18
+ *.tsbuildinfo
19
+
20
+ # Env & OS
21
+ .env
22
+ .env.*
23
+ .DS_Store
24
+ Thumbs.db
25
+
26
+ # Editor-local state (Qoder)
27
+ .qoder/
28
+
29
+ # AI tool state
30
+ .claude/
31
+ .codex/
32
+ .meituan-catpaw/
33
+
34
+ # Run artifacts & scratch notes
35
+ .live-run-output.txt
36
+ testlist.txt
37
+
38
+ # Personal (never commit)
39
+ docs/eval-harness-interview-prep.md
@@ -1,11 +1,11 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: aeval-framework
3
- Version: 0.1.0
3
+ Version: 0.3.0
4
4
  Summary: Aeval — open-source agent evaluation framework driven by OTel traces (suites, graders, metrics, storage, API, CLI)
5
5
  Project-URL: Homepage, https://github.com/QiYuyyds/Aeval
6
6
  Project-URL: Repository, https://github.com/QiYuyyds/Aeval
7
7
  Project-URL: Documentation, https://github.com/QiYuyyds/Aeval/tree/main/docs
8
- Project-URL: Changelog, https://github.com/QiYuyyds/Aeval/releases
8
+ Project-URL: Changelog, https://github.com/QiYuyyds/Aeval/blob/main/CHANGELOG.md
9
9
  Author: Aeval contributors
10
10
  License: MIT
11
11
  Keywords: agents,evaluation,llm,otel,testing,tracing
@@ -2,7 +2,7 @@
2
2
  # Distribution name: aeval-framework (PyPI). Python module: agent_eval, CLI: eval-suite.
3
3
  # (`agent-eval` on PyPI belongs to an unrelated project — UK AISI agenteval.)
4
4
  name = "aeval-framework"
5
- version = "0.1.0"
5
+ version = "0.3.0"
6
6
  description = "Aeval — open-source agent evaluation framework driven by OTel traces (suites, graders, metrics, storage, API, CLI)"
7
7
  readme = "README.md"
8
8
  requires-python = ">=3.11"
@@ -52,7 +52,7 @@ eval-suite = "agent_eval.cli:main"
52
52
  Homepage = "https://github.com/QiYuyyds/Aeval"
53
53
  Repository = "https://github.com/QiYuyyds/Aeval"
54
54
  Documentation = "https://github.com/QiYuyyds/Aeval/tree/main/docs"
55
- Changelog = "https://github.com/QiYuyyds/Aeval/releases"
55
+ Changelog = "https://github.com/QiYuyyds/Aeval/blob/main/CHANGELOG.md"
56
56
 
57
57
  [build-system]
58
58
  requires = ["hatchling"]
@@ -60,6 +60,9 @@ build-backend = "hatchling.build"
60
60
 
61
61
  [tool.hatch.build.targets.wheel]
62
62
  packages = ["src/agent_eval"]
63
+ # src/agent_eval/packs/** (含 starter pack 的 suite.yaml/manifest.json) 作为
64
+ # package data 随 wheel 分发 —— `eval-suite run demo` 的零 setup 首跑依赖它;
65
+ # 仓库 packs/starter/ 与之同源, tests/test_packaging.py 钉住两侧一致。
63
66
 
64
67
  [tool.pytest.ini_options]
65
68
  asyncio_mode = "auto"
@@ -11,4 +11,4 @@ Usage:
11
11
  result = await runner.run_suite(suite)
12
12
  """
13
13
 
14
- __version__ = "0.1.0"
14
+ __version__ = "0.3.0"
@@ -0,0 +1,218 @@
1
+ """
2
+ FastAPI application factory for Aeval.
3
+
4
+ Creates a FastAPI app with all eval routes mounted.
5
+ Can be used standalone or mounted in an existing app.
6
+
7
+ Usage:
8
+ # Standalone
9
+ app = create_app(runner=my_runner)
10
+
11
+ # Mounted in existing FastAPI app
12
+ from fastapi import FastAPI
13
+ app = FastAPI()
14
+ eval_app = create_app(runner=my_runner)
15
+ app.mount("/api/eval", eval_app)
16
+ """
17
+
18
+ from __future__ import annotations
19
+
20
+ from typing import Any
21
+
22
+ from fastapi import FastAPI
23
+
24
+ from agent_eval.api.routes import datasets, graders, metrics, runs, suites, tasks
25
+ from agent_eval.core.metrics import MIN_ALIGNED_RATINGS_FOR_AGREEMENT
26
+ from agent_eval.core.runner import EvalRunner
27
+ from agent_eval.core.types import (
28
+ DEFAULT_BOOTSTRAP_ROUNDS,
29
+ DEFAULT_CONFIDENCE_LEVEL,
30
+ DEFAULT_INVALID_RATIO_LIMIT,
31
+ MIN_VALID_TRIALS_FOR_SATURATION,
32
+ STATISTICS_VERSION,
33
+ )
34
+ from agent_eval.graders import get_grader_catalog
35
+ from agent_eval.trace.mapping import (
36
+ ATTRIBUTE_MAPPING_VERSION,
37
+ OTEL_GENAI_SPEC_VERSION,
38
+ VOCABULARY_OTEL_GENAI,
39
+ VOCABULARY_SPEC_VERSIONS,
40
+ known_vocabularies,
41
+ )
42
+
43
+ # Global runner reference (set by create_app)
44
+ _runner: EvalRunner | None = None
45
+
46
+
47
+ def _get_runner() -> EvalRunner | None:
48
+ """Get the global EvalRunner instance"""
49
+ return _runner
50
+
51
+
52
+ def set_runner(runner: EvalRunner | None) -> None:
53
+ """Set the global EvalRunner instance.
54
+
55
+ Used by the host app to inject the real runner during async startup
56
+ (e.g. main.py lifespan → eval_integration.config.create_aeval_runner).
57
+ """
58
+ global _runner
59
+ _runner = runner
60
+
61
+
62
+ DISTRIBUTION_NAME = "aeval-framework" # PyPI 发行包名 (Python 模块为 agent_eval)
63
+
64
+
65
+ def package_version() -> str:
66
+ """包版本: 优先取已安装元数据, 源码直跑时回退模块常量。"""
67
+ from importlib.metadata import PackageNotFoundError
68
+ from importlib.metadata import version as _metadata_version
69
+
70
+ try:
71
+ return _metadata_version(DISTRIBUTION_NAME)
72
+ except PackageNotFoundError:
73
+ from agent_eval import __version__
74
+
75
+ return __version__
76
+
77
+
78
+ def statistics_defaults() -> dict[str, Any]:
79
+ """统计口径版本与 D7 数值默认值 (经元信息接口公布, 供调用方判断可比性)。"""
80
+ return {
81
+ "version": STATISTICS_VERSION,
82
+ "confidence_level": DEFAULT_CONFIDENCE_LEVEL,
83
+ "bootstrap_rounds": DEFAULT_BOOTSTRAP_ROUNDS,
84
+ "min_valid_trials_for_saturation": MIN_VALID_TRIALS_FOR_SATURATION,
85
+ "gate_invalid_ratio_limit": DEFAULT_INVALID_RATIO_LIMIT,
86
+ "min_aligned_ratings_for_agreement": MIN_ALIGNED_RATINGS_FOR_AGREEMENT,
87
+ }
88
+
89
+
90
+ def meta_payload(
91
+ api_prefix: str = "",
92
+ endpoints: list[str] | None = None,
93
+ version: str | None = None,
94
+ ) -> dict[str, Any]:
95
+ """元信息响应体: 版本 + 统计口径 + 能力清单。
96
+
97
+ 同一大版本内响应结构向后兼容, 但统计口径的数值语义可能变化 —— 口径版本
98
+ 必须显式公布, 调用方才能判断两个 run 是否可直接比较。
99
+ """
100
+ # 扩展点发现与 CLI 同源 (core/discovery.discover_extensions):
101
+ # 两处给出的自定义判据/环境/模拟器名字集合必须一致, 不允许
102
+ # 「接口看得见、命令行用不上」的分歧 (spec: cli)。
103
+ from agent_eval.core.discovery import discover_extensions
104
+
105
+ try:
106
+ discovered = discover_extensions().catalog()
107
+ except Exception: # noqa: BLE001 — 元数据异常不阻断 /meta
108
+ discovered = {}
109
+ discovered_graders = [e["name"] for e in discovered.get("graders", [])]
110
+ builtin_graders = [g["name"] for g in get_grader_catalog()]
111
+ # 发现的自定义判据追加在内置之后 (去重); 与 CLI extensions 清单同一份来源
112
+ graders = sorted(builtin_graders) + sorted(
113
+ n for n in discovered_graders if n not in builtin_graders
114
+ )
115
+ return {
116
+ "name": "Aeval",
117
+ "package": DISTRIBUTION_NAME,
118
+ "version": version or package_version(),
119
+ "api_prefix": api_prefix,
120
+ "endpoints": endpoints or ["/suites", "/tasks", "/runs", "/compare", "/graders",
121
+ "/datasets", "/metrics", "/health"],
122
+ "statistics": statistics_defaults(),
123
+ "evidence": {
124
+ "spec_version": OTEL_GENAI_SPEC_VERSION,
125
+ "mapping_version": ATTRIBUTE_MAPPING_VERSION,
126
+ # default_mapping(vocabulary=...) 的可填值在这里枚举: 调用方不需要读
127
+ # 源码猜词汇, 也不需要为了发现一个字符串去 import 内部模块。
128
+ "vocabularies": {
129
+ "default": VOCABULARY_OTEL_GENAI,
130
+ "known": list(known_vocabularies()),
131
+ "spec_versions": dict(VOCABULARY_SPEC_VERSIONS),
132
+ },
133
+ "tool_arguments_captured_by_default": False,
134
+ "model_content_captured_by_default": False,
135
+ "capture_is_one_declaration": True,
136
+ },
137
+ "capabilities": {
138
+ "graders": graders,
139
+ "extensions": discovered,
140
+ "user_simulation": True,
141
+ "run_event_injection": True,
142
+ "judgment_moments": ["at_end", "not_at_end", "any_time", "after_last_event"],
143
+ "environment_identity_boundary": True,
144
+ "storage": ["memory", "sqlite"],
145
+ "trace_providers": ["phoenix (optional, lazily imported)"],
146
+ "sse": True,
147
+ "datasets": True,
148
+ "metrics": True,
149
+ "validity_verdicts": True,
150
+ "confidence_intervals": True,
151
+ "normalized_trace_observations": True,
152
+ "termination_reasons": True,
153
+ "cost_axis": True,
154
+ "evidence_provenance_levels": True,
155
+ "deferred_grading": True,
156
+ # ④ agent 指标目录: 证据感知测量上下文 / 诊断-判分角色轴 / κ/α / 门
157
+ "evidence_aware_metrics": True,
158
+ "metric_roles": ["diagnostic", "judging"],
159
+ "inter_rater_agreement": ["cohen_kappa", "krippendorff_alpha"],
160
+ "reward_basis_gates": True,
161
+ # 重评分本期只有库层入口 (EvalRunner.regrade_run)。它牵涉同一大版本的
162
+ # 响应结构兼容承诺, 暴露面另立变更 —— 列出来免得调用方以为 /runs 上能调。
163
+ "regrade_over_http": False,
164
+ "regrade_over_cli": False,
165
+ },
166
+ "not_exposed": {
167
+ "regrade": (
168
+ "证据归档与重评分已落地 (EvalRunner.regrade_run / verdict_drift), "
169
+ "但 HTTP 与 CLI 入口本期未提供"
170
+ )
171
+ },
172
+ }
173
+
174
+
175
+ def create_app(runner: EvalRunner | None = None) -> FastAPI:
176
+ """
177
+ Create a FastAPI app with Aeval routes.
178
+
179
+ Args:
180
+ runner: EvalRunner instance. If None, routes will return 503.
181
+
182
+ Returns:
183
+ FastAPI application
184
+ """
185
+ app = FastAPI(
186
+ title="Aeval API",
187
+ version="0.1.0",
188
+ description="Agent Evaluation Framework API",
189
+ )
190
+
191
+ # Store runner in app state and global reference
192
+ global _runner
193
+ _runner = runner
194
+ app.state.runner = runner
195
+
196
+ # Include routers
197
+ app.include_router(suites.router, prefix="/suites", tags=["suites"])
198
+ app.include_router(tasks.router, prefix="/tasks", tags=["tasks"])
199
+ app.include_router(runs.router, prefix="/runs", tags=["runs"])
200
+ # compare 挂在 /compare (spec §REST API), 不带 /runs 前缀
201
+ app.include_router(runs.compare_router, tags=["compare"])
202
+ app.include_router(graders.router, prefix="/graders", tags=["graders"])
203
+ # 数据集管理 (change ③: 数据集构建闭环 — CRUD/导入/挖掘/生成/质量/to-suite)
204
+ app.include_router(datasets.router, prefix="/datasets", tags=["datasets"])
205
+ # 批量评测 (change ④: 对已有输出直接批量打分 — POST /metrics/batch)
206
+ app.include_router(metrics.router, prefix="/metrics", tags=["metrics"])
207
+
208
+ @app.get("/health")
209
+ async def health():
210
+ # 动态读取全局 runner — host app 会在 startup 阶段注入真实 runner
211
+ return {"status": "ok", "runner_configured": _get_runner() is not None}
212
+
213
+ @app.get("/meta")
214
+ async def meta():
215
+ """寄宿形态的元信息接口 (挂载前缀由宿主决定, 例如 /api/eval/meta)。"""
216
+ return meta_payload()
217
+
218
+ return app
@@ -29,6 +29,9 @@ from pydantic import BaseModel, Field
29
29
  from sse_starlette.sse import EventSourceResponse
30
30
 
31
31
  from agent_eval.api.events import run_event_bus
32
+ from agent_eval.core.comparison import runs_comparable
33
+ from agent_eval.core.metrics import classify_trial, trial_invalid_reason
34
+ from agent_eval.core.types import STATISTICS_VERSION, TrialVerdict
32
35
 
33
36
  router = APIRouter()
34
37
 
@@ -60,6 +63,9 @@ async def list_runs(suite_name: str | None = None, limit: int = 50):
60
63
  "started_at": r.started_at,
61
64
  "completed_at": r.completed_at,
62
65
  "duration_ms": r.duration_ms,
66
+ "statistics_version": r.statistics_version,
67
+ "evidence": r.evidence.model_dump() if r.evidence else None,
68
+ "regrade": _regrade_entry(r),
63
69
  "task_count": len(r.trials),
64
70
  "summary": r.summary.model_dump() if r.summary else None,
65
71
  }
@@ -98,7 +104,12 @@ async def create_run(request: CreateRunRequest):
98
104
  from agent_eval.core.types import RunResult
99
105
 
100
106
  await runner.storage.save_run(
101
- RunResult(run_id=run_id, suite_name=suite.name, status="pending")
107
+ RunResult(
108
+ run_id=run_id,
109
+ suite_name=suite.name,
110
+ status="pending",
111
+ statistics_version=STATISTICS_VERSION,
112
+ )
102
113
  )
103
114
 
104
115
  async def _run():
@@ -164,6 +175,10 @@ async def get_run(run_id: str):
164
175
  "completed_at": run.completed_at,
165
176
  "duration_ms": run.duration_ms,
166
177
  "error": run.error,
178
+ "statistics_version": run.statistics_version,
179
+ "evidence": run.evidence.model_dump() if run.evidence else None,
180
+ # 本期只开库层重评分入口: 这里如实标能不能重评与原因, 不提供 HTTP 触发
181
+ "regrade": _regrade_entry(run),
167
182
  "trials": {
168
183
  task_id: [
169
184
  {
@@ -171,14 +186,44 @@ async def get_run(run_id: str):
171
186
  "trace_id": t.trace_id,
172
187
  "success": t.success,
173
188
  "score": t.avg_score(),
189
+ # trial 总分 (权重/策略/乘性门塌缩后的值; None = 该字段落盘前的历史 run)
190
+ "synthesized_score": t.synthesized_score,
174
191
  "duration_ms": t.duration_ms,
175
192
  "error": t.error,
193
+ "verdict": t.verdict.value,
194
+ "invalid_reason": t.invalid_reason.value if t.invalid_reason else None,
195
+ "termination_reason": (
196
+ t.termination_reason.value if t.termination_reason else None
197
+ ),
198
+ "metrics": t.metrics,
199
+ "evidence_gaps": [gap.model_dump() for gap in t.evidence_gaps],
200
+ "unrecognized_attributes": t.unrecognized_attributes,
201
+ # 结论的分量: 它依据的最弱一级证据 + 证据是否已归档
202
+ "weakest_evidence": (
203
+ t.weakest_evidence.value if t.weakest_evidence else None
204
+ ),
205
+ "evidence_archived": t.evidence_archived,
176
206
  "grader_results": [
177
207
  {
178
208
  "grader_name": gr.grader_name,
179
209
  "score": gr.score,
180
210
  "passed": gr.passed,
181
211
  "explanation": gr.explanation,
212
+ "verdict": gr.verdict.value,
213
+ "invalid_reason": (
214
+ gr.invalid_reason.value if gr.invalid_reason else None
215
+ ),
216
+ "evidence_levels": [
217
+ level.value for level in gr.evidence_levels
218
+ ],
219
+ "judgment_moment": (
220
+ gr.judgment_moment.value if gr.judgment_moment else None
221
+ ),
222
+ "subject_only": gr.subject_only,
223
+ # 门结果: 因子 / 是否乘入总分 / 未生效原因 (spec: graders)
224
+ "gate_factor": gr.gate_factor,
225
+ "gate_applied": gr.gate_applied,
226
+ "gate_reason": gr.gate_reason,
182
227
  }
183
228
  for gr in t.grader_results
184
229
  ],
@@ -191,6 +236,14 @@ async def get_run(run_id: str):
191
236
  }
192
237
 
193
238
 
239
+ def _regrade_entry(run) -> dict[str, Any]:
240
+ """能不能重评分 + 为什么不能 (与库层 ``regrade_run`` 共用同一份判断)。"""
241
+ from agent_eval.core.runner import regrade_state
242
+
243
+ possible, reason = regrade_state(run)
244
+ return {"available": possible, "reason": reason, "exposed_over_http": False}
245
+
246
+
194
247
  @router.delete("/{run_id}")
195
248
  async def delete_run(run_id: str):
196
249
  """删除运行"""
@@ -204,6 +257,10 @@ async def delete_run(run_id: str):
204
257
  if not deleted:
205
258
  raise HTTPException(status_code=404, detail=f"Run '{run_id}' not found")
206
259
 
260
+ # 派生物一并清除: 内容寻址的评分缓存可能缓存了该 run 采集到的证据内容
261
+ runner.evict_run_cache(run_id)
262
+ _background_tasks.pop(run_id, None)
263
+
207
264
  return {"deleted": True}
208
265
 
209
266
 
@@ -410,11 +467,13 @@ async def submit_human_score(run_id: str, request: HumanScoreRequest):
410
467
  "threshold", threshold
411
468
  )
412
469
 
413
- # 更新已存的 GraderResult
470
+ # 更新已存的 GraderResult (评分回传 → 离开 pending 通道)
414
471
  grader_result.score = request.score
415
472
  grader_result.passed = request.score >= threshold
416
473
  grader_result.explanation = request.explanation or "人工评分"
417
474
  grader_result.confidence = 1.0
475
+ grader_result.verdict = TrialVerdict.VALID
476
+ grader_result.invalid_reason = None
418
477
  grader_result.details = {
419
478
  **grader_result.details,
420
479
  "status": "scored",
@@ -425,6 +484,11 @@ async def submit_human_score(run_id: str, request: HumanScoreRequest):
425
484
  # 重算 trial 成功状态与 run 汇总 (含该 task 汇总)
426
485
  if task is not None:
427
486
  trial.success = runner._compute_trial_success(task, trial.grader_results)
487
+ trial.synthesized_score = runner._trial_weighted_score(task, trial)
488
+ trial.verdict = classify_trial(trial)
489
+ trial.invalid_reason = trial_invalid_reason(trial)
490
+ if run.statistics_version is None:
491
+ run.statistics_version = STATISTICS_VERSION
428
492
  run.summary = runner._compute_summary(run, suite) if suite is not None else run.summary
429
493
  await runner.storage.save_run(run)
430
494
 
@@ -436,6 +500,7 @@ async def submit_human_score(run_id: str, request: HumanScoreRequest):
436
500
  "score": grader_result.score,
437
501
  "passed": grader_result.passed,
438
502
  "trial_success": trial.success,
503
+ "trial_verdict": trial.verdict.value,
439
504
  "summary": run.summary.model_dump() if run.summary else None,
440
505
  }
441
506
 
@@ -489,85 +554,174 @@ async def compare_runs(request: CompareRequest):
489
554
 
490
555
 
491
556
  def _build_comparison(run_a, run_b) -> dict[str, Any]:
492
- """构建两次运行的对比"""
557
+ """构建两次运行的对比 (口径版本不同则标注不可比; 区间重叠则不判方向)。
558
+
559
+ 既有字段 (`a`/`b`/`delta`/`regressions`/`improvements`/`tasks`) 的名称与类型
560
+ 保持不变; 分母为 0 时 `a`/`b`/`delta` 为 null 而非 0.0。
561
+ """
493
562
  summary_a = run_a.summary
494
563
  summary_b = run_b.summary
495
564
 
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}"] = {
565
+ # 可比性前置与 CLI / 基线门同源 (core.comparison.runs_comparable)
566
+ comparable, not_comparable_reason = runs_comparable(run_a, run_b)
567
+ version_a = getattr(run_a, "statistics_version", None)
568
+ version_b = getattr(run_b, "statistics_version", None)
569
+ evidence_a = getattr(run_a, "evidence", None)
570
+ evidence_b = getattr(run_b, "evidence", None)
571
+
572
+ all_k_values: set[int] = set()
573
+ for summary in (summary_a, summary_b):
574
+ for k in (summary.pass_at_k or {}):
575
+ all_k_values.add(int(k))
576
+
577
+ def _entry(
578
+ a_val: float | None,
579
+ b_val: float | None,
580
+ a_ci: tuple[float | None, float | None] | None,
581
+ b_ci: tuple[float | None, float | None] | None,
582
+ extrapolated: bool,
583
+ ) -> dict[str, Any]:
584
+ overlapping = _intervals_overlap(a_ci, b_ci)
585
+ return {
508
586
  "a": a_val,
509
587
  "b": b_val,
510
- "delta": round(b_val - a_val, 4),
588
+ "delta": _delta(a_val, b_val),
589
+ "a_ci": _ci_list(a_ci),
590
+ "b_ci": _ci_list(b_ci),
591
+ "intervals_overlap": overlapping,
592
+ "significant": (
593
+ None if overlapping is None else (comparable and not overlapping)
594
+ ),
595
+ "extrapolated": extrapolated,
596
+ "comparable": comparable,
511
597
  }
512
598
 
599
+ def _k_map(summary, field: str) -> dict:
600
+ raw = getattr(summary, field, None) or {}
601
+ return {int(k): v for k, v in raw.items()}
602
+
603
+ pass_at_k_comparison = {}
513
604
  pass_power_k_comparison = {}
514
605
  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
- }
606
+ est_a = _k_map(summary_a, "estimates").get(k)
607
+ est_b = _k_map(summary_b, "estimates").get(k)
608
+ pass_at_k_comparison[f"pass_at_{k}"] = _entry(
609
+ est_a.value if est_a else None,
610
+ est_b.value if est_b else None,
611
+ _est_ci(est_a),
612
+ _est_ci(est_b),
613
+ bool(est_a and est_a.extrapolated) or bool(est_b and est_b.extrapolated),
614
+ )
615
+
616
+ pow_a = _k_map(summary_a, "power_estimates").get(k)
617
+ pow_b = _k_map(summary_b, "power_estimates").get(k)
618
+ pass_power_k_comparison[f"pass_power_{k}"] = _entry(
619
+ pow_a.value if pow_a else None,
620
+ pow_b.value if pow_b else None,
621
+ _est_ci(pow_a),
622
+ _est_ci(pow_b),
623
+ bool(pow_a and pow_a.extrapolated) or bool(pow_b and pow_b.extrapolated),
624
+ )
522
625
 
523
- # 逐 task 对比
626
+ dist_a = getattr(summary_a, "score_distribution", None)
627
+ dist_b = getattr(summary_b, "score_distribution", None)
628
+ avg_entry = _entry(
629
+ summary_a.avg_score,
630
+ summary_b.avg_score,
631
+ _dist_ci(dist_a),
632
+ _dist_ci(dist_b),
633
+ False,
634
+ )
635
+
636
+ # 逐 task 对比: 只有区间不重叠才判为退化/提升 (D4)
524
637
  task_a_map = {ts.task_id: ts for ts in summary_a.task_summaries}
525
638
  task_b_map = {ts.task_id: ts for ts in summary_b.task_summaries}
526
639
 
527
640
  all_task_ids = set(task_a_map.keys()) | set(task_b_map.keys())
528
- regressions = []
529
- improvements = []
530
- task_comparisons = {}
641
+ regressions: list[dict[str, Any]] = []
642
+ improvements: list[dict[str, Any]] = []
643
+ task_comparisons: dict[str, Any] = {}
531
644
 
532
645
  for task_id in sorted(all_task_ids):
533
646
  ts_a = task_a_map.get(task_id)
534
647
  ts_b = task_b_map.get(task_id)
648
+ if not (ts_a and ts_b):
649
+ continue
650
+
651
+ a_score = ts_a.avg_score
652
+ b_score = ts_b.avg_score
653
+ delta = _delta(a_score, b_score)
654
+ overlapping = _intervals_overlap(
655
+ _dist_ci(ts_a.score_distribution), _dist_ci(ts_b.score_distribution)
656
+ )
657
+ significant = (
658
+ comparable and overlapping is False and delta is not None and abs(delta) > 0.1
659
+ )
660
+ task_comparisons[task_id] = {
661
+ "a": a_score,
662
+ "b": b_score,
663
+ "delta": delta,
664
+ "a_ci": _ci_list(_dist_ci(ts_a.score_distribution)),
665
+ "b_ci": _ci_list(_dist_ci(ts_b.score_distribution)),
666
+ "intervals_overlap": overlapping,
667
+ "significant": (None if overlapping is None else (
668
+ comparable and overlapping is False
669
+ )),
670
+ "comparable": comparable,
671
+ }
535
672
 
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
- })
673
+ if not significant:
674
+ continue
675
+ row = {"task_id": task_id, "a": a_score, "b": b_score, "delta": delta}
676
+ if delta < 0:
677
+ regressions.append(row)
678
+ else:
679
+ improvements.append(row)
561
680
 
562
681
  return {
563
682
  "pass_at_k": pass_at_k_comparison,
564
683
  "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
- },
684
+ "avg_score": avg_entry,
570
685
  "regressions": regressions,
571
686
  "improvements": improvements,
572
687
  "tasks": task_comparisons,
688
+ "statistics_version": {"a": version_a, "b": version_b},
689
+ "evidence_boundary": {
690
+ "a": evidence_a.model_dump() if evidence_a else None,
691
+ "b": evidence_b.model_dump() if evidence_b else None,
692
+ },
693
+ "comparable": comparable,
694
+ "not_comparable_reason": not_comparable_reason,
573
695
  }
696
+
697
+
698
+ def _delta(a: float | None, b: float | None) -> float | None:
699
+ if a is None or b is None:
700
+ return None
701
+ return round(b - a, 4)
702
+
703
+
704
+ def _est_ci(est) -> tuple[float | None, float | None] | None:
705
+ if est is None or est.p_lower_bound is None or est.p_upper_bound is None:
706
+ return None
707
+ return (est.p_lower_bound, est.p_upper_bound)
708
+
709
+
710
+ def _dist_ci(dist) -> tuple[float | None, float | None] | None:
711
+ if dist is None or dist.ci_low is None or dist.ci_high is None:
712
+ return None
713
+ return (dist.ci_low, dist.ci_high)
714
+
715
+
716
+ def _ci_list(ci: tuple[float | None, float | None] | None) -> list[float] | None:
717
+ return [ci[0], ci[1]] if ci else None
718
+
719
+
720
+ def _intervals_overlap(
721
+ a: tuple[float | None, float | None] | None,
722
+ b: tuple[float | None, float | None] | None,
723
+ ) -> bool | None:
724
+ """两侧区间是否重叠 (D4)。任一侧无区间 → None (无法判定)。"""
725
+ if a is None or b is None:
726
+ return None
727
+ return not (a[1] < b[0] or b[1] < a[0])