aeval-framework 0.2.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 (127) hide show
  1. {aeval_framework-0.2.0 → aeval_framework-0.3.0}/.gitignore +13 -0
  2. {aeval_framework-0.2.0 → aeval_framework-0.3.0}/PKG-INFO +2 -2
  3. {aeval_framework-0.2.0 → aeval_framework-0.3.0}/pyproject.toml +5 -2
  4. {aeval_framework-0.2.0 → aeval_framework-0.3.0}/src/agent_eval/__init__.py +1 -1
  5. {aeval_framework-0.2.0 → aeval_framework-0.3.0}/src/agent_eval/api/app.py +28 -1
  6. {aeval_framework-0.2.0 → aeval_framework-0.3.0}/src/agent_eval/api/routes/runs.py +11 -36
  7. {aeval_framework-0.2.0 → aeval_framework-0.3.0}/src/agent_eval/api/routes/tasks.py +2 -1
  8. {aeval_framework-0.2.0 → aeval_framework-0.3.0}/src/agent_eval/cli.py +569 -107
  9. aeval_framework-0.3.0/src/agent_eval/core/comparison.py +340 -0
  10. {aeval_framework-0.2.0 → aeval_framework-0.3.0}/src/agent_eval/core/contract.py +281 -0
  11. aeval_framework-0.3.0/src/agent_eval/core/discovery.py +149 -0
  12. {aeval_framework-0.2.0 → aeval_framework-0.3.0}/src/agent_eval/core/metrics.py +389 -0
  13. aeval_framework-0.3.0/src/agent_eval/core/packaging.py +442 -0
  14. {aeval_framework-0.2.0 → aeval_framework-0.3.0}/src/agent_eval/core/runner.py +695 -43
  15. aeval_framework-0.3.0/src/agent_eval/core/simulator.py +144 -0
  16. {aeval_framework-0.2.0 → aeval_framework-0.3.0}/src/agent_eval/core/types.py +679 -4
  17. {aeval_framework-0.2.0 → aeval_framework-0.3.0}/src/agent_eval/graders/_evidence.py +5 -1
  18. {aeval_framework-0.2.0 → aeval_framework-0.3.0}/src/agent_eval/graders/metric.py +28 -19
  19. {aeval_framework-0.2.0 → aeval_framework-0.3.0}/src/agent_eval/graders/model_based.py +100 -12
  20. aeval_framework-0.3.0/src/agent_eval/graders/presentation_probes.py +392 -0
  21. {aeval_framework-0.2.0 → aeval_framework-0.3.0}/src/agent_eval/graders/state_check.py +19 -2
  22. {aeval_framework-0.2.0 → aeval_framework-0.3.0}/src/agent_eval/metrics/__init__.py +8 -0
  23. {aeval_framework-0.2.0 → aeval_framework-0.3.0}/src/agent_eval/metrics/answer_relevancy.py +3 -9
  24. aeval_framework-0.3.0/src/agent_eval/metrics/base.py +352 -0
  25. {aeval_framework-0.2.0 → aeval_framework-0.3.0}/src/agent_eval/metrics/batch_evaluation.py +12 -6
  26. {aeval_framework-0.2.0 → aeval_framework-0.3.0}/src/agent_eval/metrics/context_precision.py +4 -10
  27. {aeval_framework-0.2.0 → aeval_framework-0.3.0}/src/agent_eval/metrics/context_recall.py +4 -9
  28. {aeval_framework-0.2.0 → aeval_framework-0.3.0}/src/agent_eval/metrics/faithfulness.py +4 -10
  29. aeval_framework-0.3.0/src/agent_eval/metrics/llm_judge.py +179 -0
  30. {aeval_framework-0.2.0 → aeval_framework-0.3.0}/src/agent_eval/metrics/prompt_metric.py +6 -3
  31. {aeval_framework-0.2.0 → aeval_framework-0.3.0}/src/agent_eval/metrics/pytest_plugin.py +106 -28
  32. {aeval_framework-0.2.0 → aeval_framework-0.3.0}/src/agent_eval/metrics/report.py +89 -5
  33. aeval_framework-0.3.0/src/agent_eval/packs/__init__.py +8 -0
  34. aeval_framework-0.3.0/src/agent_eval/packs/starter/README.md +30 -0
  35. aeval_framework-0.3.0/src/agent_eval/packs/starter/manifest.json +8 -0
  36. aeval_framework-0.3.0/src/agent_eval/packs/starter/suite.yaml +29 -0
  37. aeval_framework-0.3.0/tests/smoke_achat_runner.py +94 -0
  38. aeval_framework-0.3.0/tests/test_agent_metric_catalog.py +474 -0
  39. {aeval_framework-0.2.0 → aeval_framework-0.3.0}/tests/test_api.py +44 -0
  40. aeval_framework-0.3.0/tests/test_baseline_comparison.py +250 -0
  41. aeval_framework-0.3.0/tests/test_cli.py +706 -0
  42. aeval_framework-0.3.0/tests/test_conversation.py +772 -0
  43. aeval_framework-0.3.0/tests/test_discovery.py +267 -0
  44. aeval_framework-0.3.0/tests/test_environment_identity.py +339 -0
  45. {aeval_framework-0.2.0 → aeval_framework-0.3.0}/tests/test_eval_dataset_api.py +3 -3
  46. {aeval_framework-0.2.0 → aeval_framework-0.3.0}/tests/test_eval_metric_pipeline.py +6 -7
  47. {aeval_framework-0.2.0 → aeval_framework-0.3.0}/tests/test_eval_metrics_module.py +14 -14
  48. aeval_framework-0.3.0/tests/test_eval_pytest_plugin.py +540 -0
  49. aeval_framework-0.3.0/tests/test_holdout_and_canary.py +301 -0
  50. aeval_framework-0.3.0/tests/test_metric_roles_gates_agreement.py +734 -0
  51. aeval_framework-0.3.0/tests/test_packaging.py +436 -0
  52. aeval_framework-0.3.0/tests/test_power_analysis.py +133 -0
  53. aeval_framework-0.3.0/tests/test_presentation_probes.py +710 -0
  54. aeval_framework-0.3.0/tests/test_prompt_determinism.py +210 -0
  55. {aeval_framework-0.2.0 → aeval_framework-0.3.0}/tests/test_regrade_and_archive.py +249 -0
  56. aeval_framework-0.2.0/.coverage +0 -0
  57. aeval_framework-0.2.0/src/agent_eval/metrics/base.py +0 -212
  58. aeval_framework-0.2.0/src/agent_eval/metrics/llm_judge.py +0 -100
  59. aeval_framework-0.2.0/tests/test_cli.py +0 -361
  60. aeval_framework-0.2.0/tests/test_eval_pytest_plugin.py +0 -282
  61. {aeval_framework-0.2.0 → aeval_framework-0.3.0}/README.md +0 -0
  62. {aeval_framework-0.2.0 → aeval_framework-0.3.0}/src/agent_eval/api/__init__.py +0 -0
  63. {aeval_framework-0.2.0 → aeval_framework-0.3.0}/src/agent_eval/api/events.py +0 -0
  64. {aeval_framework-0.2.0 → aeval_framework-0.3.0}/src/agent_eval/api/routes/__init__.py +0 -0
  65. {aeval_framework-0.2.0 → aeval_framework-0.3.0}/src/agent_eval/api/routes/datasets.py +0 -0
  66. {aeval_framework-0.2.0 → aeval_framework-0.3.0}/src/agent_eval/api/routes/graders.py +0 -0
  67. {aeval_framework-0.2.0 → aeval_framework-0.3.0}/src/agent_eval/api/routes/metrics.py +0 -0
  68. {aeval_framework-0.2.0 → aeval_framework-0.3.0}/src/agent_eval/api/routes/suites.py +0 -0
  69. {aeval_framework-0.2.0 → aeval_framework-0.3.0}/src/agent_eval/api/standalone.py +0 -0
  70. {aeval_framework-0.2.0 → aeval_framework-0.3.0}/src/agent_eval/core/__init__.py +0 -0
  71. {aeval_framework-0.2.0 → aeval_framework-0.3.0}/src/agent_eval/core/pricing.py +0 -0
  72. {aeval_framework-0.2.0 → aeval_framework-0.3.0}/src/agent_eval/core/redaction.py +0 -0
  73. {aeval_framework-0.2.0 → aeval_framework-0.3.0}/src/agent_eval/core/suite.py +0 -0
  74. {aeval_framework-0.2.0 → aeval_framework-0.3.0}/src/agent_eval/dataset/__init__.py +0 -0
  75. {aeval_framework-0.2.0 → aeval_framework-0.3.0}/src/agent_eval/dataset/models.py +0 -0
  76. {aeval_framework-0.2.0 → aeval_framework-0.3.0}/src/agent_eval/dataset/quality.py +0 -0
  77. {aeval_framework-0.2.0 → aeval_framework-0.3.0}/src/agent_eval/dataset/sources/__init__.py +0 -0
  78. {aeval_framework-0.2.0 → aeval_framework-0.3.0}/src/agent_eval/dataset/sources/llm_generator.py +0 -0
  79. {aeval_framework-0.2.0 → aeval_framework-0.3.0}/src/agent_eval/dataset/sources/manual.py +0 -0
  80. {aeval_framework-0.2.0 → aeval_framework-0.3.0}/src/agent_eval/dataset/sources/regression.py +0 -0
  81. {aeval_framework-0.2.0 → aeval_framework-0.3.0}/src/agent_eval/dataset/sources/trace_mining.py +0 -0
  82. {aeval_framework-0.2.0 → aeval_framework-0.3.0}/src/agent_eval/dataset/storage.py +0 -0
  83. {aeval_framework-0.2.0 → aeval_framework-0.3.0}/src/agent_eval/dataset/version.py +0 -0
  84. {aeval_framework-0.2.0 → aeval_framework-0.3.0}/src/agent_eval/examples/__init__.py +0 -0
  85. {aeval_framework-0.2.0 → aeval_framework-0.3.0}/src/agent_eval/examples/basic_usage.py +0 -0
  86. {aeval_framework-0.2.0 → aeval_framework-0.3.0}/src/agent_eval/examples/mock_runner.py +0 -0
  87. {aeval_framework-0.2.0 → aeval_framework-0.3.0}/src/agent_eval/graders/__init__.py +0 -0
  88. {aeval_framework-0.2.0 → aeval_framework-0.3.0}/src/agent_eval/graders/_verdicts.py +0 -0
  89. {aeval_framework-0.2.0 → aeval_framework-0.3.0}/src/agent_eval/graders/artifact_check.py +0 -0
  90. {aeval_framework-0.2.0 → aeval_framework-0.3.0}/src/agent_eval/graders/code_based.py +0 -0
  91. {aeval_framework-0.2.0 → aeval_framework-0.3.0}/src/agent_eval/graders/human.py +0 -0
  92. {aeval_framework-0.2.0 → aeval_framework-0.3.0}/src/agent_eval/graders/step_level.py +0 -0
  93. {aeval_framework-0.2.0 → aeval_framework-0.3.0}/src/agent_eval/graders/tool_calls.py +0 -0
  94. {aeval_framework-0.2.0 → aeval_framework-0.3.0}/src/agent_eval/graders/transcript.py +0 -0
  95. {aeval_framework-0.2.0 → aeval_framework-0.3.0}/src/agent_eval/metrics/synthetic_data.py +0 -0
  96. {aeval_framework-0.2.0 → aeval_framework-0.3.0}/src/agent_eval/storage/__init__.py +0 -0
  97. {aeval_framework-0.2.0 → aeval_framework-0.3.0}/src/agent_eval/storage/memory.py +0 -0
  98. {aeval_framework-0.2.0 → aeval_framework-0.3.0}/src/agent_eval/storage/sqlite.py +0 -0
  99. {aeval_framework-0.2.0 → aeval_framework-0.3.0}/src/agent_eval/trace/__init__.py +0 -0
  100. {aeval_framework-0.2.0 → aeval_framework-0.3.0}/src/agent_eval/trace/mapping.py +0 -0
  101. {aeval_framework-0.2.0 → aeval_framework-0.3.0}/src/agent_eval/trace/normalize.py +0 -0
  102. {aeval_framework-0.2.0 → aeval_framework-0.3.0}/src/agent_eval/trace/observations.py +0 -0
  103. {aeval_framework-0.2.0 → aeval_framework-0.3.0}/src/agent_eval/trace/phoenix.py +0 -0
  104. {aeval_framework-0.2.0 → aeval_framework-0.3.0}/tests/conftest.py +0 -0
  105. {aeval_framework-0.2.0 → aeval_framework-0.3.0}/tests/test_builtin_graders.py +0 -0
  106. {aeval_framework-0.2.0 → aeval_framework-0.3.0}/tests/test_collection_phases.py +0 -0
  107. {aeval_framework-0.2.0 → aeval_framework-0.3.0}/tests/test_e2e.py +0 -0
  108. {aeval_framework-0.2.0 → aeval_framework-0.3.0}/tests/test_eval_batch_evaluation.py +0 -0
  109. {aeval_framework-0.2.0 → aeval_framework-0.3.0}/tests/test_eval_dataset_sources.py +0 -0
  110. {aeval_framework-0.2.0 → aeval_framework-0.3.0}/tests/test_eval_dataset_storage.py +0 -0
  111. {aeval_framework-0.2.0 → aeval_framework-0.3.0}/tests/test_eval_metrics_api.py +0 -0
  112. {aeval_framework-0.2.0 → aeval_framework-0.3.0}/tests/test_eval_prompt_metric.py +0 -0
  113. {aeval_framework-0.2.0 → aeval_framework-0.3.0}/tests/test_eval_report.py +0 -0
  114. {aeval_framework-0.2.0 → aeval_framework-0.3.0}/tests/test_eval_task_history.py +0 -0
  115. {aeval_framework-0.2.0 → aeval_framework-0.3.0}/tests/test_evidence_capture_and_storage.py +0 -0
  116. {aeval_framework-0.2.0 → aeval_framework-0.3.0}/tests/test_graders.py +0 -0
  117. {aeval_framework-0.2.0 → aeval_framework-0.3.0}/tests/test_import_isolation.py +0 -0
  118. {aeval_framework-0.2.0 → aeval_framework-0.3.0}/tests/test_metrics.py +0 -0
  119. {aeval_framework-0.2.0 → aeval_framework-0.3.0}/tests/test_run_storage_fields.py +0 -0
  120. {aeval_framework-0.2.0 → aeval_framework-0.3.0}/tests/test_runner.py +0 -0
  121. {aeval_framework-0.2.0 → aeval_framework-0.3.0}/tests/test_sse.py +0 -0
  122. {aeval_framework-0.2.0 → aeval_framework-0.3.0}/tests/test_standalone_api.py +0 -0
  123. {aeval_framework-0.2.0 → aeval_framework-0.3.0}/tests/test_suite.py +0 -0
  124. {aeval_framework-0.2.0 → aeval_framework-0.3.0}/tests/test_termination_and_budgets.py +0 -0
  125. {aeval_framework-0.2.0 → aeval_framework-0.3.0}/tests/test_trace_normalization.py +0 -0
  126. {aeval_framework-0.2.0 → aeval_framework-0.3.0}/tests/test_trial_evidence.py +0 -0
  127. {aeval_framework-0.2.0 → aeval_framework-0.3.0}/tests/test_vocabulary_isolation.py +0 -0
@@ -9,6 +9,7 @@ build/
9
9
  .ruff_cache/
10
10
  *.db
11
11
  *.sqlite3
12
+ .coverage
12
13
 
13
14
  # Node / Next.js (dashboard)
14
15
  node_modules/
@@ -24,3 +25,15 @@ Thumbs.db
24
25
 
25
26
  # Editor-local state (Qoder)
26
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.2.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.2.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"
@@ -22,6 +22,7 @@ from typing import Any
22
22
  from fastapi import FastAPI
23
23
 
24
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
25
26
  from agent_eval.core.runner import EvalRunner
26
27
  from agent_eval.core.types import (
27
28
  DEFAULT_BOOTSTRAP_ROUNDS,
@@ -82,6 +83,7 @@ def statistics_defaults() -> dict[str, Any]:
82
83
  "bootstrap_rounds": DEFAULT_BOOTSTRAP_ROUNDS,
83
84
  "min_valid_trials_for_saturation": MIN_VALID_TRIALS_FOR_SATURATION,
84
85
  "gate_invalid_ratio_limit": DEFAULT_INVALID_RATIO_LIMIT,
86
+ "min_aligned_ratings_for_agreement": MIN_ALIGNED_RATINGS_FOR_AGREEMENT,
85
87
  }
86
88
 
87
89
 
@@ -95,6 +97,21 @@ def meta_payload(
95
97
  同一大版本内响应结构向后兼容, 但统计口径的数值语义可能变化 —— 口径版本
96
98
  必须显式公布, 调用方才能判断两个 run 是否可直接比较。
97
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
+ )
98
115
  return {
99
116
  "name": "Aeval",
100
117
  "package": DISTRIBUTION_NAME,
@@ -118,7 +135,12 @@ def meta_payload(
118
135
  "capture_is_one_declaration": True,
119
136
  },
120
137
  "capabilities": {
121
- "graders": [g["name"] for g in get_grader_catalog()],
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,
122
144
  "storage": ["memory", "sqlite"],
123
145
  "trace_providers": ["phoenix (optional, lazily imported)"],
124
146
  "sse": True,
@@ -131,6 +153,11 @@ def meta_payload(
131
153
  "cost_axis": True,
132
154
  "evidence_provenance_levels": True,
133
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,
134
161
  # 重评分本期只有库层入口 (EvalRunner.regrade_run)。它牵涉同一大版本的
135
162
  # 响应结构兼容承诺, 暴露面另立变更 —— 列出来免得调用方以为 /runs 上能调。
136
163
  "regrade_over_http": False,
@@ -29,6 +29,7 @@ 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
32
33
  from agent_eval.core.metrics import classify_trial, trial_invalid_reason
33
34
  from agent_eval.core.types import STATISTICS_VERSION, TrialVerdict
34
35
 
@@ -185,6 +186,8 @@ async def get_run(run_id: str):
185
186
  "trace_id": t.trace_id,
186
187
  "success": t.success,
187
188
  "score": t.avg_score(),
189
+ # trial 总分 (权重/策略/乘性门塌缩后的值; None = 该字段落盘前的历史 run)
190
+ "synthesized_score": t.synthesized_score,
188
191
  "duration_ms": t.duration_ms,
189
192
  "error": t.error,
190
193
  "verdict": t.verdict.value,
@@ -217,6 +220,10 @@ async def get_run(run_id: str):
217
220
  gr.judgment_moment.value if gr.judgment_moment else None
218
221
  ),
219
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,
220
227
  }
221
228
  for gr in t.grader_results
222
229
  ],
@@ -477,6 +484,7 @@ async def submit_human_score(run_id: str, request: HumanScoreRequest):
477
484
  # 重算 trial 成功状态与 run 汇总 (含该 task 汇总)
478
485
  if task is not None:
479
486
  trial.success = runner._compute_trial_success(task, trial.grader_results)
487
+ trial.synthesized_score = runner._trial_weighted_score(task, trial)
480
488
  trial.verdict = classify_trial(trial)
481
489
  trial.invalid_reason = trial_invalid_reason(trial)
482
490
  if run.statistics_version is None:
@@ -554,13 +562,12 @@ def _build_comparison(run_a, run_b) -> dict[str, Any]:
554
562
  summary_a = run_a.summary
555
563
  summary_b = run_b.summary
556
564
 
565
+ # 可比性前置与 CLI / 基线门同源 (core.comparison.runs_comparable)
566
+ comparable, not_comparable_reason = runs_comparable(run_a, run_b)
557
567
  version_a = getattr(run_a, "statistics_version", None)
558
568
  version_b = getattr(run_b, "statistics_version", None)
559
569
  evidence_a = getattr(run_a, "evidence", None)
560
570
  evidence_b = getattr(run_b, "evidence", None)
561
- same_caliber = version_a is not None and version_a == version_b
562
- same_boundary, boundary_reason = _compare_evidence_boundaries(evidence_a, evidence_b)
563
- comparable = same_caliber and same_boundary
564
571
 
565
572
  all_k_values: set[int] = set()
566
573
  for summary in (summary_a, summary_b):
@@ -684,42 +691,10 @@ def _build_comparison(run_a, run_b) -> dict[str, Any]:
684
691
  "b": evidence_b.model_dump() if evidence_b else None,
685
692
  },
686
693
  "comparable": comparable,
687
- "not_comparable_reason": _not_comparable_reason(
688
- comparable, same_caliber, version_a, version_b, boundary_reason
689
- ),
694
+ "not_comparable_reason": not_comparable_reason,
690
695
  }
691
696
 
692
697
 
693
- def _compare_evidence_boundaries(
694
- evidence_a: Any, evidence_b: Any
695
- ) -> tuple[bool, str | None]:
696
- """两个 run 的证据边界是否一致 (缺记录即无法判定可比)。"""
697
- if evidence_a is None and evidence_b is None:
698
- return False, "两个 run 均未记录证据采集边界 (历史 run), 无法判定可比性"
699
- if evidence_a is None or evidence_b is None:
700
- missing = "a" if evidence_a is None else "b"
701
- return False, f"run_{missing} 未记录证据采集边界 (历史 run), 无法判定可比性"
702
- return evidence_a.compare_with(evidence_b)
703
-
704
-
705
- def _not_comparable_reason(
706
- comparable: bool,
707
- same_caliber: bool,
708
- version_a: str | None,
709
- version_b: str | None,
710
- boundary_reason: str | None,
711
- ) -> str | None:
712
- if comparable:
713
- return None
714
- if not same_caliber:
715
- return (
716
- "两个 run 的统计口径版本不同"
717
- if version_a is not None and version_b is not None
718
- else "至少一个 run 未记录统计口径版本 (历史 run), 无法判定可比性"
719
- )
720
- return boundary_reason or "两个 run 的证据采集边界不同"
721
-
722
-
723
698
  def _delta(a: float | None, b: float | None) -> float | None:
724
699
  if a is None or b is None:
725
700
  return None
@@ -109,8 +109,9 @@ async def get_task_history(task_id: str):
109
109
  "valid_trials": len(valid),
110
110
  "invalid_trials": len(buckets[TrialVerdict.INVALID]),
111
111
  "pending_trials": len(buckets[TrialVerdict.PENDING]),
112
+ # 与 run summary 的 avg_score 同一口径: trial 总分 (含门塌缩), 非 grader 简均
112
113
  "avg_score": (
113
- round(sum(t.avg_score() for t in valid) / len(valid), 4)
114
+ round(sum(t.total_score() for t in valid) / len(valid), 4)
114
115
  if valid else None
115
116
  ),
116
117
  "graders": {