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.
- aeval_framework-0.3.0/.gitignore +39 -0
- {aeval_framework-0.1.0 → aeval_framework-0.3.0}/PKG-INFO +2 -2
- {aeval_framework-0.1.0 → aeval_framework-0.3.0}/pyproject.toml +5 -2
- {aeval_framework-0.1.0 → aeval_framework-0.3.0}/src/agent_eval/__init__.py +1 -1
- aeval_framework-0.3.0/src/agent_eval/api/app.py +218 -0
- {aeval_framework-0.1.0 → aeval_framework-0.3.0}/src/agent_eval/api/routes/runs.py +211 -57
- {aeval_framework-0.1.0 → aeval_framework-0.3.0}/src/agent_eval/api/routes/tasks.py +13 -3
- {aeval_framework-0.1.0 → aeval_framework-0.3.0}/src/agent_eval/api/standalone.py +13 -32
- aeval_framework-0.3.0/src/agent_eval/cli.py +1225 -0
- aeval_framework-0.3.0/src/agent_eval/core/__init__.py +99 -0
- aeval_framework-0.3.0/src/agent_eval/core/comparison.py +340 -0
- aeval_framework-0.3.0/src/agent_eval/core/contract.py +816 -0
- aeval_framework-0.3.0/src/agent_eval/core/discovery.py +149 -0
- aeval_framework-0.3.0/src/agent_eval/core/metrics.py +1060 -0
- aeval_framework-0.3.0/src/agent_eval/core/packaging.py +442 -0
- aeval_framework-0.3.0/src/agent_eval/core/pricing.py +152 -0
- aeval_framework-0.3.0/src/agent_eval/core/redaction.py +92 -0
- aeval_framework-0.3.0/src/agent_eval/core/runner.py +2579 -0
- aeval_framework-0.3.0/src/agent_eval/core/simulator.py +144 -0
- aeval_framework-0.3.0/src/agent_eval/core/types.py +1939 -0
- aeval_framework-0.3.0/src/agent_eval/examples/mock_runner.py +366 -0
- aeval_framework-0.3.0/src/agent_eval/graders/_evidence.py +243 -0
- aeval_framework-0.3.0/src/agent_eval/graders/_verdicts.py +37 -0
- {aeval_framework-0.1.0 → aeval_framework-0.3.0}/src/agent_eval/graders/artifact_check.py +59 -27
- {aeval_framework-0.1.0 → aeval_framework-0.3.0}/src/agent_eval/graders/code_based.py +46 -12
- {aeval_framework-0.1.0 → aeval_framework-0.3.0}/src/agent_eval/graders/human.py +25 -5
- aeval_framework-0.3.0/src/agent_eval/graders/metric.py +204 -0
- aeval_framework-0.3.0/src/agent_eval/graders/model_based.py +324 -0
- aeval_framework-0.3.0/src/agent_eval/graders/presentation_probes.py +392 -0
- aeval_framework-0.3.0/src/agent_eval/graders/state_check.py +280 -0
- {aeval_framework-0.1.0 → aeval_framework-0.3.0}/src/agent_eval/graders/step_level.py +46 -31
- aeval_framework-0.3.0/src/agent_eval/graders/tool_calls.py +161 -0
- aeval_framework-0.3.0/src/agent_eval/graders/transcript.py +166 -0
- {aeval_framework-0.1.0 → aeval_framework-0.3.0}/src/agent_eval/metrics/__init__.py +8 -0
- {aeval_framework-0.1.0 → aeval_framework-0.3.0}/src/agent_eval/metrics/answer_relevancy.py +4 -10
- aeval_framework-0.3.0/src/agent_eval/metrics/base.py +352 -0
- {aeval_framework-0.1.0 → aeval_framework-0.3.0}/src/agent_eval/metrics/batch_evaluation.py +12 -6
- {aeval_framework-0.1.0 → aeval_framework-0.3.0}/src/agent_eval/metrics/context_precision.py +4 -10
- {aeval_framework-0.1.0 → aeval_framework-0.3.0}/src/agent_eval/metrics/context_recall.py +4 -9
- {aeval_framework-0.1.0 → aeval_framework-0.3.0}/src/agent_eval/metrics/faithfulness.py +4 -10
- aeval_framework-0.3.0/src/agent_eval/metrics/llm_judge.py +179 -0
- {aeval_framework-0.1.0 → aeval_framework-0.3.0}/src/agent_eval/metrics/prompt_metric.py +6 -3
- aeval_framework-0.3.0/src/agent_eval/metrics/pytest_plugin.py +493 -0
- aeval_framework-0.3.0/src/agent_eval/metrics/report.py +251 -0
- aeval_framework-0.3.0/src/agent_eval/packs/__init__.py +8 -0
- aeval_framework-0.3.0/src/agent_eval/packs/starter/README.md +30 -0
- aeval_framework-0.3.0/src/agent_eval/packs/starter/manifest.json +8 -0
- aeval_framework-0.3.0/src/agent_eval/packs/starter/suite.yaml +29 -0
- {aeval_framework-0.1.0 → aeval_framework-0.3.0}/src/agent_eval/storage/memory.py +62 -6
- {aeval_framework-0.1.0 → aeval_framework-0.3.0}/src/agent_eval/storage/sqlite.py +135 -4
- aeval_framework-0.3.0/src/agent_eval/trace/__init__.py +56 -0
- aeval_framework-0.3.0/src/agent_eval/trace/mapping.py +243 -0
- aeval_framework-0.3.0/src/agent_eval/trace/normalize.py +572 -0
- aeval_framework-0.3.0/src/agent_eval/trace/observations.py +135 -0
- {aeval_framework-0.1.0 → aeval_framework-0.3.0}/src/agent_eval/trace/phoenix.py +57 -3
- aeval_framework-0.3.0/tests/conftest.py +225 -0
- aeval_framework-0.3.0/tests/smoke_achat_runner.py +94 -0
- aeval_framework-0.3.0/tests/test_agent_metric_catalog.py +474 -0
- {aeval_framework-0.1.0 → aeval_framework-0.3.0}/tests/test_api.py +165 -4
- aeval_framework-0.3.0/tests/test_baseline_comparison.py +250 -0
- aeval_framework-0.3.0/tests/test_builtin_graders.py +457 -0
- aeval_framework-0.3.0/tests/test_cli.py +706 -0
- aeval_framework-0.3.0/tests/test_collection_phases.py +644 -0
- aeval_framework-0.3.0/tests/test_conversation.py +772 -0
- aeval_framework-0.3.0/tests/test_discovery.py +267 -0
- aeval_framework-0.3.0/tests/test_e2e.py +437 -0
- aeval_framework-0.3.0/tests/test_environment_identity.py +339 -0
- {aeval_framework-0.1.0 → aeval_framework-0.3.0}/tests/test_eval_dataset_api.py +20 -14
- {aeval_framework-0.1.0 → aeval_framework-0.3.0}/tests/test_eval_metric_pipeline.py +77 -14
- {aeval_framework-0.1.0 → aeval_framework-0.3.0}/tests/test_eval_metrics_module.py +20 -14
- aeval_framework-0.3.0/tests/test_eval_pytest_plugin.py +540 -0
- {aeval_framework-0.1.0 → aeval_framework-0.3.0}/tests/test_eval_task_history.py +75 -0
- aeval_framework-0.3.0/tests/test_evidence_capture_and_storage.py +343 -0
- {aeval_framework-0.1.0 → aeval_framework-0.3.0}/tests/test_graders.py +27 -23
- aeval_framework-0.3.0/tests/test_holdout_and_canary.py +301 -0
- aeval_framework-0.3.0/tests/test_metric_roles_gates_agreement.py +734 -0
- aeval_framework-0.3.0/tests/test_metrics.py +699 -0
- aeval_framework-0.3.0/tests/test_packaging.py +436 -0
- aeval_framework-0.3.0/tests/test_power_analysis.py +133 -0
- aeval_framework-0.3.0/tests/test_presentation_probes.py +710 -0
- aeval_framework-0.3.0/tests/test_prompt_determinism.py +210 -0
- aeval_framework-0.3.0/tests/test_regrade_and_archive.py +792 -0
- aeval_framework-0.3.0/tests/test_run_storage_fields.py +261 -0
- {aeval_framework-0.1.0 → aeval_framework-0.3.0}/tests/test_runner.py +276 -28
- {aeval_framework-0.1.0 → aeval_framework-0.3.0}/tests/test_standalone_api.py +42 -3
- aeval_framework-0.3.0/tests/test_suite.py +438 -0
- aeval_framework-0.3.0/tests/test_termination_and_budgets.py +302 -0
- aeval_framework-0.3.0/tests/test_trace_normalization.py +427 -0
- aeval_framework-0.3.0/tests/test_trial_evidence.py +351 -0
- aeval_framework-0.3.0/tests/test_vocabulary_isolation.py +65 -0
- aeval_framework-0.1.0/.coverage +0 -0
- aeval_framework-0.1.0/.gitignore +0 -23
- aeval_framework-0.1.0/src/agent_eval/api/app.py +0 -82
- aeval_framework-0.1.0/src/agent_eval/cli.py +0 -455
- aeval_framework-0.1.0/src/agent_eval/core/__init__.py +0 -48
- aeval_framework-0.1.0/src/agent_eval/core/contract.py +0 -296
- aeval_framework-0.1.0/src/agent_eval/core/metrics.py +0 -184
- aeval_framework-0.1.0/src/agent_eval/core/runner.py +0 -868
- aeval_framework-0.1.0/src/agent_eval/core/types.py +0 -227
- aeval_framework-0.1.0/src/agent_eval/examples/mock_runner.py +0 -195
- aeval_framework-0.1.0/src/agent_eval/graders/metric.py +0 -142
- aeval_framework-0.1.0/src/agent_eval/graders/model_based.py +0 -179
- aeval_framework-0.1.0/src/agent_eval/graders/state_check.py +0 -106
- aeval_framework-0.1.0/src/agent_eval/graders/tool_calls.py +0 -102
- aeval_framework-0.1.0/src/agent_eval/graders/transcript.py +0 -86
- aeval_framework-0.1.0/src/agent_eval/metrics/base.py +0 -155
- aeval_framework-0.1.0/src/agent_eval/metrics/llm_judge.py +0 -100
- aeval_framework-0.1.0/src/agent_eval/metrics/pytest_plugin.py +0 -308
- aeval_framework-0.1.0/src/agent_eval/metrics/report.py +0 -149
- aeval_framework-0.1.0/src/agent_eval/trace/__init__.py +0 -16
- aeval_framework-0.1.0/tests/conftest.py +0 -9
- aeval_framework-0.1.0/tests/test_builtin_graders.py +0 -298
- aeval_framework-0.1.0/tests/test_cli.py +0 -280
- aeval_framework-0.1.0/tests/test_e2e.py +0 -243
- aeval_framework-0.1.0/tests/test_eval_pytest_plugin.py +0 -218
- aeval_framework-0.1.0/tests/test_metrics.py +0 -130
- aeval_framework-0.1.0/tests/test_suite.py +0 -209
- {aeval_framework-0.1.0 → aeval_framework-0.3.0}/README.md +0 -0
- {aeval_framework-0.1.0 → aeval_framework-0.3.0}/src/agent_eval/api/__init__.py +0 -0
- {aeval_framework-0.1.0 → aeval_framework-0.3.0}/src/agent_eval/api/events.py +0 -0
- {aeval_framework-0.1.0 → aeval_framework-0.3.0}/src/agent_eval/api/routes/__init__.py +0 -0
- {aeval_framework-0.1.0 → aeval_framework-0.3.0}/src/agent_eval/api/routes/datasets.py +0 -0
- {aeval_framework-0.1.0 → aeval_framework-0.3.0}/src/agent_eval/api/routes/graders.py +0 -0
- {aeval_framework-0.1.0 → aeval_framework-0.3.0}/src/agent_eval/api/routes/metrics.py +0 -0
- {aeval_framework-0.1.0 → aeval_framework-0.3.0}/src/agent_eval/api/routes/suites.py +0 -0
- {aeval_framework-0.1.0 → aeval_framework-0.3.0}/src/agent_eval/core/suite.py +0 -0
- {aeval_framework-0.1.0 → aeval_framework-0.3.0}/src/agent_eval/dataset/__init__.py +0 -0
- {aeval_framework-0.1.0 → aeval_framework-0.3.0}/src/agent_eval/dataset/models.py +0 -0
- {aeval_framework-0.1.0 → aeval_framework-0.3.0}/src/agent_eval/dataset/quality.py +0 -0
- {aeval_framework-0.1.0 → aeval_framework-0.3.0}/src/agent_eval/dataset/sources/__init__.py +0 -0
- {aeval_framework-0.1.0 → aeval_framework-0.3.0}/src/agent_eval/dataset/sources/llm_generator.py +0 -0
- {aeval_framework-0.1.0 → aeval_framework-0.3.0}/src/agent_eval/dataset/sources/manual.py +0 -0
- {aeval_framework-0.1.0 → aeval_framework-0.3.0}/src/agent_eval/dataset/sources/regression.py +0 -0
- {aeval_framework-0.1.0 → aeval_framework-0.3.0}/src/agent_eval/dataset/sources/trace_mining.py +0 -0
- {aeval_framework-0.1.0 → aeval_framework-0.3.0}/src/agent_eval/dataset/storage.py +0 -0
- {aeval_framework-0.1.0 → aeval_framework-0.3.0}/src/agent_eval/dataset/version.py +0 -0
- {aeval_framework-0.1.0 → aeval_framework-0.3.0}/src/agent_eval/examples/__init__.py +0 -0
- {aeval_framework-0.1.0 → aeval_framework-0.3.0}/src/agent_eval/examples/basic_usage.py +0 -0
- {aeval_framework-0.1.0 → aeval_framework-0.3.0}/src/agent_eval/graders/__init__.py +0 -0
- {aeval_framework-0.1.0 → aeval_framework-0.3.0}/src/agent_eval/metrics/synthetic_data.py +0 -0
- {aeval_framework-0.1.0 → aeval_framework-0.3.0}/src/agent_eval/storage/__init__.py +0 -0
- {aeval_framework-0.1.0 → aeval_framework-0.3.0}/tests/test_eval_batch_evaluation.py +0 -0
- {aeval_framework-0.1.0 → aeval_framework-0.3.0}/tests/test_eval_dataset_sources.py +0 -0
- {aeval_framework-0.1.0 → aeval_framework-0.3.0}/tests/test_eval_dataset_storage.py +0 -0
- {aeval_framework-0.1.0 → aeval_framework-0.3.0}/tests/test_eval_metrics_api.py +0 -0
- {aeval_framework-0.1.0 → aeval_framework-0.3.0}/tests/test_eval_prompt_metric.py +0 -0
- {aeval_framework-0.1.0 → aeval_framework-0.3.0}/tests/test_eval_report.py +0 -0
- {aeval_framework-0.1.0 → aeval_framework-0.3.0}/tests/test_import_isolation.py +0 -0
- {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.
|
|
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/
|
|
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.
|
|
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/
|
|
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"
|
|
@@ -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(
|
|
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
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
for
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
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":
|
|
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
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
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
|
-
|
|
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
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
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])
|