aeval-framework 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- aeval_framework-0.1.0.dist-info/METADATA +42 -0
- aeval_framework-0.1.0.dist-info/RECORD +63 -0
- aeval_framework-0.1.0.dist-info/WHEEL +4 -0
- aeval_framework-0.1.0.dist-info/entry_points.txt +2 -0
- agent_eval/__init__.py +14 -0
- agent_eval/api/__init__.py +14 -0
- agent_eval/api/app.py +82 -0
- agent_eval/api/events.py +96 -0
- agent_eval/api/routes/__init__.py +0 -0
- agent_eval/api/routes/datasets.py +441 -0
- agent_eval/api/routes/graders.py +19 -0
- agent_eval/api/routes/metrics.py +49 -0
- agent_eval/api/routes/runs.py +573 -0
- agent_eval/api/routes/suites.py +84 -0
- agent_eval/api/routes/tasks.py +114 -0
- agent_eval/api/standalone.py +105 -0
- agent_eval/cli.py +455 -0
- agent_eval/core/__init__.py +48 -0
- agent_eval/core/contract.py +296 -0
- agent_eval/core/metrics.py +184 -0
- agent_eval/core/runner.py +868 -0
- agent_eval/core/suite.py +60 -0
- agent_eval/core/types.py +227 -0
- agent_eval/dataset/__init__.py +31 -0
- agent_eval/dataset/models.py +199 -0
- agent_eval/dataset/quality.py +194 -0
- agent_eval/dataset/sources/__init__.py +45 -0
- agent_eval/dataset/sources/llm_generator.py +219 -0
- agent_eval/dataset/sources/manual.py +172 -0
- agent_eval/dataset/sources/regression.py +201 -0
- agent_eval/dataset/sources/trace_mining.py +277 -0
- agent_eval/dataset/storage.py +342 -0
- agent_eval/dataset/version.py +72 -0
- agent_eval/examples/__init__.py +0 -0
- agent_eval/examples/basic_usage.py +175 -0
- agent_eval/examples/mock_runner.py +195 -0
- agent_eval/graders/__init__.py +91 -0
- agent_eval/graders/artifact_check.py +114 -0
- agent_eval/graders/code_based.py +101 -0
- agent_eval/graders/human.py +77 -0
- agent_eval/graders/metric.py +142 -0
- agent_eval/graders/model_based.py +179 -0
- agent_eval/graders/state_check.py +106 -0
- agent_eval/graders/step_level.py +116 -0
- agent_eval/graders/tool_calls.py +102 -0
- agent_eval/graders/transcript.py +86 -0
- agent_eval/metrics/__init__.py +110 -0
- agent_eval/metrics/answer_relevancy.py +57 -0
- agent_eval/metrics/base.py +155 -0
- agent_eval/metrics/batch_evaluation.py +267 -0
- agent_eval/metrics/context_precision.py +62 -0
- agent_eval/metrics/context_recall.py +71 -0
- agent_eval/metrics/faithfulness.py +72 -0
- agent_eval/metrics/llm_judge.py +100 -0
- agent_eval/metrics/prompt_metric.py +150 -0
- agent_eval/metrics/pytest_plugin.py +308 -0
- agent_eval/metrics/report.py +149 -0
- agent_eval/metrics/synthetic_data.py +203 -0
- agent_eval/storage/__init__.py +17 -0
- agent_eval/storage/memory.py +95 -0
- agent_eval/storage/sqlite.py +240 -0
- agent_eval/trace/__init__.py +16 -0
- agent_eval/trace/phoenix.py +144 -0
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Core types and contracts for the Aeval evaluation framework.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from agent_eval.core.contract import (
|
|
6
|
+
AgentRunner,
|
|
7
|
+
EnvironmentManager,
|
|
8
|
+
Grader,
|
|
9
|
+
Storage,
|
|
10
|
+
TraceProvider,
|
|
11
|
+
)
|
|
12
|
+
from agent_eval.core.metrics import aggregate_metrics, pass_at_k, pass_power_k
|
|
13
|
+
from agent_eval.core.types import (
|
|
14
|
+
EvalSuite,
|
|
15
|
+
EvalTask,
|
|
16
|
+
GraderConfig,
|
|
17
|
+
GraderResult,
|
|
18
|
+
GraderType,
|
|
19
|
+
RunResult,
|
|
20
|
+
RunSummary,
|
|
21
|
+
ScoreStrategy,
|
|
22
|
+
TaskSummary,
|
|
23
|
+
TrialResult,
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
__all__ = [
|
|
27
|
+
# Types
|
|
28
|
+
"EvalTask",
|
|
29
|
+
"EvalSuite",
|
|
30
|
+
"GraderConfig",
|
|
31
|
+
"GraderResult",
|
|
32
|
+
"GraderType",
|
|
33
|
+
"RunResult",
|
|
34
|
+
"RunSummary",
|
|
35
|
+
"ScoreStrategy",
|
|
36
|
+
"TaskSummary",
|
|
37
|
+
"TrialResult",
|
|
38
|
+
# Contracts
|
|
39
|
+
"AgentRunner",
|
|
40
|
+
"EnvironmentManager",
|
|
41
|
+
"Grader",
|
|
42
|
+
"Storage",
|
|
43
|
+
"TraceProvider",
|
|
44
|
+
# Metrics
|
|
45
|
+
"aggregate_metrics",
|
|
46
|
+
"pass_at_k",
|
|
47
|
+
"pass_power_k",
|
|
48
|
+
]
|
|
@@ -0,0 +1,296 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Integration contracts for the Aeval evaluation framework.
|
|
3
|
+
|
|
4
|
+
This module defines the Protocol interfaces that projects must implement
|
|
5
|
+
to integrate with Aeval. Only AgentRunner is required; all others have
|
|
6
|
+
default implementations.
|
|
7
|
+
|
|
8
|
+
Contracts:
|
|
9
|
+
- AgentRunner (REQUIRED): Run an agent task and return trace_id + transcript + outcome
|
|
10
|
+
- TraceProvider (optional): Fetch trace spans from a trace backend
|
|
11
|
+
- Grader (optional): Score a single trial result
|
|
12
|
+
- Storage (optional): Persist run/suite results
|
|
13
|
+
- EnvironmentManager (optional): Setup/teardown environment for each trial
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
from __future__ import annotations
|
|
17
|
+
|
|
18
|
+
from dataclasses import dataclass, field
|
|
19
|
+
from typing import Any, Protocol, runtime_checkable
|
|
20
|
+
|
|
21
|
+
from agent_eval.core.types import (
|
|
22
|
+
EvalSuite,
|
|
23
|
+
EvalTask,
|
|
24
|
+
GraderConfig,
|
|
25
|
+
GraderResult,
|
|
26
|
+
RunResult,
|
|
27
|
+
TrialResult,
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
# ─── Errors ───────────────────────────────────────────────────────────────────
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class TransientError(Exception):
|
|
34
|
+
"""
|
|
35
|
+
瞬态错误 — AgentRunner 实现方显式抛出。
|
|
36
|
+
|
|
37
|
+
框架仅对 TransientError 做指数退避重试 (默认最多 2 次)。
|
|
38
|
+
httpx 超时/网络抖动等属于实现层知识, 由实现方判断并包装为本异常;
|
|
39
|
+
其他异常 (含 asyncio.TimeoutError) 不重试, 直接记失败。
|
|
40
|
+
"""
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
# ─── Evaluation Context ───────────────────────────────────────────────────────
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
@dataclass
|
|
47
|
+
class EvalContext:
|
|
48
|
+
"""
|
|
49
|
+
单次 trial 的评分上下文 — 贯穿评分调用, 在 grader 之间共享。
|
|
50
|
+
|
|
51
|
+
Attributes:
|
|
52
|
+
run_id: 所属 run 的 ID
|
|
53
|
+
task: 任务定义
|
|
54
|
+
trial: 本次 trial 的结果 (评分过程中可能被填充)
|
|
55
|
+
spans: trace span 列表
|
|
56
|
+
shared_state: 同一 trial 内各 grader 间共享的可变状态
|
|
57
|
+
grader_config: 当前评分调用的 grader 配置 (runner 每次调用前以
|
|
58
|
+
replace() 注入; 供 name 与配置名不一致的分发型 grader 定位
|
|
59
|
+
自己的配置, 如 MetricGrader)
|
|
60
|
+
"""
|
|
61
|
+
|
|
62
|
+
run_id: str
|
|
63
|
+
task: EvalTask
|
|
64
|
+
trial: TrialResult
|
|
65
|
+
spans: list[dict[str, Any]] = field(default_factory=list)
|
|
66
|
+
shared_state: dict[str, Any] = field(default_factory=dict)
|
|
67
|
+
grader_config: GraderConfig | None = None
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
# ─── Required Contract ────────────────────────────────────────────────────────
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
@runtime_checkable
|
|
74
|
+
class AgentRunner(Protocol):
|
|
75
|
+
"""
|
|
76
|
+
项目必须实现: 运行 Agent 并返回 trace。
|
|
77
|
+
|
|
78
|
+
这是唯一的必选接入点。框架通过这个接口与 Agent 系统交互,
|
|
79
|
+
不需要知道 Agent 的内部实现细节。
|
|
80
|
+
|
|
81
|
+
Example:
|
|
82
|
+
class MyAgentRunner:
|
|
83
|
+
async def run(self, task: EvalTask) -> tuple[str, list[dict], dict]:
|
|
84
|
+
# 1. 准备环境
|
|
85
|
+
# 2. 发送 prompt 给 Agent
|
|
86
|
+
# 3. 等待 Agent 完成
|
|
87
|
+
# 4. 收集 trace_id, transcript, outcome
|
|
88
|
+
return trace_id, transcript, outcome
|
|
89
|
+
"""
|
|
90
|
+
|
|
91
|
+
async def run(
|
|
92
|
+
self,
|
|
93
|
+
task: EvalTask,
|
|
94
|
+
) -> tuple[str, list[dict[str, Any]], dict[str, Any]]:
|
|
95
|
+
"""
|
|
96
|
+
执行一个评测任务。
|
|
97
|
+
|
|
98
|
+
Args:
|
|
99
|
+
task: 评测任务定义
|
|
100
|
+
|
|
101
|
+
Returns:
|
|
102
|
+
tuple: (trace_id, transcript, outcome)
|
|
103
|
+
- trace_id: OTel trace ID
|
|
104
|
+
- transcript: 完整对话记录
|
|
105
|
+
- outcome: 环境最终状态
|
|
106
|
+
|
|
107
|
+
Raises:
|
|
108
|
+
AgentRunError: Agent 执行失败 (超时/崩溃/被拦截)
|
|
109
|
+
"""
|
|
110
|
+
...
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
# ─── Optional Contracts ───────────────────────────────────────────────────────
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
@runtime_checkable
|
|
117
|
+
class TraceProvider(Protocol):
|
|
118
|
+
"""
|
|
119
|
+
Trace 数据获取。
|
|
120
|
+
|
|
121
|
+
默认提供 Phoenix 实现, 可自定义以支持其他后端 (Jaeger, Tempo, ...)。
|
|
122
|
+
"""
|
|
123
|
+
|
|
124
|
+
async def get_spans(
|
|
125
|
+
self,
|
|
126
|
+
trace_id: str,
|
|
127
|
+
) -> list[dict[str, Any]]:
|
|
128
|
+
"""
|
|
129
|
+
获取一个 trace 的所有 span。
|
|
130
|
+
|
|
131
|
+
Args:
|
|
132
|
+
trace_id: OTel trace ID
|
|
133
|
+
|
|
134
|
+
Returns:
|
|
135
|
+
span 列表, 每个 span 是 dict, 包含:
|
|
136
|
+
- name: str # span 名称
|
|
137
|
+
- attributes: dict # span 属性
|
|
138
|
+
- start_time: str # 开始时间
|
|
139
|
+
- end_time: str # 结束时间
|
|
140
|
+
- status: dict # 状态
|
|
141
|
+
"""
|
|
142
|
+
...
|
|
143
|
+
|
|
144
|
+
async def get_trace_ids(
|
|
145
|
+
self,
|
|
146
|
+
filters: dict[str, Any] | None = None,
|
|
147
|
+
limit: int = 100,
|
|
148
|
+
) -> list[str]:
|
|
149
|
+
"""
|
|
150
|
+
查询 trace ID 列表 (用于历史记录浏览)。
|
|
151
|
+
|
|
152
|
+
Args:
|
|
153
|
+
filters: 过滤条件 (时间范围/状态/标签等)
|
|
154
|
+
limit: 返回数量限制
|
|
155
|
+
|
|
156
|
+
Returns:
|
|
157
|
+
trace ID 列表
|
|
158
|
+
"""
|
|
159
|
+
...
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
@runtime_checkable
|
|
163
|
+
class Grader(Protocol):
|
|
164
|
+
"""
|
|
165
|
+
评分器接口。
|
|
166
|
+
|
|
167
|
+
内置 6 个通用实现, 项目可自定义以适配业务逻辑。
|
|
168
|
+
"""
|
|
169
|
+
|
|
170
|
+
name: str # 评分器唯一名称
|
|
171
|
+
|
|
172
|
+
async def grade(
|
|
173
|
+
self,
|
|
174
|
+
trial: TrialResult,
|
|
175
|
+
spans: list[dict[str, Any]],
|
|
176
|
+
task: EvalTask,
|
|
177
|
+
context: EvalContext | None = None,
|
|
178
|
+
) -> GraderResult:
|
|
179
|
+
"""
|
|
180
|
+
对一次 trial 进行评分。
|
|
181
|
+
|
|
182
|
+
Args:
|
|
183
|
+
trial: trial 结果 (含 transcript/outcome/metrics)
|
|
184
|
+
spans: trace span 列表 (用于分析过程)
|
|
185
|
+
task: 任务定义 (含 grader config)
|
|
186
|
+
context: 评分上下文 (run_id/task/trial/spans/shared_state)
|
|
187
|
+
|
|
188
|
+
Returns:
|
|
189
|
+
GraderResult: 评分结果
|
|
190
|
+
"""
|
|
191
|
+
...
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
@runtime_checkable
|
|
195
|
+
class Storage(Protocol):
|
|
196
|
+
"""
|
|
197
|
+
结果持久化。
|
|
198
|
+
|
|
199
|
+
默认 SQLite, 可选 PostgreSQL / Memory。
|
|
200
|
+
"""
|
|
201
|
+
|
|
202
|
+
# ── Run 操作 ──
|
|
203
|
+
|
|
204
|
+
async def save_run(self, run: RunResult) -> None:
|
|
205
|
+
"""保存运行结果"""
|
|
206
|
+
...
|
|
207
|
+
|
|
208
|
+
async def get_run(self, run_id: str) -> RunResult | None:
|
|
209
|
+
"""获取运行结果"""
|
|
210
|
+
...
|
|
211
|
+
|
|
212
|
+
async def list_runs(
|
|
213
|
+
self, suite_name: str | None = None, limit: int = 50
|
|
214
|
+
) -> list[RunResult]:
|
|
215
|
+
"""列出运行历史"""
|
|
216
|
+
...
|
|
217
|
+
|
|
218
|
+
async def delete_run(self, run_id: str) -> bool:
|
|
219
|
+
"""删除运行结果"""
|
|
220
|
+
...
|
|
221
|
+
|
|
222
|
+
# ── Suite 操作 ──
|
|
223
|
+
|
|
224
|
+
async def save_suite(self, suite: EvalSuite) -> None:
|
|
225
|
+
"""保存评测套件"""
|
|
226
|
+
...
|
|
227
|
+
|
|
228
|
+
async def get_suite(self, name: str) -> EvalSuite | None:
|
|
229
|
+
"""获取评测套件"""
|
|
230
|
+
...
|
|
231
|
+
|
|
232
|
+
async def list_suites(self) -> list[EvalSuite]:
|
|
233
|
+
"""列出所有评测套件"""
|
|
234
|
+
...
|
|
235
|
+
|
|
236
|
+
async def delete_suite(self, name: str) -> bool:
|
|
237
|
+
"""删除评测套件"""
|
|
238
|
+
...
|
|
239
|
+
|
|
240
|
+
# ── 人工评分请求 (可选, HumanGrader pending 语义使用) ──
|
|
241
|
+
|
|
242
|
+
async def save_human_score_request(self, request: dict[str, Any]) -> None:
|
|
243
|
+
"""保存人工评分请求"""
|
|
244
|
+
...
|
|
245
|
+
|
|
246
|
+
async def list_human_score_requests(
|
|
247
|
+
self, run_id: str | None = None
|
|
248
|
+
) -> list[dict[str, Any]]:
|
|
249
|
+
"""列出人工评分请求"""
|
|
250
|
+
...
|
|
251
|
+
|
|
252
|
+
|
|
253
|
+
@runtime_checkable
|
|
254
|
+
class EnvironmentManager(Protocol):
|
|
255
|
+
"""
|
|
256
|
+
环境管理。可选, 默认无操作 (NoOpEnvironment)。
|
|
257
|
+
|
|
258
|
+
用于:
|
|
259
|
+
- workspace 隔离 (每次 trial 从干净环境开始)
|
|
260
|
+
- 数据准备 (注入测试数据)
|
|
261
|
+
- 资源清理 (删除临时文件/数据库)
|
|
262
|
+
- 泄漏检测 (trial 前后环境一致性校验)
|
|
263
|
+
"""
|
|
264
|
+
|
|
265
|
+
async def setup(self, task: EvalTask) -> None:
|
|
266
|
+
"""trial 开始前: 准备环境"""
|
|
267
|
+
...
|
|
268
|
+
|
|
269
|
+
async def teardown(self, task: EvalTask) -> None:
|
|
270
|
+
"""trial 结束后: 清理环境"""
|
|
271
|
+
...
|
|
272
|
+
|
|
273
|
+
async def snapshot(self) -> dict[str, Any]:
|
|
274
|
+
"""
|
|
275
|
+
拍摄环境基线快照 (JSON 可序列化)。
|
|
276
|
+
|
|
277
|
+
Returns:
|
|
278
|
+
环境状态快照, 传给 verify_clean / restore
|
|
279
|
+
"""
|
|
280
|
+
...
|
|
281
|
+
|
|
282
|
+
async def verify_clean(self, baseline: dict[str, Any]) -> dict[str, Any]:
|
|
283
|
+
"""
|
|
284
|
+
校验环境是否与基线一致。
|
|
285
|
+
|
|
286
|
+
Args:
|
|
287
|
+
baseline: snapshot() 返回的基线快照
|
|
288
|
+
|
|
289
|
+
Returns:
|
|
290
|
+
{"clean": bool, "differences": [...]}
|
|
291
|
+
"""
|
|
292
|
+
...
|
|
293
|
+
|
|
294
|
+
async def restore(self, baseline: dict[str, Any]) -> None:
|
|
295
|
+
"""将环境恢复到基线状态"""
|
|
296
|
+
...
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Statistical metrics for the Aeval evaluation framework.
|
|
3
|
+
|
|
4
|
+
Provides pass@k and pass^k calculations for aggregating trial results,
|
|
5
|
+
as well as helper functions for metric aggregation.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import contextlib
|
|
11
|
+
from typing import Any
|
|
12
|
+
|
|
13
|
+
from agent_eval.core.types import TrialResult
|
|
14
|
+
|
|
15
|
+
# ─── pass@k / pass^k ──────────────────────────────────────────────────────────
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def pass_at_k(trials: list[TrialResult], k: int) -> float:
|
|
19
|
+
"""
|
|
20
|
+
pass@k: k 次尝试中至少成功一次的任务比例。
|
|
21
|
+
|
|
22
|
+
用于能力评估 — "Agent 有没有机会完成这个任务?"
|
|
23
|
+
|
|
24
|
+
计算逻辑 (设计文档 §4.3 修正版):
|
|
25
|
+
- k <= n (实际 trial 数): 直接判定, 至少一次成功即 1.0
|
|
26
|
+
- k > n: 二项分布外推 P(至少一次成功) = 1 - (1-p)^k,
|
|
27
|
+
其中 p = successes / n (单次成功概率的极大似然估计)
|
|
28
|
+
|
|
29
|
+
Args:
|
|
30
|
+
trials: trial 结果列表
|
|
31
|
+
k: 尝试次数
|
|
32
|
+
|
|
33
|
+
Returns:
|
|
34
|
+
0.0-1.0 之间的通过率
|
|
35
|
+
|
|
36
|
+
Examples:
|
|
37
|
+
>>> pass_at_k([fail, success, success], k=1) # 有成功即通过
|
|
38
|
+
1.0
|
|
39
|
+
>>> pass_at_k([fail, fail, fail], k=3)
|
|
40
|
+
0.0
|
|
41
|
+
>>> pass_at_k([success, fail, fail], k=5) # 1/3 成功, 外推
|
|
42
|
+
0.868...
|
|
43
|
+
"""
|
|
44
|
+
n = len(trials)
|
|
45
|
+
if n == 0 or k <= 0:
|
|
46
|
+
return 0.0
|
|
47
|
+
|
|
48
|
+
successes = sum(1 for t in trials if t.success)
|
|
49
|
+
|
|
50
|
+
if k <= n:
|
|
51
|
+
return 1.0 if successes > 0 else 0.0
|
|
52
|
+
|
|
53
|
+
# k > n: 二项分布外推
|
|
54
|
+
p = successes / n
|
|
55
|
+
if p == 0:
|
|
56
|
+
return 0.0
|
|
57
|
+
if p == 1:
|
|
58
|
+
return 1.0
|
|
59
|
+
return 1.0 - (1.0 - p) ** k
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def pass_power_k(trials: list[TrialResult], k: int) -> float:
|
|
63
|
+
"""
|
|
64
|
+
pass^k: k 次尝试全部成功的任务比例。
|
|
65
|
+
|
|
66
|
+
用于回归评估 — "Agent 每次都能可靠完成吗?"
|
|
67
|
+
|
|
68
|
+
计算逻辑 (设计文档 §4.3 修正版):
|
|
69
|
+
- k <= n (实际 trial 数): 直接判定, 前 k 次全部成功即 1.0
|
|
70
|
+
- k > n: 二项分布外推 P(全部成功) = p^k
|
|
71
|
+
|
|
72
|
+
Args:
|
|
73
|
+
trials: trial 结果列表
|
|
74
|
+
k: 尝试次数
|
|
75
|
+
|
|
76
|
+
Returns:
|
|
77
|
+
0.0-1.0 之间的通过率
|
|
78
|
+
|
|
79
|
+
Examples:
|
|
80
|
+
>>> pass_power_k([success, success, success], k=3) # 全部成功
|
|
81
|
+
1.0
|
|
82
|
+
>>> pass_power_k([success, fail, success], k=3) # 有失败
|
|
83
|
+
0.0
|
|
84
|
+
>>> pass_power_k([success, success, fail], k=5) # 2/3 成功, 外推
|
|
85
|
+
0.132...
|
|
86
|
+
"""
|
|
87
|
+
n = len(trials)
|
|
88
|
+
if n == 0 or k <= 0:
|
|
89
|
+
return 0.0
|
|
90
|
+
|
|
91
|
+
successes = sum(1 for t in trials if t.success)
|
|
92
|
+
|
|
93
|
+
if k <= n:
|
|
94
|
+
k_trials = trials[:k]
|
|
95
|
+
return 1.0 if all(t.success for t in k_trials) else 0.0
|
|
96
|
+
|
|
97
|
+
# k > n: 二项分布外推
|
|
98
|
+
p = successes / n
|
|
99
|
+
if p == 0:
|
|
100
|
+
return 0.0
|
|
101
|
+
return p**k
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
# ─── Metric Aggregation ───────────────────────────────────────────────────────
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def aggregate_metrics(trials: list[TrialResult]) -> dict[str, float]:
|
|
108
|
+
"""
|
|
109
|
+
聚合多个 trial 的过程指标。
|
|
110
|
+
|
|
111
|
+
对每个指标计算 avg/min/max。
|
|
112
|
+
|
|
113
|
+
Args:
|
|
114
|
+
trials: trial 结果列表
|
|
115
|
+
|
|
116
|
+
Returns:
|
|
117
|
+
聚合后的指标字典,格式: {metric_name_{avg|min|max}: value}
|
|
118
|
+
"""
|
|
119
|
+
if not trials:
|
|
120
|
+
return {}
|
|
121
|
+
|
|
122
|
+
# 收集所有指标 key
|
|
123
|
+
all_keys: set[str] = set()
|
|
124
|
+
for t in trials:
|
|
125
|
+
all_keys.update(t.metrics.keys())
|
|
126
|
+
|
|
127
|
+
result: dict[str, float] = {}
|
|
128
|
+
for key in all_keys:
|
|
129
|
+
values = [t.metrics[key] for t in trials if key in t.metrics]
|
|
130
|
+
if values:
|
|
131
|
+
result[f"{key}_avg"] = sum(values) / len(values)
|
|
132
|
+
result[f"{key}_min"] = min(values)
|
|
133
|
+
result[f"{key}_max"] = max(values)
|
|
134
|
+
|
|
135
|
+
return result
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def extract_metrics(
|
|
139
|
+
spans: list[dict[str, Any]],
|
|
140
|
+
tracked_metrics: list[str],
|
|
141
|
+
) -> dict[str, float]:
|
|
142
|
+
"""
|
|
143
|
+
从 trace span 中提取过程指标。
|
|
144
|
+
|
|
145
|
+
Args:
|
|
146
|
+
spans: trace span 列表
|
|
147
|
+
tracked_metrics: 要提取的指标名称列表
|
|
148
|
+
|
|
149
|
+
Returns:
|
|
150
|
+
提取到的指标字典
|
|
151
|
+
"""
|
|
152
|
+
metrics: dict[str, float] = {}
|
|
153
|
+
|
|
154
|
+
# 基础统计
|
|
155
|
+
n_turns = 0
|
|
156
|
+
n_toolcalls = 0
|
|
157
|
+
n_total_tokens = 0
|
|
158
|
+
|
|
159
|
+
for span in spans:
|
|
160
|
+
name = span.get("name", "")
|
|
161
|
+
attrs = span.get("attributes", {})
|
|
162
|
+
|
|
163
|
+
# 对话轮次
|
|
164
|
+
if "turn" in name.lower() or "message" in name.lower():
|
|
165
|
+
n_turns += 1
|
|
166
|
+
|
|
167
|
+
# 工具调用
|
|
168
|
+
if "tool.call" in name or "tool_call" in name:
|
|
169
|
+
n_toolcalls += 1
|
|
170
|
+
|
|
171
|
+
# Token 用量
|
|
172
|
+
tokens = attrs.get("agenthub.total_tokens") or attrs.get("llm.usage.total_tokens")
|
|
173
|
+
if tokens:
|
|
174
|
+
with contextlib.suppress(ValueError, TypeError):
|
|
175
|
+
n_total_tokens += int(tokens)
|
|
176
|
+
|
|
177
|
+
if "n_turns" in tracked_metrics:
|
|
178
|
+
metrics["n_turns"] = float(n_turns)
|
|
179
|
+
if "n_toolcalls" in tracked_metrics:
|
|
180
|
+
metrics["n_toolcalls"] = float(n_toolcalls)
|
|
181
|
+
if "n_total_tokens" in tracked_metrics:
|
|
182
|
+
metrics["n_total_tokens"] = float(n_total_tokens)
|
|
183
|
+
|
|
184
|
+
return metrics
|