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
agent_eval/core/suite.py
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Suite loading from YAML with strict validation.
|
|
3
|
+
|
|
4
|
+
All validation rules (semver version, unique task ids, grader name format,
|
|
5
|
+
weight >= 0, sample_count 1-10, name <= 128 chars, at least one task) live on
|
|
6
|
+
the Pydantic models in core/types.py so both YAML loading and API JSON
|
|
7
|
+
creation validate identically. This module wraps load/validation failures
|
|
8
|
+
with file-path context.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
|
|
15
|
+
import yaml
|
|
16
|
+
from pydantic import ValidationError
|
|
17
|
+
|
|
18
|
+
from agent_eval.core.types import EvalSuite
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class SuiteLoadError(Exception):
|
|
22
|
+
"""Suite YAML 加载或校验失败 (错误信息包含文件路径上下文)。"""
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def load_suite(path: str | Path) -> EvalSuite:
|
|
26
|
+
"""
|
|
27
|
+
从 YAML 文件加载评测套件。
|
|
28
|
+
|
|
29
|
+
Args:
|
|
30
|
+
path: YAML 文件路径
|
|
31
|
+
|
|
32
|
+
Returns:
|
|
33
|
+
校验通过的 EvalSuite
|
|
34
|
+
|
|
35
|
+
Raises:
|
|
36
|
+
SuiteLoadError: 文件不存在 / YAML 语法错误 / 校验失败 (均含文件路径)
|
|
37
|
+
"""
|
|
38
|
+
suite_path = Path(path)
|
|
39
|
+
|
|
40
|
+
if not suite_path.exists():
|
|
41
|
+
raise SuiteLoadError(f"Suite file not found: {suite_path}")
|
|
42
|
+
|
|
43
|
+
try:
|
|
44
|
+
with open(suite_path, encoding="utf-8") as f:
|
|
45
|
+
data = yaml.safe_load(f)
|
|
46
|
+
except OSError as e:
|
|
47
|
+
raise SuiteLoadError(f"Cannot read suite file '{suite_path}': {e}") from e
|
|
48
|
+
except yaml.YAMLError as e:
|
|
49
|
+
raise SuiteLoadError(f"Invalid YAML in suite file '{suite_path}': {e}") from e
|
|
50
|
+
|
|
51
|
+
if not isinstance(data, dict):
|
|
52
|
+
raise SuiteLoadError(
|
|
53
|
+
f"Suite file '{suite_path}' must contain a YAML mapping "
|
|
54
|
+
f"(got {type(data).__name__})"
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
try:
|
|
58
|
+
return EvalSuite(**data)
|
|
59
|
+
except ValidationError as e:
|
|
60
|
+
raise SuiteLoadError(f"Suite validation failed for '{suite_path}':\n{e}") from e
|
agent_eval/core/types.py
ADDED
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Core data types for the Aeval evaluation framework.
|
|
3
|
+
|
|
4
|
+
This module defines all the data models used throughout the framework:
|
|
5
|
+
- Task definition layer: EvalTask, EvalSuite, GraderConfig
|
|
6
|
+
- Run result layer: TrialResult, GraderResult, TaskSummary, RunSummary, RunResult
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import time
|
|
12
|
+
import uuid
|
|
13
|
+
from enum import Enum
|
|
14
|
+
from typing import Any, Literal
|
|
15
|
+
|
|
16
|
+
from pydantic import BaseModel, Field, field_validator, model_validator
|
|
17
|
+
|
|
18
|
+
# ─── Task Definition Layer ───────────────────────────────────────────────────
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class GraderType(str, Enum):
|
|
22
|
+
"""评分器类型枚举"""
|
|
23
|
+
|
|
24
|
+
CODE = "code" # 确定性评分 (字符串匹配/正则/静态分析)
|
|
25
|
+
MODEL = "model" # LLM Judge
|
|
26
|
+
STATE = "state" # 环境状态检查
|
|
27
|
+
TOOL_CALLS = "tool_calls" # 工具调用验证
|
|
28
|
+
TRANSCRIPT = "transcript" # 转录记录分析
|
|
29
|
+
ARTIFACT = "artifact" # 产物检查
|
|
30
|
+
METRIC = "metric" # LLM 输出质量指标 (AnswerRelevancy/Faithfulness/...)
|
|
31
|
+
CUSTOM = "custom" # 自定义
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class ScoreStrategy(str, Enum):
|
|
35
|
+
"""评分聚合策略"""
|
|
36
|
+
|
|
37
|
+
ALL_PASS = "all_pass" # 所有 grader 必须通过
|
|
38
|
+
WEIGHTED = "weighted" # 加权平均
|
|
39
|
+
HYBRID = "hybrid" # required 必须通过 + 非 required 加权
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class GraderConfig(BaseModel):
|
|
43
|
+
"""单个评分器的配置"""
|
|
44
|
+
|
|
45
|
+
type: GraderType
|
|
46
|
+
name: str = Field(
|
|
47
|
+
...,
|
|
48
|
+
min_length=1,
|
|
49
|
+
pattern=r"^[a-zA-Z][a-zA-Z0-9_-]*$",
|
|
50
|
+
description="评分器名称 (用于注册/查找)",
|
|
51
|
+
)
|
|
52
|
+
weight: float = Field(1.0, ge=0.0, description="权重 (用于加权评分)")
|
|
53
|
+
required: bool = Field(False, description="是否必须通过")
|
|
54
|
+
sample_count: int = Field(
|
|
55
|
+
1, ge=1, le=10, description="采样次数 (LLM Judge 多采样计算 confidence)"
|
|
56
|
+
)
|
|
57
|
+
dependencies: list[str] = Field(
|
|
58
|
+
default_factory=list,
|
|
59
|
+
description="依赖的其他 grader 名称 (拓扑排序, 依赖未通过则跳过)",
|
|
60
|
+
)
|
|
61
|
+
config: dict[str, Any] = Field(default_factory=dict, description="类型特定的配置")
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
class EvalTask(BaseModel):
|
|
65
|
+
"""单个评测任务"""
|
|
66
|
+
|
|
67
|
+
id: str = Field(..., min_length=1, description="唯一标识")
|
|
68
|
+
description: str = Field("", description="人类可读描述")
|
|
69
|
+
prompt: str = Field(..., description="给 Agent 的输入")
|
|
70
|
+
graders: list[GraderConfig] = Field(..., min_length=1, description="评分器列表")
|
|
71
|
+
env: dict[str, Any] = Field(default_factory=dict, description="环境参数 (透传给 AgentRunner)")
|
|
72
|
+
max_trials: int = Field(3, ge=1, description="默认 trial 数")
|
|
73
|
+
score_strategy: ScoreStrategy = Field(ScoreStrategy.HYBRID, description="评分聚合策略")
|
|
74
|
+
score_threshold: float = Field(0.7, ge=0.0, le=1.0, description="通过阈值 (用于 WEIGHTED/HYBRID)")
|
|
75
|
+
tracked_metrics: list[str] = Field(
|
|
76
|
+
default_factory=lambda: [
|
|
77
|
+
"n_turns",
|
|
78
|
+
"n_toolcalls",
|
|
79
|
+
"n_total_tokens",
|
|
80
|
+
"latency_ms",
|
|
81
|
+
],
|
|
82
|
+
description="从 trace 提取的过程指标",
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
def get_grader_config(self, name: str) -> dict[str, Any]:
|
|
86
|
+
"""获取指定名称的评分器配置"""
|
|
87
|
+
for g in self.graders:
|
|
88
|
+
if g.name == name:
|
|
89
|
+
return g.config
|
|
90
|
+
return {}
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
class EvalSuite(BaseModel):
|
|
94
|
+
"""评测套件 (一组任务)"""
|
|
95
|
+
|
|
96
|
+
name: str = Field(..., description="套件名称")
|
|
97
|
+
description: str = Field("", description="套件描述")
|
|
98
|
+
version: str = Field(
|
|
99
|
+
"1.0.0",
|
|
100
|
+
pattern=r"^\d+\.\d+\.\d+$",
|
|
101
|
+
description="语义化版本 (semver)",
|
|
102
|
+
)
|
|
103
|
+
tasks: list[EvalTask] = Field(..., min_length=1, description="任务列表")
|
|
104
|
+
metadata: dict[str, Any] = Field(default_factory=dict, description="自定义元数据")
|
|
105
|
+
|
|
106
|
+
@field_validator("name")
|
|
107
|
+
@classmethod
|
|
108
|
+
def _validate_name(cls, v: str) -> str:
|
|
109
|
+
if not v or not v.strip():
|
|
110
|
+
raise ValueError("Suite name cannot be empty")
|
|
111
|
+
if len(v) > 128:
|
|
112
|
+
raise ValueError("Suite name too long (max 128 chars)")
|
|
113
|
+
return v
|
|
114
|
+
|
|
115
|
+
@model_validator(mode="after")
|
|
116
|
+
def _validate_task_ids_unique(self) -> EvalSuite:
|
|
117
|
+
ids = [t.id for t in self.tasks]
|
|
118
|
+
if len(ids) != len(set(ids)):
|
|
119
|
+
duplicates = sorted({x for x in ids if ids.count(x) > 1})
|
|
120
|
+
raise ValueError(f"Duplicate task IDs: {duplicates}")
|
|
121
|
+
return self
|
|
122
|
+
|
|
123
|
+
@classmethod
|
|
124
|
+
def from_yaml(cls, path: str) -> EvalSuite:
|
|
125
|
+
"""从 YAML 文件加载评测套件 (含严格校验, 错误带文件路径上下文)"""
|
|
126
|
+
from agent_eval.core.suite import load_suite
|
|
127
|
+
|
|
128
|
+
return load_suite(path)
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
# ─── Run Result Layer ─────────────────────────────────────────────────────────
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
class GraderResult(BaseModel):
|
|
135
|
+
"""单个评分器的评分结果"""
|
|
136
|
+
|
|
137
|
+
grader_name: str
|
|
138
|
+
grader_type: GraderType
|
|
139
|
+
score: float = Field(..., ge=0.0, le=1.0, description="评分 0.0-1.0")
|
|
140
|
+
passed: bool = Field(..., description="是否通过")
|
|
141
|
+
explanation: str = Field("", description="评分理由")
|
|
142
|
+
details: dict[str, Any] = Field(default_factory=dict, description="类型特定的详情")
|
|
143
|
+
confidence: float = Field(
|
|
144
|
+
1.0, ge=0.0, le=1.0, description="置信度 (多采样时 = 1 - 不确定性)"
|
|
145
|
+
)
|
|
146
|
+
uncertainty: float = Field(
|
|
147
|
+
0.0, ge=0.0, le=1.0, description="不确定性 (多采样极差的一半)"
|
|
148
|
+
)
|
|
149
|
+
sample_count: int = Field(1, ge=1, description="评分采样次数")
|
|
150
|
+
duration_ms: float = Field(0.0, description="评分耗时 (毫秒)")
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
class TrialResult(BaseModel):
|
|
154
|
+
"""单次 trial 的完整结果"""
|
|
155
|
+
|
|
156
|
+
trial_index: int = Field(..., ge=0, description="第几次 trial (0-based)")
|
|
157
|
+
trace_id: str = Field("", description="OTel trace ID")
|
|
158
|
+
success: bool = Field(True, description="最终是否成功")
|
|
159
|
+
grader_results: list[GraderResult] = Field(default_factory=list, description="各 grader 的评分")
|
|
160
|
+
metrics: dict[str, float] = Field(default_factory=dict, description="过程指标")
|
|
161
|
+
transcript: list[dict[str, Any]] = Field(default_factory=list, description="完整对话记录")
|
|
162
|
+
outcome: dict[str, Any] = Field(default_factory=dict, description="环境最终状态")
|
|
163
|
+
duration_ms: float = Field(0.0, description="总耗时 (毫秒)")
|
|
164
|
+
error: str | None = Field(None, description="错误信息 (如果失败)")
|
|
165
|
+
|
|
166
|
+
def avg_score(self) -> float:
|
|
167
|
+
"""计算所有 grader 的平均分"""
|
|
168
|
+
if not self.grader_results:
|
|
169
|
+
return 0.0
|
|
170
|
+
return sum(r.score for r in self.grader_results) / len(self.grader_results)
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
class TaskSummary(BaseModel):
|
|
174
|
+
"""单个任务的汇总 (跨 trials)"""
|
|
175
|
+
|
|
176
|
+
task_id: str
|
|
177
|
+
task_description: str = ""
|
|
178
|
+
total_trials: int
|
|
179
|
+
pass_at_k: dict[int, float] = Field(default_factory=dict, description="{k: rate}")
|
|
180
|
+
pass_power_k: dict[int, float] = Field(default_factory=dict, description="{k: rate}")
|
|
181
|
+
avg_score: float = Field(0.0, description="所有 trial 的平均分")
|
|
182
|
+
avg_metrics: dict[str, float] = Field(default_factory=dict, description="平均过程指标")
|
|
183
|
+
failures: list[int] = Field(default_factory=list, description="失败的 trial 索引")
|
|
184
|
+
pending_trials: list[int] = Field(
|
|
185
|
+
default_factory=list, description="等待人工评分的 trial 索引 (不计入通过率)"
|
|
186
|
+
)
|
|
187
|
+
consistent: bool = Field(True, description="trial 间分数是否一致 (std < 0.2)")
|
|
188
|
+
score_std_dev: float = Field(0.0, description="trial 间分数标准差")
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
class RunSummary(BaseModel):
|
|
192
|
+
"""一次 suite 运行的汇总"""
|
|
193
|
+
|
|
194
|
+
total_tasks: int
|
|
195
|
+
total_trials: int
|
|
196
|
+
pass_at_k: dict[int, float] = Field(default_factory=dict, description="全局 pass@k")
|
|
197
|
+
pass_power_k: dict[int, float] = Field(default_factory=dict, description="全局 pass^k")
|
|
198
|
+
avg_score: float = Field(0.0, description="全局平均分")
|
|
199
|
+
avg_metrics: dict[str, float] = Field(default_factory=dict, description="全局平均指标")
|
|
200
|
+
task_summaries: list[TaskSummary] = Field(default_factory=list, description="每个任务的汇总")
|
|
201
|
+
failures: list[str] = Field(default_factory=list, description="未通过的任务 ID")
|
|
202
|
+
saturation: dict[str, Any] = Field(
|
|
203
|
+
default_factory=dict,
|
|
204
|
+
description="饱和度检测结果 (is_saturated/saturation_ratio/recommendation)",
|
|
205
|
+
)
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
class RunResult(BaseModel):
|
|
209
|
+
"""一次 suite 运行的完整结果"""
|
|
210
|
+
|
|
211
|
+
run_id: str = Field(default_factory=lambda: f"run_{uuid.uuid4().hex[:12]}")
|
|
212
|
+
suite_name: str = ""
|
|
213
|
+
status: Literal["pending", "running", "completed", "failed", "cancelled"] = "pending"
|
|
214
|
+
started_at: float = Field(default_factory=lambda: time.time() * 1000)
|
|
215
|
+
completed_at: float | None = None
|
|
216
|
+
trials: dict[str, list[TrialResult]] = Field(
|
|
217
|
+
default_factory=dict, description="task_id → trials"
|
|
218
|
+
)
|
|
219
|
+
summary: RunSummary | None = None
|
|
220
|
+
error: str | None = None
|
|
221
|
+
|
|
222
|
+
@property
|
|
223
|
+
def duration_ms(self) -> float | None:
|
|
224
|
+
"""运行总耗时 (毫秒)"""
|
|
225
|
+
if self.completed_at is not None:
|
|
226
|
+
return self.completed_at - self.started_at
|
|
227
|
+
return None
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
"""Dataset construction and management for the Aeval evaluation framework.
|
|
2
|
+
|
|
3
|
+
Modules:
|
|
4
|
+
models — EvalDataset / EvalDatasetItem with provenance + to_suite()
|
|
5
|
+
storage — DatasetStorage protocol + SQLite / Memory implementations
|
|
6
|
+
quality — DatasetQualityChecker + CoverageAnalyzer
|
|
7
|
+
version — DatasetVersionManager (semver bump + change log)
|
|
8
|
+
sources — manual import / trace mining / LLM generation / regression extract
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from agent_eval.dataset.models import (
|
|
12
|
+
DatasetError,
|
|
13
|
+
EvalDataset,
|
|
14
|
+
EvalDatasetItem,
|
|
15
|
+
SourceType,
|
|
16
|
+
)
|
|
17
|
+
from agent_eval.dataset.storage import (
|
|
18
|
+
DatasetStorage,
|
|
19
|
+
MemoryDatasetStorage,
|
|
20
|
+
SqliteDatasetStorage,
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
__all__ = [
|
|
24
|
+
"DatasetError",
|
|
25
|
+
"EvalDataset",
|
|
26
|
+
"EvalDatasetItem",
|
|
27
|
+
"SourceType",
|
|
28
|
+
"DatasetStorage",
|
|
29
|
+
"MemoryDatasetStorage",
|
|
30
|
+
"SqliteDatasetStorage",
|
|
31
|
+
]
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Dataset data models — EvalDatasetItem / EvalDataset with provenance.
|
|
3
|
+
|
|
4
|
+
A dataset is a curated pool of evaluation items. Each item carries provenance
|
|
5
|
+
(source_type / source_ref) and capability metadata; `to_suite()` converts a
|
|
6
|
+
dataset into an executable EvalSuite reusing the change-① Suite validation
|
|
7
|
+
(single source of truth for validation rules).
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import time
|
|
13
|
+
import uuid
|
|
14
|
+
from enum import Enum
|
|
15
|
+
from typing import Any
|
|
16
|
+
|
|
17
|
+
from pydantic import BaseModel, Field, ValidationError, field_validator, model_validator
|
|
18
|
+
|
|
19
|
+
from agent_eval.core.types import EvalSuite, EvalTask, GraderConfig
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def now_ms() -> float:
|
|
23
|
+
"""当前时间 (epoch 毫秒)"""
|
|
24
|
+
return time.time() * 1000
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def new_dataset_id() -> str:
|
|
28
|
+
return f"ds_{uuid.uuid4().hex[:12]}"
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class SourceType(str, Enum):
|
|
32
|
+
"""数据集条目来源类型"""
|
|
33
|
+
|
|
34
|
+
MANUAL = "manual" # 手动编写
|
|
35
|
+
TRACE_MINING = "trace_mining" # 从真实 trace 挖掘
|
|
36
|
+
LLM_GENERATED = "llm_generated" # LLM 辅助生成 (含合成数据)
|
|
37
|
+
ADVERSARIAL = "adversarial" # 对抗样本 (手工构造, 手动的子类)
|
|
38
|
+
REGRESSION = "regression" # 从 run 失败 trial 提取
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class DatasetError(Exception):
|
|
42
|
+
"""数据集操作失败 (导入/转换/升版等)。"""
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
class EvalDatasetItem(BaseModel):
|
|
46
|
+
"""评测数据集中的单个条目 (含溯源)"""
|
|
47
|
+
|
|
48
|
+
id: str = Field(..., min_length=1, description="条目唯一标识 (数据集内)")
|
|
49
|
+
prompt: str = Field(..., description="给 Agent 的输入")
|
|
50
|
+
description: str = Field("", description="人类可读描述")
|
|
51
|
+
graders: list[GraderConfig] = Field(
|
|
52
|
+
default_factory=list,
|
|
53
|
+
description="评分器配置 (空 = 待补; to_suite 与质量检查会标出)",
|
|
54
|
+
)
|
|
55
|
+
env: dict[str, Any] = Field(default_factory=dict, description="环境参数 (透传)")
|
|
56
|
+
metadata: dict[str, Any] = Field(
|
|
57
|
+
default_factory=dict,
|
|
58
|
+
description="自定义元数据 (能力维度标签放 metadata.capabilities: list[str])",
|
|
59
|
+
)
|
|
60
|
+
source_type: SourceType = Field(SourceType.MANUAL, description="来源类型")
|
|
61
|
+
source_ref: str = Field("", description="来源引用 (trace_id / run_id / 场景等)")
|
|
62
|
+
created_at: float = Field(default_factory=now_ms, description="创建时间 (epoch ms)")
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
class EvalDataset(BaseModel):
|
|
66
|
+
"""评测数据集 — 一组相关的评测条目"""
|
|
67
|
+
|
|
68
|
+
id: str = Field(default_factory=new_dataset_id, description="唯一标识")
|
|
69
|
+
name: str = Field(..., min_length=1, description="数据集名称")
|
|
70
|
+
description: str = Field("", description="描述")
|
|
71
|
+
version: str = Field(
|
|
72
|
+
"1.0.0",
|
|
73
|
+
pattern=r"^\d+\.\d+\.\d+$",
|
|
74
|
+
description="语义化版本 (semver)",
|
|
75
|
+
)
|
|
76
|
+
tags: list[str] = Field(default_factory=list, description="标签 (分类/筛选)")
|
|
77
|
+
capability_map: dict[str, float] = Field(
|
|
78
|
+
default_factory=dict,
|
|
79
|
+
description="能力维度 → 覆盖度 (0-1), 由 CoverageAnalyzer 更新",
|
|
80
|
+
)
|
|
81
|
+
items: list[EvalDatasetItem] = Field(default_factory=list, description="条目列表")
|
|
82
|
+
metadata: dict[str, Any] = Field(default_factory=dict, description="自定义元数据")
|
|
83
|
+
change_log: list[dict[str, Any]] = Field(
|
|
84
|
+
default_factory=list,
|
|
85
|
+
description="版本变更记录 [{version, change_type, note, at, item_count}]",
|
|
86
|
+
)
|
|
87
|
+
created_at: float = Field(default_factory=now_ms, description="创建时间 (epoch ms)")
|
|
88
|
+
updated_at: float = Field(default_factory=now_ms, description="更新时间 (epoch ms)")
|
|
89
|
+
|
|
90
|
+
@field_validator("name")
|
|
91
|
+
@classmethod
|
|
92
|
+
def _validate_name(cls, v: str) -> str:
|
|
93
|
+
if not v or not v.strip():
|
|
94
|
+
raise ValueError("Dataset name cannot be empty")
|
|
95
|
+
if len(v) > 128:
|
|
96
|
+
raise ValueError("Dataset name too long (max 128 chars)")
|
|
97
|
+
return v
|
|
98
|
+
|
|
99
|
+
@model_validator(mode="after")
|
|
100
|
+
def _validate_item_ids_unique(self) -> EvalDataset:
|
|
101
|
+
ids = [i.id for i in self.items]
|
|
102
|
+
if len(ids) != len(set(ids)):
|
|
103
|
+
duplicates = sorted({x for x in ids if ids.count(x) > 1})
|
|
104
|
+
raise ValueError(f"Duplicate item IDs: {duplicates}")
|
|
105
|
+
return self
|
|
106
|
+
|
|
107
|
+
def get_item(self, item_id: str) -> EvalDatasetItem | None:
|
|
108
|
+
"""按 ID 获取条目"""
|
|
109
|
+
for item in self.items:
|
|
110
|
+
if item.id == item_id:
|
|
111
|
+
return item
|
|
112
|
+
return None
|
|
113
|
+
|
|
114
|
+
def to_suite(self, name: str | None = None) -> EvalSuite:
|
|
115
|
+
"""
|
|
116
|
+
转换为可执行的评测 Suite。
|
|
117
|
+
|
|
118
|
+
条目 → 任务 (prompt/graders/env 直传); suite 元数据记录数据集
|
|
119
|
+
ID 与版本, 使 run 结果可关联回数据集版本。复用 EvalSuite 的
|
|
120
|
+
校验器 (任务 ID 唯一 / 至少一个任务 / grader 配置合法),
|
|
121
|
+
非法条目 (缺 prompt/graders、ID 重复等) 拒绝转换并给出明确错误。
|
|
122
|
+
"""
|
|
123
|
+
try:
|
|
124
|
+
tasks = [
|
|
125
|
+
EvalTask(
|
|
126
|
+
id=item.id,
|
|
127
|
+
description=item.description,
|
|
128
|
+
prompt=item.prompt,
|
|
129
|
+
graders=item.graders,
|
|
130
|
+
env=item.env,
|
|
131
|
+
)
|
|
132
|
+
for item in self.items
|
|
133
|
+
]
|
|
134
|
+
return EvalSuite(
|
|
135
|
+
name=name or self.name,
|
|
136
|
+
description=self.description,
|
|
137
|
+
tasks=tasks,
|
|
138
|
+
metadata={
|
|
139
|
+
**self.metadata,
|
|
140
|
+
"dataset_id": self.id,
|
|
141
|
+
"dataset_version": self.version,
|
|
142
|
+
},
|
|
143
|
+
)
|
|
144
|
+
except ValidationError as e:
|
|
145
|
+
raise DatasetError(
|
|
146
|
+
f"Dataset '{self.name}' (v{self.version}) → Suite conversion "
|
|
147
|
+
f"failed — fix the items below and retry:\n{e}"
|
|
148
|
+
) from e
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
# grader 类型 → 内置注册名默认值 (LLM 生成/导入缺 name 时保证可被 runner 解析)
|
|
152
|
+
_TYPE_DEFAULT_NAMES: dict[str, str] = {
|
|
153
|
+
"code": "code_based",
|
|
154
|
+
"model": "model_based",
|
|
155
|
+
"state": "state_check",
|
|
156
|
+
"tool_calls": "tool_calls",
|
|
157
|
+
"transcript": "transcript",
|
|
158
|
+
"artifact": "artifact_check",
|
|
159
|
+
"human": "human",
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
def make_grader_config(
|
|
164
|
+
grader_type: str,
|
|
165
|
+
name: str | None = None,
|
|
166
|
+
**config: Any,
|
|
167
|
+
) -> GraderConfig:
|
|
168
|
+
"""
|
|
169
|
+
便捷构造 GraderConfig (数据源/生成器使用)。
|
|
170
|
+
|
|
171
|
+
Args:
|
|
172
|
+
grader_type: grader 类型字符串 (如 "model" / "metric" / "tool_calls")
|
|
173
|
+
name: grader 名称; 缺省时 metric 用 metric_name (无则 "metric"),
|
|
174
|
+
其余类型映射到内置注册名 (model → model_based 等)
|
|
175
|
+
**config: 类型特定配置 (如 metric_name/threshold/rubric)
|
|
176
|
+
|
|
177
|
+
Raises:
|
|
178
|
+
DatasetError: 类型不合法或名称不合法 (含具体字段信息)
|
|
179
|
+
"""
|
|
180
|
+
from agent_eval.core.types import GraderType
|
|
181
|
+
|
|
182
|
+
try:
|
|
183
|
+
grader_t = GraderType(grader_type)
|
|
184
|
+
except ValueError:
|
|
185
|
+
valid = ", ".join(t.value for t in GraderType)
|
|
186
|
+
raise DatasetError(
|
|
187
|
+
f"Invalid grader type '{grader_type}' (valid: {valid})"
|
|
188
|
+
) from None
|
|
189
|
+
|
|
190
|
+
if name is None:
|
|
191
|
+
if grader_t == GraderType.METRIC:
|
|
192
|
+
name = config.get("metric_name", "metric")
|
|
193
|
+
else:
|
|
194
|
+
name = _TYPE_DEFAULT_NAMES.get(grader_t.value, grader_t.value)
|
|
195
|
+
|
|
196
|
+
try:
|
|
197
|
+
return GraderConfig(type=grader_t, name=name, config=config)
|
|
198
|
+
except ValidationError as e:
|
|
199
|
+
raise DatasetError(f"Invalid grader config (name={name!r}):\n{e}") from e
|