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,342 @@
|
|
|
1
|
+
"""
|
|
2
|
+
DatasetStorage — persistence for eval datasets, separate from run/suite storage.
|
|
3
|
+
|
|
4
|
+
Defines the DatasetStorage protocol plus SQLite and Memory implementations.
|
|
5
|
+
The implementations are composed onto the existing Storage instances
|
|
6
|
+
(`storage.datasets`) rather than opened as a second connection, so the API
|
|
7
|
+
layer reaches them through the same EvalRunner.storage instance.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import json
|
|
13
|
+
from typing import Protocol, runtime_checkable
|
|
14
|
+
|
|
15
|
+
import aiosqlite
|
|
16
|
+
|
|
17
|
+
from agent_eval.dataset.models import EvalDataset, EvalDatasetItem
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def _version_key(version: str) -> tuple[int, int, int]:
|
|
21
|
+
"""semver 排序键 (非法片段按 0 处理)"""
|
|
22
|
+
parts = []
|
|
23
|
+
for p in version.split("."):
|
|
24
|
+
try:
|
|
25
|
+
parts.append(int(p))
|
|
26
|
+
except ValueError:
|
|
27
|
+
parts.append(0)
|
|
28
|
+
while len(parts) < 3:
|
|
29
|
+
parts.append(0)
|
|
30
|
+
return tuple(parts[:3])
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
@runtime_checkable
|
|
34
|
+
class DatasetStorage(Protocol):
|
|
35
|
+
"""数据集存储接口 (datasets / dataset_items)"""
|
|
36
|
+
|
|
37
|
+
async def save_dataset(self, dataset: EvalDataset) -> None:
|
|
38
|
+
"""保存数据集 (非空 items 同步入条目表)"""
|
|
39
|
+
...
|
|
40
|
+
|
|
41
|
+
async def get_dataset(self, dataset_id: str) -> EvalDataset | None:
|
|
42
|
+
"""按 ID 获取数据集 (含条目)"""
|
|
43
|
+
...
|
|
44
|
+
|
|
45
|
+
async def get_dataset_by_name(
|
|
46
|
+
self, name: str, version: str | None = None
|
|
47
|
+
) -> EvalDataset | None:
|
|
48
|
+
"""按名称查询; version 为 None 时返回最高版本"""
|
|
49
|
+
...
|
|
50
|
+
|
|
51
|
+
async def list_datasets(self, tags: list[str] | None = None) -> list[EvalDataset]:
|
|
52
|
+
"""列出数据集; tags 非空时按标签过滤 (任一命中)"""
|
|
53
|
+
...
|
|
54
|
+
|
|
55
|
+
async def delete_dataset(self, dataset_id: str) -> bool:
|
|
56
|
+
"""删除数据集 (级联删除条目)"""
|
|
57
|
+
...
|
|
58
|
+
|
|
59
|
+
async def save_dataset_item(self, dataset_id: str, item: EvalDatasetItem) -> None:
|
|
60
|
+
"""新增或更新条目"""
|
|
61
|
+
...
|
|
62
|
+
|
|
63
|
+
async def get_dataset_items(self, dataset_id: str) -> list[EvalDatasetItem]:
|
|
64
|
+
"""列出数据集全部条目"""
|
|
65
|
+
...
|
|
66
|
+
|
|
67
|
+
async def get_dataset_item(
|
|
68
|
+
self, dataset_id: str, item_id: str
|
|
69
|
+
) -> EvalDatasetItem | None:
|
|
70
|
+
"""按 ID 获取单条条目"""
|
|
71
|
+
...
|
|
72
|
+
|
|
73
|
+
async def delete_dataset_item(self, dataset_id: str, item_id: str) -> bool:
|
|
74
|
+
"""删除单条条目"""
|
|
75
|
+
...
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def _strip_items(dataset: EvalDataset) -> EvalDataset:
|
|
79
|
+
"""数据集主记录不含条目 (条目单独存表)"""
|
|
80
|
+
return dataset.model_copy(update={"items": []})
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
class MemoryDatasetStorage:
|
|
84
|
+
"""内存数据集存储 — 用于测试与短生命周期场景"""
|
|
85
|
+
|
|
86
|
+
def __init__(self) -> None:
|
|
87
|
+
self._datasets: dict[str, EvalDataset] = {}
|
|
88
|
+
self._items: dict[str, dict[str, EvalDatasetItem]] = {}
|
|
89
|
+
|
|
90
|
+
async def save_dataset(self, dataset: EvalDataset) -> None:
|
|
91
|
+
self._datasets[dataset.id] = _strip_items(dataset)
|
|
92
|
+
bucket = self._items.setdefault(dataset.id, {})
|
|
93
|
+
for item in dataset.items:
|
|
94
|
+
bucket[item.id] = item
|
|
95
|
+
|
|
96
|
+
async def get_dataset(self, dataset_id: str) -> EvalDataset | None:
|
|
97
|
+
stored = self._datasets.get(dataset_id)
|
|
98
|
+
if stored is None:
|
|
99
|
+
return None
|
|
100
|
+
items = list(self._items.get(dataset_id, {}).values())
|
|
101
|
+
return stored.model_copy(update={"items": items})
|
|
102
|
+
|
|
103
|
+
async def get_dataset_by_name(
|
|
104
|
+
self, name: str, version: str | None = None
|
|
105
|
+
) -> EvalDataset | None:
|
|
106
|
+
candidates = [d for d in self._datasets.values() if d.name == name]
|
|
107
|
+
if version is not None:
|
|
108
|
+
candidates = [d for d in candidates if d.version == version]
|
|
109
|
+
if not candidates:
|
|
110
|
+
return None
|
|
111
|
+
best = max(candidates, key=lambda d: _version_key(d.version))
|
|
112
|
+
return await self.get_dataset(best.id)
|
|
113
|
+
|
|
114
|
+
async def list_datasets(self, tags: list[str] | None = None) -> list[EvalDataset]:
|
|
115
|
+
result = []
|
|
116
|
+
for dataset_id in self._datasets:
|
|
117
|
+
dataset = await self.get_dataset(dataset_id)
|
|
118
|
+
assert dataset is not None
|
|
119
|
+
if tags and not set(tags) & set(dataset.tags):
|
|
120
|
+
continue
|
|
121
|
+
result.append(dataset)
|
|
122
|
+
result.sort(key=lambda d: d.created_at, reverse=True)
|
|
123
|
+
return result
|
|
124
|
+
|
|
125
|
+
async def delete_dataset(self, dataset_id: str) -> bool:
|
|
126
|
+
if dataset_id not in self._datasets:
|
|
127
|
+
return False
|
|
128
|
+
del self._datasets[dataset_id]
|
|
129
|
+
self._items.pop(dataset_id, None)
|
|
130
|
+
return True
|
|
131
|
+
|
|
132
|
+
async def save_dataset_item(self, dataset_id: str, item: EvalDatasetItem) -> None:
|
|
133
|
+
if dataset_id not in self._datasets:
|
|
134
|
+
raise KeyError(f"Dataset '{dataset_id}' not found")
|
|
135
|
+
self._items.setdefault(dataset_id, {})[item.id] = item
|
|
136
|
+
|
|
137
|
+
async def get_dataset_items(self, dataset_id: str) -> list[EvalDatasetItem]:
|
|
138
|
+
return list(self._items.get(dataset_id, {}).values())
|
|
139
|
+
|
|
140
|
+
async def get_dataset_item(
|
|
141
|
+
self, dataset_id: str, item_id: str
|
|
142
|
+
) -> EvalDatasetItem | None:
|
|
143
|
+
return self._items.get(dataset_id, {}).get(item_id)
|
|
144
|
+
|
|
145
|
+
async def delete_dataset_item(self, dataset_id: str, item_id: str) -> bool:
|
|
146
|
+
bucket = self._items.get(dataset_id)
|
|
147
|
+
if bucket and item_id in bucket:
|
|
148
|
+
del bucket[item_id]
|
|
149
|
+
return True
|
|
150
|
+
return False
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
class SqliteDatasetStorage:
|
|
154
|
+
"""SQLite 数据集存储 — 与 run/suite 存储共用同一 db 文件"""
|
|
155
|
+
|
|
156
|
+
def __init__(self, db_path: str):
|
|
157
|
+
self.db_path = db_path
|
|
158
|
+
|
|
159
|
+
async def initialize(self) -> None:
|
|
160
|
+
"""建表 (由宿主 SqliteStorage.initialize() 调用)"""
|
|
161
|
+
async with aiosqlite.connect(self.db_path) as db:
|
|
162
|
+
await db.executescript("""
|
|
163
|
+
CREATE TABLE IF NOT EXISTS datasets (
|
|
164
|
+
id TEXT PRIMARY KEY,
|
|
165
|
+
name TEXT,
|
|
166
|
+
version TEXT,
|
|
167
|
+
data TEXT,
|
|
168
|
+
created_at REAL,
|
|
169
|
+
updated_at REAL
|
|
170
|
+
);
|
|
171
|
+
|
|
172
|
+
CREATE TABLE IF NOT EXISTS dataset_items (
|
|
173
|
+
dataset_id TEXT,
|
|
174
|
+
item_id TEXT,
|
|
175
|
+
data TEXT,
|
|
176
|
+
created_at REAL,
|
|
177
|
+
PRIMARY KEY (dataset_id, item_id)
|
|
178
|
+
);
|
|
179
|
+
|
|
180
|
+
CREATE INDEX IF NOT EXISTS idx_datasets_name
|
|
181
|
+
ON datasets(name);
|
|
182
|
+
CREATE INDEX IF NOT EXISTS idx_dataset_items_dataset
|
|
183
|
+
ON dataset_items(dataset_id);
|
|
184
|
+
""")
|
|
185
|
+
await db.commit()
|
|
186
|
+
|
|
187
|
+
# ── Dataset 操作 ──
|
|
188
|
+
|
|
189
|
+
async def save_dataset(self, dataset: EvalDataset) -> None:
|
|
190
|
+
async with aiosqlite.connect(self.db_path) as db:
|
|
191
|
+
await db.execute(
|
|
192
|
+
"""INSERT OR REPLACE INTO datasets
|
|
193
|
+
(id, name, version, data, created_at, updated_at)
|
|
194
|
+
VALUES (?, ?, ?, ?, ?, ?)""",
|
|
195
|
+
(
|
|
196
|
+
dataset.id,
|
|
197
|
+
dataset.name,
|
|
198
|
+
dataset.version,
|
|
199
|
+
json.dumps(_strip_items(dataset).model_dump(), default=str),
|
|
200
|
+
dataset.created_at,
|
|
201
|
+
dataset.updated_at,
|
|
202
|
+
),
|
|
203
|
+
)
|
|
204
|
+
for item in dataset.items:
|
|
205
|
+
await db.execute(
|
|
206
|
+
"""INSERT OR REPLACE INTO dataset_items
|
|
207
|
+
(dataset_id, item_id, data, created_at)
|
|
208
|
+
VALUES (?, ?, ?, ?)""",
|
|
209
|
+
(
|
|
210
|
+
dataset.id,
|
|
211
|
+
item.id,
|
|
212
|
+
json.dumps(item.model_dump(), default=str),
|
|
213
|
+
item.created_at,
|
|
214
|
+
),
|
|
215
|
+
)
|
|
216
|
+
await db.commit()
|
|
217
|
+
|
|
218
|
+
async def get_dataset(self, dataset_id: str) -> EvalDataset | None:
|
|
219
|
+
async with aiosqlite.connect(self.db_path) as db:
|
|
220
|
+
db.row_factory = aiosqlite.Row
|
|
221
|
+
cursor = await db.execute(
|
|
222
|
+
"SELECT data FROM datasets WHERE id = ?", (dataset_id,)
|
|
223
|
+
)
|
|
224
|
+
row = await cursor.fetchone()
|
|
225
|
+
if row is None:
|
|
226
|
+
return None
|
|
227
|
+
data = json.loads(row["data"])
|
|
228
|
+
cursor = await db.execute(
|
|
229
|
+
"SELECT data FROM dataset_items WHERE dataset_id = ? "
|
|
230
|
+
"ORDER BY created_at ASC, item_id ASC",
|
|
231
|
+
(dataset_id,),
|
|
232
|
+
)
|
|
233
|
+
item_rows = await cursor.fetchall()
|
|
234
|
+
data["items"] = [json.loads(r["data"]) for r in item_rows]
|
|
235
|
+
return EvalDataset(**data)
|
|
236
|
+
|
|
237
|
+
async def get_dataset_by_name(
|
|
238
|
+
self, name: str, version: str | None = None
|
|
239
|
+
) -> EvalDataset | None:
|
|
240
|
+
async with aiosqlite.connect(self.db_path) as db:
|
|
241
|
+
db.row_factory = aiosqlite.Row
|
|
242
|
+
if version is not None:
|
|
243
|
+
cursor = await db.execute(
|
|
244
|
+
"SELECT id, version FROM datasets WHERE name = ? AND version = ?",
|
|
245
|
+
(name, version),
|
|
246
|
+
)
|
|
247
|
+
else:
|
|
248
|
+
# 同名多版本 → 一次查询, Python 侧按 semver 取最高
|
|
249
|
+
cursor = await db.execute(
|
|
250
|
+
"SELECT id, version FROM datasets WHERE name = ?", (name,)
|
|
251
|
+
)
|
|
252
|
+
rows = await cursor.fetchall()
|
|
253
|
+
if not rows:
|
|
254
|
+
return None
|
|
255
|
+
best = max(rows, key=lambda r: _version_key(r["version"]))
|
|
256
|
+
return await self.get_dataset(best["id"])
|
|
257
|
+
|
|
258
|
+
async def list_datasets(self, tags: list[str] | None = None) -> list[EvalDataset]:
|
|
259
|
+
async with aiosqlite.connect(self.db_path) as db:
|
|
260
|
+
db.row_factory = aiosqlite.Row
|
|
261
|
+
cursor = await db.execute(
|
|
262
|
+
"SELECT id FROM datasets ORDER BY created_at DESC"
|
|
263
|
+
)
|
|
264
|
+
rows = await cursor.fetchall()
|
|
265
|
+
datasets = []
|
|
266
|
+
for row in rows:
|
|
267
|
+
dataset = await self.get_dataset(row["id"])
|
|
268
|
+
if dataset is None:
|
|
269
|
+
continue
|
|
270
|
+
if tags and not set(tags) & set(dataset.tags):
|
|
271
|
+
continue
|
|
272
|
+
datasets.append(dataset)
|
|
273
|
+
return datasets
|
|
274
|
+
|
|
275
|
+
async def delete_dataset(self, dataset_id: str) -> bool:
|
|
276
|
+
async with aiosqlite.connect(self.db_path) as db:
|
|
277
|
+
cursor = await db.execute(
|
|
278
|
+
"DELETE FROM datasets WHERE id = ?", (dataset_id,)
|
|
279
|
+
)
|
|
280
|
+
await db.execute(
|
|
281
|
+
"DELETE FROM dataset_items WHERE dataset_id = ?", (dataset_id,)
|
|
282
|
+
)
|
|
283
|
+
await db.commit()
|
|
284
|
+
return cursor.rowcount > 0
|
|
285
|
+
|
|
286
|
+
# ── Item 操作 ──
|
|
287
|
+
|
|
288
|
+
async def save_dataset_item(self, dataset_id: str, item: EvalDatasetItem) -> None:
|
|
289
|
+
async with aiosqlite.connect(self.db_path) as db:
|
|
290
|
+
db.row_factory = aiosqlite.Row
|
|
291
|
+
cursor = await db.execute(
|
|
292
|
+
"SELECT id FROM datasets WHERE id = ?", (dataset_id,)
|
|
293
|
+
)
|
|
294
|
+
if await cursor.fetchone() is None:
|
|
295
|
+
raise KeyError(f"Dataset '{dataset_id}' not found")
|
|
296
|
+
await db.execute(
|
|
297
|
+
"""INSERT OR REPLACE INTO dataset_items
|
|
298
|
+
(dataset_id, item_id, data, created_at)
|
|
299
|
+
VALUES (?, ?, ?, ?)""",
|
|
300
|
+
(
|
|
301
|
+
dataset_id,
|
|
302
|
+
item.id,
|
|
303
|
+
json.dumps(item.model_dump(), default=str),
|
|
304
|
+
item.created_at,
|
|
305
|
+
),
|
|
306
|
+
)
|
|
307
|
+
await db.commit()
|
|
308
|
+
|
|
309
|
+
async def get_dataset_items(self, dataset_id: str) -> list[EvalDatasetItem]:
|
|
310
|
+
async with aiosqlite.connect(self.db_path) as db:
|
|
311
|
+
db.row_factory = aiosqlite.Row
|
|
312
|
+
cursor = await db.execute(
|
|
313
|
+
"SELECT data FROM dataset_items WHERE dataset_id = ? "
|
|
314
|
+
"ORDER BY created_at ASC, item_id ASC",
|
|
315
|
+
(dataset_id,),
|
|
316
|
+
)
|
|
317
|
+
rows = await cursor.fetchall()
|
|
318
|
+
return [EvalDatasetItem(**json.loads(r["data"])) for r in rows]
|
|
319
|
+
|
|
320
|
+
async def get_dataset_item(
|
|
321
|
+
self, dataset_id: str, item_id: str
|
|
322
|
+
) -> EvalDatasetItem | None:
|
|
323
|
+
async with aiosqlite.connect(self.db_path) as db:
|
|
324
|
+
db.row_factory = aiosqlite.Row
|
|
325
|
+
cursor = await db.execute(
|
|
326
|
+
"SELECT data FROM dataset_items WHERE dataset_id = ? AND item_id = ?",
|
|
327
|
+
(dataset_id, item_id),
|
|
328
|
+
)
|
|
329
|
+
row = await cursor.fetchone()
|
|
330
|
+
if row is None:
|
|
331
|
+
return None
|
|
332
|
+
return EvalDatasetItem(**json.loads(row["data"]))
|
|
333
|
+
|
|
334
|
+
async def delete_dataset_item(self, dataset_id: str, item_id: str) -> bool:
|
|
335
|
+
async with aiosqlite.connect(self.db_path) as db:
|
|
336
|
+
cursor = await db.execute(
|
|
337
|
+
"DELETE FROM dataset_items WHERE dataset_id = ? AND item_id = ?",
|
|
338
|
+
(dataset_id, item_id),
|
|
339
|
+
)
|
|
340
|
+
await db.commit()
|
|
341
|
+
return cursor.rowcount > 0
|
|
342
|
+
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
"""Dataset semantic versioning — semver bump rules with a change log.
|
|
2
|
+
|
|
3
|
+
Rules (design §18.5.1):
|
|
4
|
+
- major: 破坏性变更 (删除条目、修改评分器)
|
|
5
|
+
- minor: 新增条目
|
|
6
|
+
- patch: 修正描述、调整阈值等不改变任务集合语义的修正
|
|
7
|
+
|
|
8
|
+
Every bump appends a change-record entry to the dataset's change_log so the
|
|
9
|
+
history is persisted with the dataset.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
from typing import Literal
|
|
15
|
+
|
|
16
|
+
from agent_eval.dataset.models import DatasetError, EvalDataset, now_ms
|
|
17
|
+
|
|
18
|
+
ChangeType = Literal["major", "minor", "patch"]
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class DatasetVersionManager:
|
|
22
|
+
"""数据集版本管理 (无状态, 纯函数式更新)"""
|
|
23
|
+
|
|
24
|
+
@staticmethod
|
|
25
|
+
def parse_version(version: str) -> tuple[int, int, int]:
|
|
26
|
+
parts = version.split(".")
|
|
27
|
+
if len(parts) != 3:
|
|
28
|
+
raise DatasetError(f"Invalid semver version '{version}' (expected major.minor.patch)")
|
|
29
|
+
try:
|
|
30
|
+
return int(parts[0]), int(parts[1]), int(parts[2])
|
|
31
|
+
except ValueError:
|
|
32
|
+
raise DatasetError(f"Invalid semver version '{version}' (non-numeric part)") from None
|
|
33
|
+
|
|
34
|
+
@classmethod
|
|
35
|
+
def bump_version(cls, version: str, change_type: ChangeType) -> str:
|
|
36
|
+
major, minor, patch = cls.parse_version(version)
|
|
37
|
+
if change_type == "major":
|
|
38
|
+
return f"{major + 1}.0.0"
|
|
39
|
+
if change_type == "minor":
|
|
40
|
+
return f"{major}.{minor + 1}.0"
|
|
41
|
+
if change_type == "patch":
|
|
42
|
+
return f"{major}.{minor}.{patch + 1}"
|
|
43
|
+
raise DatasetError(
|
|
44
|
+
f"Invalid change_type '{change_type}' (valid: major, minor, patch)"
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
@classmethod
|
|
48
|
+
def bump(
|
|
49
|
+
cls,
|
|
50
|
+
dataset: EvalDataset,
|
|
51
|
+
change_type: ChangeType,
|
|
52
|
+
change_note: str = "",
|
|
53
|
+
) -> EvalDataset:
|
|
54
|
+
"""
|
|
55
|
+
升版并记录变更。
|
|
56
|
+
|
|
57
|
+
Returns:
|
|
58
|
+
升版后的 dataset 副本 (version/updated_at/change_log 更新)
|
|
59
|
+
"""
|
|
60
|
+
new_version = cls.bump_version(dataset.version, change_type)
|
|
61
|
+
entry = {
|
|
62
|
+
"version": new_version,
|
|
63
|
+
"change_type": change_type,
|
|
64
|
+
"note": change_note,
|
|
65
|
+
"at": now_ms(),
|
|
66
|
+
"item_count": len(dataset.items),
|
|
67
|
+
}
|
|
68
|
+
return dataset.model_copy(update={
|
|
69
|
+
"version": new_version,
|
|
70
|
+
"updated_at": entry["at"],
|
|
71
|
+
"change_log": [*dataset.change_log, entry],
|
|
72
|
+
})
|
|
File without changes
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Basic usage example for Aeval.
|
|
3
|
+
|
|
4
|
+
Demonstrates how to:
|
|
5
|
+
1. Define an EvalSuite
|
|
6
|
+
2. Create an EvalRunner
|
|
7
|
+
3. Run the suite
|
|
8
|
+
4. View results
|
|
9
|
+
|
|
10
|
+
Run this file to verify the framework works:
|
|
11
|
+
python -m agent_eval.examples.basic_usage
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
import asyncio
|
|
17
|
+
|
|
18
|
+
from agent_eval.core.runner import EvalRunner
|
|
19
|
+
from agent_eval.core.types import (
|
|
20
|
+
EvalSuite,
|
|
21
|
+
EvalTask,
|
|
22
|
+
GraderConfig,
|
|
23
|
+
GraderType,
|
|
24
|
+
ScoreStrategy,
|
|
25
|
+
)
|
|
26
|
+
from agent_eval.examples.mock_runner import MockAgentRunner, MockTraceProvider
|
|
27
|
+
from agent_eval.storage import MemoryStorage
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def create_demo_suite() -> EvalSuite:
|
|
31
|
+
"""创建一个演示评测套件"""
|
|
32
|
+
return EvalSuite(
|
|
33
|
+
name="Demo Eval Suite",
|
|
34
|
+
description="A simple demo suite to verify the framework",
|
|
35
|
+
tasks=[
|
|
36
|
+
EvalTask(
|
|
37
|
+
id="hello-world",
|
|
38
|
+
description="Agent should say hello",
|
|
39
|
+
prompt="Say hello to me",
|
|
40
|
+
graders=[
|
|
41
|
+
GraderConfig(
|
|
42
|
+
type=GraderType.CODE,
|
|
43
|
+
name="code_based",
|
|
44
|
+
required=True,
|
|
45
|
+
config={
|
|
46
|
+
"checks": [
|
|
47
|
+
{
|
|
48
|
+
"type": "contains",
|
|
49
|
+
"value": "Mock response",
|
|
50
|
+
"target": "transcript",
|
|
51
|
+
}
|
|
52
|
+
],
|
|
53
|
+
"threshold": 1.0,
|
|
54
|
+
},
|
|
55
|
+
),
|
|
56
|
+
GraderConfig(
|
|
57
|
+
type=GraderType.ARTIFACT,
|
|
58
|
+
name="artifact_check",
|
|
59
|
+
config={
|
|
60
|
+
"expected_type": "code_file",
|
|
61
|
+
},
|
|
62
|
+
),
|
|
63
|
+
],
|
|
64
|
+
max_trials=3,
|
|
65
|
+
score_strategy=ScoreStrategy.HYBRID,
|
|
66
|
+
score_threshold=0.5,
|
|
67
|
+
),
|
|
68
|
+
EvalTask(
|
|
69
|
+
id="file-creation",
|
|
70
|
+
description="Agent should create a file",
|
|
71
|
+
prompt="Create a hello.py file",
|
|
72
|
+
graders=[
|
|
73
|
+
GraderConfig(
|
|
74
|
+
type=GraderType.STATE,
|
|
75
|
+
name="state_check",
|
|
76
|
+
required=True,
|
|
77
|
+
config={
|
|
78
|
+
"expectations": [
|
|
79
|
+
{
|
|
80
|
+
"type": "file_contains",
|
|
81
|
+
"path": "output.py",
|
|
82
|
+
"value": "hello",
|
|
83
|
+
}
|
|
84
|
+
],
|
|
85
|
+
"threshold": 1.0,
|
|
86
|
+
},
|
|
87
|
+
),
|
|
88
|
+
GraderConfig(
|
|
89
|
+
type=GraderType.TOOL_CALLS,
|
|
90
|
+
name="tool_calls",
|
|
91
|
+
config={
|
|
92
|
+
"required_tools": ["fs_write"],
|
|
93
|
+
},
|
|
94
|
+
),
|
|
95
|
+
],
|
|
96
|
+
max_trials=3,
|
|
97
|
+
score_strategy=ScoreStrategy.ALL_PASS,
|
|
98
|
+
),
|
|
99
|
+
],
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
async def main():
|
|
104
|
+
"""Run the demo"""
|
|
105
|
+
print("=" * 60)
|
|
106
|
+
print("Aeval Framework — Demo Run")
|
|
107
|
+
print("=" * 60)
|
|
108
|
+
|
|
109
|
+
# 1. Create components
|
|
110
|
+
agent_runner = MockAgentRunner(success_rate=0.8)
|
|
111
|
+
trace_provider = MockTraceProvider()
|
|
112
|
+
storage = MemoryStorage()
|
|
113
|
+
|
|
114
|
+
# 2. Create EvalRunner
|
|
115
|
+
runner = EvalRunner(
|
|
116
|
+
agent_runner=agent_runner,
|
|
117
|
+
trace_provider=trace_provider,
|
|
118
|
+
storage=storage,
|
|
119
|
+
concurrency=2,
|
|
120
|
+
)
|
|
121
|
+
|
|
122
|
+
# 3. Create suite
|
|
123
|
+
suite = create_demo_suite()
|
|
124
|
+
print(f"\nSuite: {suite.name}")
|
|
125
|
+
print(f"Tasks: {len(suite.tasks)}")
|
|
126
|
+
print(f"Trials per task: {suite.tasks[0].max_trials}")
|
|
127
|
+
|
|
128
|
+
# 4. Run suite
|
|
129
|
+
print("\nRunning evaluation...")
|
|
130
|
+
result = await runner.run_suite(suite)
|
|
131
|
+
|
|
132
|
+
# 5. Display results
|
|
133
|
+
print("\n" + "=" * 60)
|
|
134
|
+
print("Results")
|
|
135
|
+
print("=" * 60)
|
|
136
|
+
|
|
137
|
+
print(f"\nRun ID: {result.run_id}")
|
|
138
|
+
print(f"Status: {result.status}")
|
|
139
|
+
print(f"Duration: {result.duration_ms:.0f}ms" if result.duration_ms else "N/A")
|
|
140
|
+
|
|
141
|
+
if result.summary:
|
|
142
|
+
summary = result.summary
|
|
143
|
+
print(f"\nTotal Tasks: {summary.total_tasks}")
|
|
144
|
+
print(f"Total Trials: {summary.total_trials}")
|
|
145
|
+
print(f"Average Score: {summary.avg_score:.2f}")
|
|
146
|
+
|
|
147
|
+
if 1 in summary.pass_at_k:
|
|
148
|
+
print(f"Pass@1: {summary.pass_at_k[1]:.2f}")
|
|
149
|
+
if 3 in summary.pass_at_k:
|
|
150
|
+
print(f"Pass@3: {summary.pass_at_k[3]:.2f}")
|
|
151
|
+
if 1 in summary.pass_power_k:
|
|
152
|
+
print(f"Pass^1: {summary.pass_power_k[1]:.2f}")
|
|
153
|
+
if 3 in summary.pass_power_k:
|
|
154
|
+
print(f"Pass^3: {summary.pass_power_k[3]:.2f}")
|
|
155
|
+
|
|
156
|
+
if summary.failures:
|
|
157
|
+
print(f"\nFailed Tasks: {', '.join(summary.failures)}")
|
|
158
|
+
|
|
159
|
+
print("\nPer-Task Results:")
|
|
160
|
+
for ts in summary.task_summaries:
|
|
161
|
+
status = "✅" if not ts.failures else "❌"
|
|
162
|
+
print(
|
|
163
|
+
f" {status} {ts.task_id}: "
|
|
164
|
+
f"avg_score={ts.avg_score:.2f}, "
|
|
165
|
+
f"trials={ts.total_trials}, "
|
|
166
|
+
f"failures={len(ts.failures)}"
|
|
167
|
+
)
|
|
168
|
+
|
|
169
|
+
print("\n" + "=" * 60)
|
|
170
|
+
print("Demo complete!")
|
|
171
|
+
print("=" * 60)
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
if __name__ == "__main__":
|
|
175
|
+
asyncio.run(main())
|