evalrun 0.4.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.
- agents/__init__.py +6 -0
- agents/auditor/__init__.py +13 -0
- agents/auditor/budget_auditor.py +91 -0
- agents/auditor/parser.py +139 -0
- agents/auditor/prompts.py +64 -0
- agents/auditor/schema.py +67 -0
- agents/base.py +20 -0
- agents/reflection/__init__.py +3 -0
- agents/reflection/agent.py +77 -0
- agents/reflection/prompts.py +18 -0
- agents/research/__init__.py +4 -0
- agents/research/agent.py +45 -0
- agents/research/planner.py +52 -0
- agents/research/prompts.py +14 -0
- agents/support/__init__.py +5 -0
- agents/support/triage_agent.py +45 -0
- agents/travel/__init__.py +11 -0
- agents/travel/agent.py +377 -0
- agents/travel/prompts.py +30 -0
- agents/travel/session.py +110 -0
- cli/__init__.py +6 -0
- cli/demo.py +47 -0
- cli/formatter.py +93 -0
- cli/html_reporter.py +647 -0
- cli/main.py +423 -0
- cli/progress.py +38 -0
- cli/resolver.py +99 -0
- evalrun-0.4.0.dist-info/METADATA +268 -0
- evalrun-0.4.0.dist-info/RECORD +100 -0
- evalrun-0.4.0.dist-info/WHEEL +5 -0
- evalrun-0.4.0.dist-info/entry_points.txt +2 -0
- evalrun-0.4.0.dist-info/licenses/LICENSE +0 -0
- evalrun-0.4.0.dist-info/top_level.txt +4 -0
- framework/__init__.py +70 -0
- framework/core/__init__.py +17 -0
- framework/core/adapters.py +118 -0
- framework/core/contracts.py +88 -0
- framework/core/suite.py +44 -0
- framework/evaluation/__init__.py +22 -0
- framework/evaluation/base.py +29 -0
- framework/evaluation/dimensions.py +7 -0
- framework/evaluation/engine.py +110 -0
- framework/evaluation/evaluators/__init__.py +7 -0
- framework/evaluation/evaluators/adaptability.py +27 -0
- framework/evaluation/evaluators/base_llm.py +104 -0
- framework/evaluation/evaluators/constraint.py +27 -0
- framework/evaluation/evaluators/information_accuracy.py +41 -0
- framework/evaluation/evaluators/personalization.py +27 -0
- framework/evaluation/evaluators/planning.py +27 -0
- framework/evaluation/evaluators/support.py +81 -0
- framework/evaluation/prompts/__init__.py +11 -0
- framework/evaluation/prompts/adaptability.py +57 -0
- framework/evaluation/prompts/base.py +52 -0
- framework/evaluation/prompts/constraint.py +41 -0
- framework/evaluation/prompts/information_accuracy.py +79 -0
- framework/evaluation/prompts/personalization.py +57 -0
- framework/evaluation/prompts/planning.py +61 -0
- framework/evaluation/runner.py +363 -0
- framework/evaluation/testing.py +25 -0
- framework/exceptions.py +49 -0
- framework/llms/__init__.py +8 -0
- framework/llms/base.py +37 -0
- framework/llms/factory.py +38 -0
- framework/llms/gemini.py +85 -0
- framework/llms/mock.py +25 -0
- framework/llms/openai.py +94 -0
- framework/llms/openai_compatible.py +139 -0
- framework/mcp/__init__.py +20 -0
- framework/mcp/client.py +62 -0
- framework/mcp/constraints.py +125 -0
- framework/mcp/revision_summary.py +122 -0
- framework/mcp/server.py +49 -0
- framework/memory/__init__.py +3 -0
- framework/memory/base.py +17 -0
- framework/models.py +83 -0
- framework/parser.py +45 -0
- framework/parsers/__init__.py +12 -0
- framework/parsers/frontmatter.py +28 -0
- framework/parsers/mapper.py +66 -0
- framework/parsers/markdown.py +122 -0
- framework/parsers/transformers.py +112 -0
- framework/profiles/__init__.py +28 -0
- framework/profiles/registry.py +104 -0
- framework/profiles/support.py +27 -0
- framework/profiles/travel.py +78 -0
- framework/regression/__init__.py +17 -0
- framework/regression/comparator.py +273 -0
- framework/regression/loader.py +145 -0
- framework/sdk.py +151 -0
- framework/utils.py +41 -0
- framework/verification/__init__.py +13 -0
- framework/verification/base.py +32 -0
- framework/verification/extractor.py +105 -0
- framework/verification/local.py +122 -0
- framework/verification/models.py +112 -0
- framework/verification/pipeline.py +36 -0
- framework/verification/prompts.py +24 -0
- framework/verification/utils.py +54 -0
- ui/__init__.py +1 -0
- ui/server.py +255 -0
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
"""Evaluation profiles for travel planning agents."""
|
|
2
|
+
|
|
3
|
+
from framework.models import EvaluationProfile
|
|
4
|
+
from framework.evaluation.dimensions import (
|
|
5
|
+
CONSTRAINT_SATISFACTION,
|
|
6
|
+
PLANNING_QUALITY,
|
|
7
|
+
INFORMATION_ACCURACY,
|
|
8
|
+
PERSONALIZATION,
|
|
9
|
+
ADAPTABILITY,
|
|
10
|
+
)
|
|
11
|
+
|
|
12
|
+
TRAVEL_PROFILE = EvaluationProfile(
|
|
13
|
+
name="travel-agent",
|
|
14
|
+
weights={
|
|
15
|
+
CONSTRAINT_SATISFACTION: 25.0,
|
|
16
|
+
PLANNING_QUALITY: 20.0,
|
|
17
|
+
INFORMATION_ACCURACY: 20.0,
|
|
18
|
+
PERSONALIZATION: 20.0,
|
|
19
|
+
ADAPTABILITY: 15.0,
|
|
20
|
+
},
|
|
21
|
+
pass_threshold=75.0,
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
TRAVEL_ROUTE_OPTIMIZATION_PROFILE = EvaluationProfile(
|
|
25
|
+
name="travel-route-optimization",
|
|
26
|
+
weights={
|
|
27
|
+
CONSTRAINT_SATISFACTION: 25.0,
|
|
28
|
+
PLANNING_QUALITY: 55.0,
|
|
29
|
+
INFORMATION_ACCURACY: 10.0,
|
|
30
|
+
PERSONALIZATION: 5.0,
|
|
31
|
+
ADAPTABILITY: 5.0,
|
|
32
|
+
},
|
|
33
|
+
pass_threshold=70.0,
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
TRAVEL_REMOTE_WORKER_TIMEZONES_PROFILE = EvaluationProfile(
|
|
37
|
+
name="travel-remote-worker-timezones",
|
|
38
|
+
weights={
|
|
39
|
+
CONSTRAINT_SATISFACTION: 35.0,
|
|
40
|
+
PLANNING_QUALITY: 20.0,
|
|
41
|
+
INFORMATION_ACCURACY: 10.0,
|
|
42
|
+
PERSONALIZATION: 30.0,
|
|
43
|
+
ADAPTABILITY: 5.0,
|
|
44
|
+
},
|
|
45
|
+
pass_threshold=75.0,
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
TRAVEL_MID_TRIP_REPLANNING_PROFILE = EvaluationProfile(
|
|
49
|
+
name="travel-mid-trip-replanning",
|
|
50
|
+
weights={
|
|
51
|
+
ADAPTABILITY: 55.0,
|
|
52
|
+
CONSTRAINT_SATISFACTION: 20.0,
|
|
53
|
+
PLANNING_QUALITY: 15.0,
|
|
54
|
+
INFORMATION_ACCURACY: 5.0,
|
|
55
|
+
PERSONALIZATION: 5.0,
|
|
56
|
+
},
|
|
57
|
+
pass_threshold=75.0,
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
TRAVEL_INFORMATION_GATHERING_UNCERTAINTY_PROFILE = EvaluationProfile(
|
|
61
|
+
name="travel-information-gathering-uncertainty",
|
|
62
|
+
weights={
|
|
63
|
+
CONSTRAINT_SATISFACTION: 50.0,
|
|
64
|
+
INFORMATION_ACCURACY: 25.0,
|
|
65
|
+
PLANNING_QUALITY: 15.0,
|
|
66
|
+
PERSONALIZATION: 5.0,
|
|
67
|
+
ADAPTABILITY: 5.0,
|
|
68
|
+
},
|
|
69
|
+
pass_threshold=75.0,
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
PROFILE_REGISTRY = {
|
|
73
|
+
TRAVEL_PROFILE.name: TRAVEL_PROFILE,
|
|
74
|
+
TRAVEL_ROUTE_OPTIMIZATION_PROFILE.name: TRAVEL_ROUTE_OPTIMIZATION_PROFILE,
|
|
75
|
+
TRAVEL_REMOTE_WORKER_TIMEZONES_PROFILE.name: TRAVEL_REMOTE_WORKER_TIMEZONES_PROFILE,
|
|
76
|
+
TRAVEL_MID_TRIP_REPLANNING_PROFILE.name: TRAVEL_MID_TRIP_REPLANNING_PROFILE,
|
|
77
|
+
TRAVEL_INFORMATION_GATHERING_UNCERTAINTY_PROFILE.name: TRAVEL_INFORMATION_GATHERING_UNCERTAINTY_PROFILE,
|
|
78
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
"""Regression analysis and baseline comparison package."""
|
|
2
|
+
|
|
3
|
+
from framework.regression.loader import load_baseline_manifest
|
|
4
|
+
from framework.regression.comparator import (
|
|
5
|
+
compare_runs,
|
|
6
|
+
DimensionDelta,
|
|
7
|
+
ScenarioComparison,
|
|
8
|
+
RegressionReport,
|
|
9
|
+
)
|
|
10
|
+
|
|
11
|
+
__all__ = [
|
|
12
|
+
"load_baseline_manifest",
|
|
13
|
+
"compare_runs",
|
|
14
|
+
"DimensionDelta",
|
|
15
|
+
"ScenarioComparison",
|
|
16
|
+
"RegressionReport",
|
|
17
|
+
]
|
|
@@ -0,0 +1,273 @@
|
|
|
1
|
+
"""Baseline comparator engine for score delta calculation and release gate enforcement."""
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass, field
|
|
4
|
+
from typing import Any, Dict, List, Optional
|
|
5
|
+
from framework.models import EvaluationResult
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
@dataclass
|
|
9
|
+
class DimensionDelta:
|
|
10
|
+
"""Stores score delta for a single evaluation dimension."""
|
|
11
|
+
|
|
12
|
+
dimension: str
|
|
13
|
+
baseline_score: float
|
|
14
|
+
candidate_score: float
|
|
15
|
+
delta: float
|
|
16
|
+
is_regression: bool
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@dataclass
|
|
20
|
+
class ScenarioComparison:
|
|
21
|
+
"""Stores regression comparison metrics for a single scenario."""
|
|
22
|
+
|
|
23
|
+
scenario_id: str
|
|
24
|
+
scenario_name: str
|
|
25
|
+
status: str # PASSED, FAILED, BLOCKED, REGRESSED, MISSING_IN_CANDIDATE, NEW
|
|
26
|
+
baseline_score: Optional[float]
|
|
27
|
+
candidate_score: Optional[float]
|
|
28
|
+
overall_delta: Optional[float]
|
|
29
|
+
evaluator_passed: bool
|
|
30
|
+
auditor_gate: str
|
|
31
|
+
is_regression: bool
|
|
32
|
+
dimension_deltas: List[DimensionDelta] = field(default_factory=list)
|
|
33
|
+
|
|
34
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
35
|
+
return {
|
|
36
|
+
"scenario_id": self.scenario_id,
|
|
37
|
+
"scenario_name": self.scenario_name,
|
|
38
|
+
"status": self.status,
|
|
39
|
+
"baseline_score": round(self.baseline_score, 2) if self.baseline_score is not None else None,
|
|
40
|
+
"candidate_score": round(self.candidate_score, 2) if self.candidate_score is not None else None,
|
|
41
|
+
"overall_delta": round(self.overall_delta, 2) if self.overall_delta is not None else None,
|
|
42
|
+
"evaluator_passed": self.evaluator_passed,
|
|
43
|
+
"auditor_gate": self.auditor_gate,
|
|
44
|
+
"is_regression": self.is_regression,
|
|
45
|
+
"dimension_deltas": [
|
|
46
|
+
{
|
|
47
|
+
"dimension": d.dimension,
|
|
48
|
+
"baseline_score": round(d.baseline_score, 2),
|
|
49
|
+
"candidate_score": round(d.candidate_score, 2),
|
|
50
|
+
"delta": round(d.delta, 2),
|
|
51
|
+
"is_regression": d.is_regression,
|
|
52
|
+
}
|
|
53
|
+
for d in self.dimension_deltas
|
|
54
|
+
],
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
@dataclass
|
|
59
|
+
class RegressionReport:
|
|
60
|
+
"""Encapsulates full run baseline comparison report."""
|
|
61
|
+
|
|
62
|
+
run_id: str
|
|
63
|
+
baseline_run_id: Optional[str]
|
|
64
|
+
regression_detected: bool
|
|
65
|
+
release_blocked: bool
|
|
66
|
+
max_allowed_overall_regression: float
|
|
67
|
+
max_allowed_dimension_regression: float
|
|
68
|
+
scenario_comparisons: List[ScenarioComparison]
|
|
69
|
+
summary: Dict[str, Any]
|
|
70
|
+
|
|
71
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
72
|
+
return {
|
|
73
|
+
"run_id": self.run_id,
|
|
74
|
+
"baseline_run_id": self.baseline_run_id,
|
|
75
|
+
"regression_detected": self.regression_detected,
|
|
76
|
+
"release_blocked": self.release_blocked,
|
|
77
|
+
"max_allowed_overall_regression": self.max_allowed_overall_regression,
|
|
78
|
+
"max_allowed_dimension_regression": self.max_allowed_dimension_regression,
|
|
79
|
+
"summary": self.summary,
|
|
80
|
+
"scenarios": [sc.to_dict() for sc in self.scenario_comparisons],
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def compare_runs(
|
|
85
|
+
candidate_results: List[EvaluationResult],
|
|
86
|
+
baseline_data: Dict[str, Any],
|
|
87
|
+
max_overall_drop: float = 5.0,
|
|
88
|
+
max_dim_drop: float = 10.0,
|
|
89
|
+
candidate_run_id: str = "candidate-run",
|
|
90
|
+
) -> RegressionReport:
|
|
91
|
+
"""Compares candidate evaluation results against baseline data.
|
|
92
|
+
|
|
93
|
+
Calculates overall and dimension score deltas (candidate - baseline).
|
|
94
|
+
Enforces non-short-circuiting 3-tier release gates:
|
|
95
|
+
1. Evaluator Threshold Gate
|
|
96
|
+
2. Independent Auditor Gate (not BLOCK)
|
|
97
|
+
3. Regression Delta Gate (overall delta >= -max_overall_drop AND dim deltas >= -max_dim_drop)
|
|
98
|
+
|
|
99
|
+
Args:
|
|
100
|
+
candidate_results: List of candidate EvaluationResult objects.
|
|
101
|
+
baseline_data: Dictionary returned by load_baseline_manifest().
|
|
102
|
+
max_overall_drop: Maximum allowed overall score drop before regressed.
|
|
103
|
+
max_dim_drop: Maximum allowed per-dimension score drop before regressed.
|
|
104
|
+
candidate_run_id: Identifier for candidate run.
|
|
105
|
+
|
|
106
|
+
Returns:
|
|
107
|
+
A RegressionReport containing comparison details and release block status.
|
|
108
|
+
"""
|
|
109
|
+
baseline_manifest = baseline_data.get("manifest", {})
|
|
110
|
+
baseline_scenarios = baseline_data.get("scenarios", {})
|
|
111
|
+
baseline_run_id = baseline_manifest.get("run_id")
|
|
112
|
+
|
|
113
|
+
candidate_by_id = {res.benchmark_id: res for res in candidate_results}
|
|
114
|
+
|
|
115
|
+
comparisons: List[ScenarioComparison] = []
|
|
116
|
+
regression_detected = False
|
|
117
|
+
release_blocked = False
|
|
118
|
+
|
|
119
|
+
# 1. Check for baseline scenarios missing from candidate run
|
|
120
|
+
for b_id, b_info in baseline_scenarios.items():
|
|
121
|
+
if b_id not in candidate_by_id:
|
|
122
|
+
regression_detected = True
|
|
123
|
+
release_blocked = True
|
|
124
|
+
comparisons.append(
|
|
125
|
+
ScenarioComparison(
|
|
126
|
+
scenario_id=b_id,
|
|
127
|
+
scenario_name=b_info.get("scenario_name", b_id),
|
|
128
|
+
status="MISSING_IN_CANDIDATE",
|
|
129
|
+
baseline_score=b_info.get("overall_score"),
|
|
130
|
+
candidate_score=None,
|
|
131
|
+
overall_delta=None,
|
|
132
|
+
evaluator_passed=False,
|
|
133
|
+
auditor_gate="UNKNOWN",
|
|
134
|
+
is_regression=True,
|
|
135
|
+
)
|
|
136
|
+
)
|
|
137
|
+
|
|
138
|
+
# 2. Evaluate all candidate scenarios
|
|
139
|
+
for cand_res in candidate_results:
|
|
140
|
+
s_id = cand_res.benchmark_id
|
|
141
|
+
cand_score = cand_res.overall_score
|
|
142
|
+
eval_passed = cand_res.passed
|
|
143
|
+
|
|
144
|
+
meta = getattr(cand_res, "agent_metadata", {})
|
|
145
|
+
auditor_gate = meta.get("audit_gate_decision", "PASS")
|
|
146
|
+
|
|
147
|
+
if auditor_gate != "PASS":
|
|
148
|
+
release_blocked = True
|
|
149
|
+
|
|
150
|
+
if not eval_passed:
|
|
151
|
+
release_blocked = True
|
|
152
|
+
|
|
153
|
+
if s_id in baseline_scenarios:
|
|
154
|
+
b_info = baseline_scenarios[s_id]
|
|
155
|
+
base_score = b_info["overall_score"]
|
|
156
|
+
overall_delta = cand_score - base_score
|
|
157
|
+
|
|
158
|
+
# Formula: delta < -max_overall_drop
|
|
159
|
+
overall_regressed = overall_delta < (-abs(max_overall_drop))
|
|
160
|
+
|
|
161
|
+
# Per-dimension deltas
|
|
162
|
+
base_dims = b_info.get("dimension_scores", {})
|
|
163
|
+
cand_dims = {ds.dimension: ds.score for ds in cand_res.dimension_scores}
|
|
164
|
+
|
|
165
|
+
dimension_deltas: List[DimensionDelta] = []
|
|
166
|
+
dim_regressed = False
|
|
167
|
+
all_dimensions = set(base_dims.keys()).union(cand_dims.keys())
|
|
168
|
+
|
|
169
|
+
for d_name in sorted(all_dimensions):
|
|
170
|
+
if d_name in base_dims and d_name in cand_dims:
|
|
171
|
+
b_dscore = base_dims[d_name]
|
|
172
|
+
c_dscore = cand_dims[d_name]
|
|
173
|
+
ddelta = c_dscore - b_dscore
|
|
174
|
+
d_is_regressed = ddelta < (-abs(max_dim_drop))
|
|
175
|
+
if d_is_regressed:
|
|
176
|
+
dim_regressed = True
|
|
177
|
+
dimension_deltas.append(
|
|
178
|
+
DimensionDelta(
|
|
179
|
+
dimension=d_name,
|
|
180
|
+
baseline_score=b_dscore,
|
|
181
|
+
candidate_score=c_dscore,
|
|
182
|
+
delta=ddelta,
|
|
183
|
+
is_regression=d_is_regressed,
|
|
184
|
+
)
|
|
185
|
+
)
|
|
186
|
+
elif d_name in base_dims and d_name not in cand_dims:
|
|
187
|
+
b_dscore = base_dims[d_name]
|
|
188
|
+
dim_regressed = True
|
|
189
|
+
dimension_deltas.append(
|
|
190
|
+
DimensionDelta(
|
|
191
|
+
dimension=f"{d_name} (Missing in Candidate)",
|
|
192
|
+
baseline_score=b_dscore,
|
|
193
|
+
candidate_score=0.0,
|
|
194
|
+
delta=-b_dscore,
|
|
195
|
+
is_regression=True,
|
|
196
|
+
)
|
|
197
|
+
)
|
|
198
|
+
else:
|
|
199
|
+
c_dscore = cand_dims[d_name]
|
|
200
|
+
dimension_deltas.append(
|
|
201
|
+
DimensionDelta(
|
|
202
|
+
dimension=f"{d_name} (New Dimension)",
|
|
203
|
+
baseline_score=0.0,
|
|
204
|
+
candidate_score=c_dscore,
|
|
205
|
+
delta=c_dscore,
|
|
206
|
+
is_regression=False,
|
|
207
|
+
)
|
|
208
|
+
)
|
|
209
|
+
|
|
210
|
+
is_scenario_regression = overall_regressed or dim_regressed
|
|
211
|
+
if is_scenario_regression:
|
|
212
|
+
regression_detected = True
|
|
213
|
+
release_blocked = True
|
|
214
|
+
status_str = "REGRESSED"
|
|
215
|
+
elif not eval_passed:
|
|
216
|
+
status_str = "FAILED"
|
|
217
|
+
elif auditor_gate != "PASS":
|
|
218
|
+
status_str = "BLOCKED"
|
|
219
|
+
else:
|
|
220
|
+
status_str = "PASSED"
|
|
221
|
+
|
|
222
|
+
comparisons.append(
|
|
223
|
+
ScenarioComparison(
|
|
224
|
+
scenario_id=s_id,
|
|
225
|
+
scenario_name=cand_res.benchmark_name,
|
|
226
|
+
status=status_str,
|
|
227
|
+
baseline_score=base_score,
|
|
228
|
+
candidate_score=cand_score,
|
|
229
|
+
overall_delta=overall_delta,
|
|
230
|
+
evaluator_passed=eval_passed,
|
|
231
|
+
auditor_gate=auditor_gate,
|
|
232
|
+
is_regression=is_scenario_regression,
|
|
233
|
+
dimension_deltas=dimension_deltas,
|
|
234
|
+
)
|
|
235
|
+
)
|
|
236
|
+
else:
|
|
237
|
+
# Candidate scenario is new
|
|
238
|
+
if not eval_passed or auditor_gate != "PASS":
|
|
239
|
+
release_blocked = True
|
|
240
|
+
|
|
241
|
+
status_str = "PASSED" if (eval_passed and auditor_gate == "PASS") else ("BLOCKED" if auditor_gate != "PASS" else "FAILED")
|
|
242
|
+
comparisons.append(
|
|
243
|
+
ScenarioComparison(
|
|
244
|
+
scenario_id=s_id,
|
|
245
|
+
scenario_name=cand_res.benchmark_name,
|
|
246
|
+
status=f"NEW ({status_str})",
|
|
247
|
+
baseline_score=None,
|
|
248
|
+
candidate_score=cand_score,
|
|
249
|
+
overall_delta=None,
|
|
250
|
+
evaluator_passed=eval_passed,
|
|
251
|
+
auditor_gate=auditor_gate,
|
|
252
|
+
is_regression=False,
|
|
253
|
+
)
|
|
254
|
+
)
|
|
255
|
+
|
|
256
|
+
summary = {
|
|
257
|
+
"total_scenarios": len(comparisons),
|
|
258
|
+
"passed": sum(1 for c in comparisons if c.status == "PASSED"),
|
|
259
|
+
"failed": sum(1 for c in comparisons if not c.evaluator_passed),
|
|
260
|
+
"blocked": sum(1 for c in comparisons if c.auditor_gate != "PASS"),
|
|
261
|
+
"regressed": sum(1 for c in comparisons if c.is_regression),
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
return RegressionReport(
|
|
265
|
+
run_id=candidate_run_id,
|
|
266
|
+
baseline_run_id=baseline_run_id,
|
|
267
|
+
regression_detected=regression_detected,
|
|
268
|
+
release_blocked=release_blocked,
|
|
269
|
+
max_allowed_overall_regression=max_overall_drop,
|
|
270
|
+
max_allowed_dimension_regression=max_dim_drop,
|
|
271
|
+
scenario_comparisons=comparisons,
|
|
272
|
+
summary=summary,
|
|
273
|
+
)
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
"""Baseline manifest and report loader for regression comparison."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import os
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import Any, Dict, Optional
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def load_baseline_manifest(path_str: str) -> Dict[str, Any]:
|
|
10
|
+
"""Loads baseline manifest and per-scenario evaluation reports.
|
|
11
|
+
|
|
12
|
+
Args:
|
|
13
|
+
path_str: Path to a baseline directory containing manifest.json or path to manifest.json file.
|
|
14
|
+
|
|
15
|
+
Returns:
|
|
16
|
+
A dictionary structured as:
|
|
17
|
+
{
|
|
18
|
+
"manifest": dict,
|
|
19
|
+
"scenarios": {
|
|
20
|
+
"<scenario_id>": {
|
|
21
|
+
"scenario_id": str,
|
|
22
|
+
"scenario_name": str,
|
|
23
|
+
"overall_score": float,
|
|
24
|
+
"passed": bool,
|
|
25
|
+
"audit_gate_decision": str,
|
|
26
|
+
"dimension_scores": { "<dim_name>": float },
|
|
27
|
+
"report_path": str,
|
|
28
|
+
"itinerary_path": str
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
Raises:
|
|
34
|
+
FileNotFoundError: If baseline manifest file or directory does not exist.
|
|
35
|
+
ValueError: If manifest format is invalid.
|
|
36
|
+
"""
|
|
37
|
+
path = Path(path_str)
|
|
38
|
+
if not path.exists():
|
|
39
|
+
raise FileNotFoundError(f"Baseline path '{path_str}' does not exist.")
|
|
40
|
+
|
|
41
|
+
is_dedicated_dir = path.is_dir()
|
|
42
|
+
if is_dedicated_dir:
|
|
43
|
+
manifest_file = path / "manifest.json"
|
|
44
|
+
base_dir = path
|
|
45
|
+
else:
|
|
46
|
+
manifest_file = path
|
|
47
|
+
base_dir = path.parent
|
|
48
|
+
|
|
49
|
+
if not manifest_file.exists():
|
|
50
|
+
raise FileNotFoundError(f"Baseline manifest file '{manifest_file}' not found.")
|
|
51
|
+
|
|
52
|
+
with open(manifest_file, "r", encoding="utf-8") as f:
|
|
53
|
+
manifest_data = json.load(f)
|
|
54
|
+
|
|
55
|
+
scenarios_by_id: Dict[str, Dict[str, Any]] = {}
|
|
56
|
+
|
|
57
|
+
# 1. Parse scenarios array if present in manifest.json
|
|
58
|
+
if "scenarios" in manifest_data and isinstance(manifest_data["scenarios"], list):
|
|
59
|
+
for item in manifest_data["scenarios"]:
|
|
60
|
+
s_id = item.get("scenario_id") or item.get("benchmark_id")
|
|
61
|
+
if not s_id:
|
|
62
|
+
continue
|
|
63
|
+
|
|
64
|
+
dim_scores = item.get("dimension_scores", {})
|
|
65
|
+
if isinstance(dim_scores, list):
|
|
66
|
+
dim_scores = {d["dimension"]: d["score"] for d in dim_scores if "dimension" in d and "score" in d}
|
|
67
|
+
|
|
68
|
+
rep_path = item.get("report_path", "")
|
|
69
|
+
|
|
70
|
+
# If report_path is specified and file exists, load detailed report file
|
|
71
|
+
if rep_path and not dim_scores:
|
|
72
|
+
full_rep = base_dir / rep_path
|
|
73
|
+
# Older manifests used the adapter class name in report_path.
|
|
74
|
+
# Fall back to the actual scenario-id filename so those
|
|
75
|
+
# baselines remain usable after report naming was corrected.
|
|
76
|
+
if not full_rep.exists() and base_dir.exists():
|
|
77
|
+
matches = sorted(base_dir.glob(f"*_{s_id}_report.json"))
|
|
78
|
+
if matches:
|
|
79
|
+
full_rep = matches[0]
|
|
80
|
+
if full_rep.exists():
|
|
81
|
+
try:
|
|
82
|
+
with open(full_rep, "r", encoding="utf-8") as rf:
|
|
83
|
+
rep = json.load(rf)
|
|
84
|
+
dim_scores = {
|
|
85
|
+
ds["dimension"]: float(ds["score"])
|
|
86
|
+
for ds in rep.get("dimension_scores", [])
|
|
87
|
+
if isinstance(ds, dict) and "dimension" in ds and "score" in ds
|
|
88
|
+
}
|
|
89
|
+
except Exception:
|
|
90
|
+
pass
|
|
91
|
+
|
|
92
|
+
scenarios_by_id[s_id] = {
|
|
93
|
+
"scenario_id": s_id,
|
|
94
|
+
"scenario_name": item.get("scenario_name") or item.get("name") or s_id,
|
|
95
|
+
"overall_score": float(item.get("overall_score", 0.0)),
|
|
96
|
+
"passed": bool(item.get("passed", False)),
|
|
97
|
+
"audit_gate_decision": item.get("audit_gate_decision") or item.get("auditor_gate") or "PASS",
|
|
98
|
+
"dimension_scores": dim_scores,
|
|
99
|
+
"report_path": rep_path,
|
|
100
|
+
"itinerary_path": item.get("itinerary_path", ""),
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
# 2. Only scan directory for report files if path_str was a dedicated directory
|
|
104
|
+
if is_dedicated_dir and base_dir.exists():
|
|
105
|
+
for fname in os.listdir(base_dir):
|
|
106
|
+
if fname.endswith("_report.json") and fname not in ("manifest.json", "regression_report.json"):
|
|
107
|
+
report_filepath = base_dir / fname
|
|
108
|
+
try:
|
|
109
|
+
with open(report_filepath, "r", encoding="utf-8") as f:
|
|
110
|
+
rep = json.load(f)
|
|
111
|
+
s_id = rep.get("benchmark_id") or rep.get("scenario_id")
|
|
112
|
+
if not s_id or s_id in scenarios_by_id:
|
|
113
|
+
continue
|
|
114
|
+
|
|
115
|
+
dim_scores = {
|
|
116
|
+
ds["dimension"]: float(ds["score"])
|
|
117
|
+
for ds in rep.get("dimension_scores", [])
|
|
118
|
+
if isinstance(ds, dict) and "dimension" in ds and "score" in ds
|
|
119
|
+
}
|
|
120
|
+
agent_meta = rep.get("agent_metadata", {})
|
|
121
|
+
audit_gate = agent_meta.get("audit_gate_decision", "PASS")
|
|
122
|
+
|
|
123
|
+
scenarios_by_id[s_id] = {
|
|
124
|
+
"scenario_id": s_id,
|
|
125
|
+
"scenario_name": rep.get("benchmark_name") or rep.get("scenario_name") or s_id,
|
|
126
|
+
"overall_score": float(rep.get("overall_score", 0.0)),
|
|
127
|
+
"passed": bool(rep.get("passed", False)),
|
|
128
|
+
"audit_gate_decision": audit_gate,
|
|
129
|
+
"dimension_scores": dim_scores,
|
|
130
|
+
"report_path": fname,
|
|
131
|
+
"itinerary_path": rep.get("itinerary_path", ""),
|
|
132
|
+
}
|
|
133
|
+
except Exception:
|
|
134
|
+
pass
|
|
135
|
+
|
|
136
|
+
if not scenarios_by_id:
|
|
137
|
+
raise ValueError(
|
|
138
|
+
f"Baseline manifest '{manifest_file}' contains no scenario results. "
|
|
139
|
+
"Pass a dedicated run directory containing scenario reports or a manifest with scenario/report entries."
|
|
140
|
+
)
|
|
141
|
+
|
|
142
|
+
return {
|
|
143
|
+
"manifest": manifest_data,
|
|
144
|
+
"scenarios": scenarios_by_id,
|
|
145
|
+
}
|
framework/sdk.py
ADDED
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
"""Public Python SDK API for programmatic agent evaluation and regression testing."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import os
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import Any, Dict, List, Optional, Union
|
|
7
|
+
|
|
8
|
+
from framework.core.adapters import PythonAgentAdapter, HttpAgentAdapter, CliAgentAdapter
|
|
9
|
+
from framework.evaluation.runner import BenchmarkRunner
|
|
10
|
+
from framework.llms.openai_compatible import OpenAICompatibleLLM
|
|
11
|
+
from framework.models import EvaluationResult
|
|
12
|
+
from framework.regression import load_baseline_manifest, compare_runs, RegressionReport
|
|
13
|
+
from cli.resolver import resolve_agent
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def evaluate(
|
|
17
|
+
scenario: str,
|
|
18
|
+
agent: str,
|
|
19
|
+
model: str,
|
|
20
|
+
base_url: str = "https://api.openai.com/v1",
|
|
21
|
+
api_key: Optional[str] = None,
|
|
22
|
+
judge_model: Optional[str] = None,
|
|
23
|
+
judge_base_url: Optional[str] = None,
|
|
24
|
+
judge_api_key: Optional[str] = None,
|
|
25
|
+
ground_truth: str = "ground_truth/japan_demo.json",
|
|
26
|
+
output_dir: str = "./eval_results",
|
|
27
|
+
) -> List[EvaluationResult]:
|
|
28
|
+
"""Programmatically runs agent evaluation on a scenario or suite directory.
|
|
29
|
+
|
|
30
|
+
Args:
|
|
31
|
+
scenario: Path to a scenario file (.md) or suite directory.
|
|
32
|
+
agent: Agent specifier string ('module:Class' or 'module:factory').
|
|
33
|
+
model: Target agent model name.
|
|
34
|
+
base_url: Target agent model OpenAI-compatible base URL.
|
|
35
|
+
api_key: Target agent API key.
|
|
36
|
+
judge_model: Judge model name (defaults to target model).
|
|
37
|
+
judge_base_url: Judge base URL (defaults to target base_url).
|
|
38
|
+
judge_api_key: Judge API key (defaults to target api_key).
|
|
39
|
+
ground_truth: Ground truth knowledge base path.
|
|
40
|
+
output_dir: Target output directory for evaluation artifacts.
|
|
41
|
+
|
|
42
|
+
Returns:
|
|
43
|
+
List of EvaluationResult objects.
|
|
44
|
+
"""
|
|
45
|
+
target_api_key = api_key or os.getenv("OPENAI_API_KEY", "EMPTY")
|
|
46
|
+
judge_model_name = judge_model or model
|
|
47
|
+
j_base_url = judge_base_url or base_url
|
|
48
|
+
j_api_key = judge_api_key or target_api_key
|
|
49
|
+
|
|
50
|
+
# Initialize Target LLM
|
|
51
|
+
target_llm = OpenAICompatibleLLM(
|
|
52
|
+
model_name=model,
|
|
53
|
+
base_url=base_url,
|
|
54
|
+
api_key=target_api_key,
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
# Initialize Judge LLM
|
|
58
|
+
judge_llm = OpenAICompatibleLLM(
|
|
59
|
+
model_name=judge_model_name,
|
|
60
|
+
base_url=j_base_url,
|
|
61
|
+
api_key=j_api_key,
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
# Resolve target agent
|
|
65
|
+
resolved_agent = resolve_agent(agent, target_llm)
|
|
66
|
+
|
|
67
|
+
out_path = Path(output_dir)
|
|
68
|
+
out_path.mkdir(parents=True, exist_ok=True)
|
|
69
|
+
|
|
70
|
+
# Instantiate BenchmarkRunner with output_dir
|
|
71
|
+
runner = BenchmarkRunner(
|
|
72
|
+
agent=resolved_agent,
|
|
73
|
+
judge_llm=judge_llm,
|
|
74
|
+
local_verifier_path=ground_truth,
|
|
75
|
+
output_dir=str(out_path),
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
scenario_path = Path(scenario)
|
|
79
|
+
results: List[EvaluationResult] = []
|
|
80
|
+
|
|
81
|
+
if scenario_path.is_dir():
|
|
82
|
+
for fname in os.listdir(scenario_path):
|
|
83
|
+
if fname.endswith(".md") or fname.endswith(".json"):
|
|
84
|
+
s_file = scenario_path / fname
|
|
85
|
+
try:
|
|
86
|
+
res = runner.run(str(s_file))
|
|
87
|
+
results.append(res)
|
|
88
|
+
except Exception as e:
|
|
89
|
+
print(f"Warning: Failed to evaluate scenario '{s_file}': {e}")
|
|
90
|
+
else:
|
|
91
|
+
res = runner.run(str(scenario_path))
|
|
92
|
+
results.append(res)
|
|
93
|
+
|
|
94
|
+
# Write SDK evaluation artifacts
|
|
95
|
+
try:
|
|
96
|
+
from datetime import datetime, timezone
|
|
97
|
+
import uuid
|
|
98
|
+
from cli.formatter import redact_credentials
|
|
99
|
+
from cli.html_reporter import generate_html_report
|
|
100
|
+
|
|
101
|
+
run_id = f"evalrun-sdk-{datetime.now(timezone.utc).strftime('%Y%m%d-%H%M%S')}-{uuid.uuid4().hex[:6]}"
|
|
102
|
+
manifest = {
|
|
103
|
+
"run_id": run_id,
|
|
104
|
+
"timestamp_utc": datetime.now(timezone.utc).isoformat(),
|
|
105
|
+
"target_agent_spec": agent,
|
|
106
|
+
# Never place the actual credential in the artifact structure.
|
|
107
|
+
# The key is used only by the in-process model client.
|
|
108
|
+
"target_model": {"model_name": model, "base_url": base_url, "api_key": "[REDACTED]"},
|
|
109
|
+
"judge_model": {"model_name": judge_model_name, "base_url": j_base_url, "api_key": "[REDACTED]"},
|
|
110
|
+
"ground_truth_path": ground_truth,
|
|
111
|
+
"output_dir": str(out_path),
|
|
112
|
+
"total_scenarios": len(results),
|
|
113
|
+
"overall_passed": all(r.passed for r in results),
|
|
114
|
+
}
|
|
115
|
+
with open(out_path / "manifest.json", "w", encoding="utf-8") as f:
|
|
116
|
+
json.dump(redact_credentials(manifest), f, indent=2)
|
|
117
|
+
|
|
118
|
+
generate_html_report(results, redact_credentials(manifest), str(out_path))
|
|
119
|
+
except Exception as e:
|
|
120
|
+
print(f"Warning: Failed to generate SDK report artifacts: {e}")
|
|
121
|
+
|
|
122
|
+
return results
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def compare(
|
|
126
|
+
candidate_results: List[EvaluationResult],
|
|
127
|
+
baseline: str,
|
|
128
|
+
max_regression: float = 5.0,
|
|
129
|
+
max_dimension_regression: float = 10.0,
|
|
130
|
+
candidate_run_id: str = "candidate-run",
|
|
131
|
+
) -> RegressionReport:
|
|
132
|
+
"""Programmatically compares candidate evaluation results against a baseline.
|
|
133
|
+
|
|
134
|
+
Args:
|
|
135
|
+
candidate_results: List of candidate EvaluationResult objects.
|
|
136
|
+
baseline: Path to baseline directory or manifest.json file.
|
|
137
|
+
max_regression: Maximum allowed overall score drop before release blocked.
|
|
138
|
+
max_dimension_regression: Maximum allowed per-dimension score drop before release blocked.
|
|
139
|
+
candidate_run_id: Identifier for candidate run.
|
|
140
|
+
|
|
141
|
+
Returns:
|
|
142
|
+
A RegressionReport object.
|
|
143
|
+
"""
|
|
144
|
+
baseline_data = load_baseline_manifest(baseline)
|
|
145
|
+
return compare_runs(
|
|
146
|
+
candidate_results=candidate_results,
|
|
147
|
+
baseline_data=baseline_data,
|
|
148
|
+
max_overall_drop=max_regression,
|
|
149
|
+
max_dim_drop=max_dimension_regression,
|
|
150
|
+
candidate_run_id=candidate_run_id,
|
|
151
|
+
)
|