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
cli/html_reporter.py
ADDED
|
@@ -0,0 +1,647 @@
|
|
|
1
|
+
"""HTML Report Generator for evalrun CLI."""
|
|
2
|
+
|
|
3
|
+
import html
|
|
4
|
+
import json
|
|
5
|
+
import os
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import Any, Dict, List, Optional
|
|
8
|
+
from framework.models import EvaluationResult
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def generate_html_report(
|
|
12
|
+
results: List[EvaluationResult],
|
|
13
|
+
manifest: Dict[str, Any],
|
|
14
|
+
output_dir: str,
|
|
15
|
+
regression_report: Optional[Dict[str, Any]] = None,
|
|
16
|
+
) -> str:
|
|
17
|
+
"""Generates a standalone, beautifully styled interactive HTML evaluation report.
|
|
18
|
+
|
|
19
|
+
Args:
|
|
20
|
+
results: List of EvaluationResult objects.
|
|
21
|
+
manifest: Redacted run configuration manifest dictionary.
|
|
22
|
+
output_dir: Target output directory path.
|
|
23
|
+
regression_report: Optional baseline comparison dictionary.
|
|
24
|
+
|
|
25
|
+
Returns:
|
|
26
|
+
Absolute path to the generated report.html file.
|
|
27
|
+
"""
|
|
28
|
+
output_path = Path(output_dir) / "report.html"
|
|
29
|
+
|
|
30
|
+
total_runs = len(results)
|
|
31
|
+
passed_count = sum(1 for r in results if r.passed and getattr(r, "agent_metadata", {}).get("audit_gate_decision", "N/A") in ("PASS", "N/A"))
|
|
32
|
+
blocked_count = sum(1 for r in results if getattr(r, "agent_metadata", {}).get("audit_gate_decision") == "BLOCK")
|
|
33
|
+
failed_count = total_runs - passed_count
|
|
34
|
+
|
|
35
|
+
reg_detected = regression_report.get("regression_detected", False) if regression_report else False
|
|
36
|
+
is_blocked = (regression_report.get("release_blocked", False) if regression_report else False) or (failed_count > 0) or (blocked_count > 0)
|
|
37
|
+
|
|
38
|
+
avg_score = sum(r.overall_score for r in results) / total_runs if total_runs > 0 else 0.0
|
|
39
|
+
|
|
40
|
+
# Top level verdict banner
|
|
41
|
+
if is_blocked:
|
|
42
|
+
verdict_class = "verdict-blocked"
|
|
43
|
+
verdict_title = "RELEASE BLOCKED"
|
|
44
|
+
verdict_sub = f"{failed_count} evaluator failure(s), {blocked_count} auditor block(s)"
|
|
45
|
+
if reg_detected:
|
|
46
|
+
verdict_sub += ", score regression detected against baseline"
|
|
47
|
+
else:
|
|
48
|
+
verdict_class = "verdict-approved"
|
|
49
|
+
verdict_title = "RELEASE APPROVED"
|
|
50
|
+
verdict_sub = "All evaluation thresholds, auditor gates, and baseline regression checks passed cleanly."
|
|
51
|
+
|
|
52
|
+
reg_by_id = {}
|
|
53
|
+
if regression_report:
|
|
54
|
+
for s in regression_report.get("scenarios", []):
|
|
55
|
+
reg_by_id[s["scenario_id"]] = s
|
|
56
|
+
|
|
57
|
+
# Sort results fail/blocked first for human review
|
|
58
|
+
def result_sort_key(res: EvaluationResult) -> int:
|
|
59
|
+
gate = getattr(res, "agent_metadata", {}).get("audit_gate_decision", "N/A")
|
|
60
|
+
is_reg = reg_by_id.get(res.benchmark_id, {}).get("is_regression", False)
|
|
61
|
+
if is_reg or gate != "PASS" or not res.passed:
|
|
62
|
+
return 0
|
|
63
|
+
return 1
|
|
64
|
+
|
|
65
|
+
sorted_results = sorted(results, key=result_sort_key)
|
|
66
|
+
|
|
67
|
+
cards_html = []
|
|
68
|
+
for res in sorted_results:
|
|
69
|
+
meta = getattr(res, "agent_metadata", {})
|
|
70
|
+
gate = meta.get("audit_gate_decision", "N/A")
|
|
71
|
+
trace = meta.get("run_trace", {})
|
|
72
|
+
audit = meta.get("audit_report", {})
|
|
73
|
+
raw_output = meta.get("raw_content") or ""
|
|
74
|
+
|
|
75
|
+
reg_info = reg_by_id.get(res.benchmark_id, {})
|
|
76
|
+
is_reg = reg_info.get("is_regression", False)
|
|
77
|
+
|
|
78
|
+
eval_badge_class = "badge-pass" if res.passed else "badge-fail"
|
|
79
|
+
eval_status_str = "PASS" if res.passed else "FAIL"
|
|
80
|
+
|
|
81
|
+
gate_badge_class = "badge-pass" if gate == "PASS" else "badge-block"
|
|
82
|
+
gate_status_str = gate
|
|
83
|
+
|
|
84
|
+
card_status_tag = "passed"
|
|
85
|
+
if not res.passed:
|
|
86
|
+
card_status_tag = "failed"
|
|
87
|
+
elif gate != "PASS":
|
|
88
|
+
card_status_tag = "blocked"
|
|
89
|
+
elif is_reg:
|
|
90
|
+
card_status_tag = "regressed"
|
|
91
|
+
|
|
92
|
+
# Lowest scoring dimension
|
|
93
|
+
lowest_dim = min(res.dimension_scores, key=lambda d: d.score) if res.dimension_scores else None
|
|
94
|
+
lowest_dim_html = ""
|
|
95
|
+
if lowest_dim:
|
|
96
|
+
lowest_score_class = "score-high" if lowest_dim.score >= 80 else ("score-med" if lowest_dim.score >= 60 else "score-low")
|
|
97
|
+
lowest_dim_html = f"""
|
|
98
|
+
<div class="highlight-card">
|
|
99
|
+
<strong>Lowest Dimension:</strong> {html.escape(lowest_dim.dimension)}
|
|
100
|
+
(<span class="{lowest_score_class}">{lowest_dim.score:.1f}/100</span>) — {html.escape(lowest_dim.reason[:120])}...
|
|
101
|
+
</div>
|
|
102
|
+
"""
|
|
103
|
+
|
|
104
|
+
# Baseline delta section
|
|
105
|
+
baseline_delta_html = ""
|
|
106
|
+
if reg_info:
|
|
107
|
+
b_score = reg_info.get("baseline_score")
|
|
108
|
+
c_score = reg_info.get("candidate_score")
|
|
109
|
+
delta_val = reg_info.get("overall_delta")
|
|
110
|
+
|
|
111
|
+
delta_class = "score-high" if (delta_val or 0) >= 0 else ("score-med" if (delta_val or 0) >= -5.0 else "score-low")
|
|
112
|
+
delta_str = f"{delta_val:+.2f}" if delta_val is not None else "N/A"
|
|
113
|
+
|
|
114
|
+
dim_delta_rows = []
|
|
115
|
+
for dd in reg_info.get("dimension_deltas", []):
|
|
116
|
+
dd_class = "score-high" if dd["delta"] >= 0 else ("score-med" if dd["delta"] >= -10.0 else "score-low")
|
|
117
|
+
dim_delta_rows.append(f"""
|
|
118
|
+
<tr>
|
|
119
|
+
<td>{html.escape(dd['dimension'])}</td>
|
|
120
|
+
<td>{dd['baseline_score']:.1f}</td>
|
|
121
|
+
<td>{dd['candidate_score']:.1f}</td>
|
|
122
|
+
<td><span class="{dd_class}">{dd['delta']:+.2f}</span></td>
|
|
123
|
+
</tr>
|
|
124
|
+
""")
|
|
125
|
+
|
|
126
|
+
baseline_delta_html = f"""
|
|
127
|
+
<div class="section-card baseline-card">
|
|
128
|
+
<h3>Baseline Comparison Deltas</h3>
|
|
129
|
+
<div class="delta-summary-grid">
|
|
130
|
+
<div><strong>Baseline Score:</strong> {b_score if b_score is not None else 'N/A'}</div>
|
|
131
|
+
<div><strong>Candidate Score:</strong> {c_score if c_score is not None else 'N/A'}</div>
|
|
132
|
+
<div><strong>Overall Delta:</strong> <span class="{delta_class}">{delta_str}</span></div>
|
|
133
|
+
<div><strong>Status:</strong> <code>{html.escape(str(reg_info.get('status')))}</code></div>
|
|
134
|
+
</div>
|
|
135
|
+
{f'<table class="data-table"><thead><tr><th>Dimension</th><th>Baseline</th><th>Candidate</th><th>Delta</th></tr></thead><tbody>{"".join(dim_delta_rows)}</tbody></table>' if dim_delta_rows else ''}
|
|
136
|
+
</div>
|
|
137
|
+
"""
|
|
138
|
+
|
|
139
|
+
dimension_rows = []
|
|
140
|
+
for ds in res.dimension_scores:
|
|
141
|
+
score_class = "score-high" if ds.score >= 80 else ("score-med" if ds.score >= 60 else "score-low")
|
|
142
|
+
score_pct = max(0, min(100, ds.score))
|
|
143
|
+
dimension_rows.append(f"""
|
|
144
|
+
<tr>
|
|
145
|
+
<td><strong>{html.escape(ds.dimension)}</strong></td>
|
|
146
|
+
<td>
|
|
147
|
+
<div class="score-bar-container">
|
|
148
|
+
<div class="score-bar-fill" style="width: {score_pct}%;"></div>
|
|
149
|
+
</div>
|
|
150
|
+
<span class="{score_class}">{ds.score:.1f} / 100</span>
|
|
151
|
+
</td>
|
|
152
|
+
<td>{html.escape(ds.reason)}</td>
|
|
153
|
+
</tr>
|
|
154
|
+
""")
|
|
155
|
+
|
|
156
|
+
auditor_section = ""
|
|
157
|
+
if audit:
|
|
158
|
+
violations_html = ""
|
|
159
|
+
if audit.get("violations"):
|
|
160
|
+
v_items = []
|
|
161
|
+
for v in audit["violations"]:
|
|
162
|
+
v_items.append(f"""
|
|
163
|
+
<li>
|
|
164
|
+
<strong>[{html.escape(str(v.get('violation_type')))}]</strong>
|
|
165
|
+
{html.escape(str(v.get('description')))}
|
|
166
|
+
<span class="disc-amt">(Discrepancy: ₹{v.get('estimated_discrepancy_inr', 0):,.2f})</span>
|
|
167
|
+
</li>
|
|
168
|
+
""")
|
|
169
|
+
violations_html = f"""
|
|
170
|
+
<div class="violations-box">
|
|
171
|
+
<h4>Detected Budget Violations</h4>
|
|
172
|
+
<ul>{''.join(v_items)}</ul>
|
|
173
|
+
</div>
|
|
174
|
+
"""
|
|
175
|
+
|
|
176
|
+
auditor_section = f"""
|
|
177
|
+
<div class="section-card auditor-card">
|
|
178
|
+
<h3>Independent Budget Auditor Gate Report</h3>
|
|
179
|
+
<div class="auditor-meta">
|
|
180
|
+
<p><strong>Gate Decision:</strong> <span class="{gate_badge_class}">{gate_status_str}</span></p>
|
|
181
|
+
<p><strong>Auditor Score:</strong> {audit.get('audit_score', 0.0):.2f} / 100</p>
|
|
182
|
+
<p><strong>Confidence:</strong> {audit.get('audit_confidence', 0.0):.2f}</p>
|
|
183
|
+
<p><strong>Reasoning:</strong> {html.escape(str(audit.get('reasoning_summary', 'N/A')))}</p>
|
|
184
|
+
</div>
|
|
185
|
+
{violations_html}
|
|
186
|
+
</div>
|
|
187
|
+
"""
|
|
188
|
+
|
|
189
|
+
trace_section = ""
|
|
190
|
+
if trace:
|
|
191
|
+
trace_section = f"""
|
|
192
|
+
<div class="section-card trace-card">
|
|
193
|
+
<h3>Execution Run Trace</h3>
|
|
194
|
+
<table class="trace-table">
|
|
195
|
+
<tr><th>Trace ID</th><td><code>{html.escape(str(trace.get('trace_id', 'N/A')))}</code></td></tr>
|
|
196
|
+
<tr><th>Started (UTC)</th><td>{html.escape(str(trace.get('started_at_utc', 'N/A')))}</td></tr>
|
|
197
|
+
<tr><th>Finished (UTC)</th><td>{html.escape(str(trace.get('finished_at_utc', 'N/A')))}</td></tr>
|
|
198
|
+
<tr><th>Latency</th><td>{trace.get('latency_seconds', 0.0):.2f}s</td></tr>
|
|
199
|
+
<tr><th>Status</th><td><code>{html.escape(str(trace.get('status', 'N/A')))}</code></td></tr>
|
|
200
|
+
</table>
|
|
201
|
+
</div>
|
|
202
|
+
"""
|
|
203
|
+
|
|
204
|
+
is_failing_card = (card_status_tag != "passed")
|
|
205
|
+
open_attr = "open" if is_failing_card else ""
|
|
206
|
+
|
|
207
|
+
raw_output_section = ""
|
|
208
|
+
if raw_output:
|
|
209
|
+
raw_output_section = f"""
|
|
210
|
+
<details class="raw-output-details" {open_attr}>
|
|
211
|
+
<summary>Inspect Raw Agent Output Text {f'(Auto-Opened on {card_status_tag.upper()})' if is_failing_card else ''}</summary>
|
|
212
|
+
<pre class="raw-output-box">{html.escape(raw_output)}</pre>
|
|
213
|
+
</details>
|
|
214
|
+
"""
|
|
215
|
+
|
|
216
|
+
card_id = f"scenario-{res.benchmark_id}"
|
|
217
|
+
|
|
218
|
+
cards_html.append(f"""
|
|
219
|
+
<div class="scenario-card" id="{card_id}" data-status="{card_status_tag}" data-name="{html.escape(res.benchmark_name.lower())}">
|
|
220
|
+
<div class="scenario-header">
|
|
221
|
+
<h2>{html.escape(res.benchmark_name)}</h2>
|
|
222
|
+
<div class="badges">
|
|
223
|
+
<span class="badge {eval_badge_class}">Evaluator: {eval_status_str}</span>
|
|
224
|
+
<span class="badge {gate_badge_class}">Auditor: {gate_status_str}</span>
|
|
225
|
+
{f'<span class="badge badge-regressed">REGRESSED</span>' if is_reg else ''}
|
|
226
|
+
<span class="overall-score-pill">Score: {res.overall_score:.2f} / 100</span>
|
|
227
|
+
</div>
|
|
228
|
+
</div>
|
|
229
|
+
|
|
230
|
+
{lowest_dim_html}
|
|
231
|
+
{baseline_delta_html}
|
|
232
|
+
{auditor_section}
|
|
233
|
+
{trace_section}
|
|
234
|
+
|
|
235
|
+
<div class="section-card">
|
|
236
|
+
<h3>Dimension Breakdown</h3>
|
|
237
|
+
<table class="data-table">
|
|
238
|
+
<thead>
|
|
239
|
+
<tr>
|
|
240
|
+
<th style="width: 25%;">Dimension</th>
|
|
241
|
+
<th style="width: 20%;">Score</th>
|
|
242
|
+
<th style="width: 55%;">LLM Judge Justification</th>
|
|
243
|
+
</tr>
|
|
244
|
+
</thead>
|
|
245
|
+
<tbody>
|
|
246
|
+
{''.join(dimension_rows)}
|
|
247
|
+
</tbody>
|
|
248
|
+
</table>
|
|
249
|
+
</div>
|
|
250
|
+
|
|
251
|
+
{raw_output_section}
|
|
252
|
+
</div>
|
|
253
|
+
""")
|
|
254
|
+
|
|
255
|
+
# Build Failure Jump Links Bar
|
|
256
|
+
jump_links = []
|
|
257
|
+
for res in sorted_results:
|
|
258
|
+
meta = getattr(res, "agent_metadata", {})
|
|
259
|
+
gate = meta.get("audit_gate_decision", "N/A")
|
|
260
|
+
is_reg = reg_by_id.get(res.benchmark_id, {}).get("is_regression", False)
|
|
261
|
+
if is_reg or gate != "PASS" or not res.passed:
|
|
262
|
+
status_lbl = "REGRESSED" if is_reg else ("BLOCKED" if gate != "PASS" else "FAILED")
|
|
263
|
+
card_id = f"scenario-{res.benchmark_id}"
|
|
264
|
+
jump_links.append(f'<a href="#{card_id}" class="jump-pill">{html.escape(res.benchmark_name)} [{status_lbl}]</a>')
|
|
265
|
+
|
|
266
|
+
jump_bar_html = ""
|
|
267
|
+
if jump_links:
|
|
268
|
+
jump_bar_html = f"""
|
|
269
|
+
<div class="jump-bar">
|
|
270
|
+
<span class="jump-title">Quick Failure Jump:</span>
|
|
271
|
+
{' '.join(jump_links)}
|
|
272
|
+
</div>
|
|
273
|
+
"""
|
|
274
|
+
|
|
275
|
+
regressed_btn_html = '<button class="filter-btn" onclick="setFilter(\'regressed\', this)">Regressed</button>' if regression_report else ''
|
|
276
|
+
|
|
277
|
+
html_content = f"""<!DOCTYPE html>
|
|
278
|
+
<html lang="en">
|
|
279
|
+
<head>
|
|
280
|
+
<meta charset="UTF-8">
|
|
281
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
282
|
+
<title>evalrun Evaluation Report — {html.escape(manifest.get('run_id', 'N/A'))}</title>
|
|
283
|
+
<style>
|
|
284
|
+
:root {{
|
|
285
|
+
--bg-color: #0f172a;
|
|
286
|
+
--card-bg: #1e293b;
|
|
287
|
+
--text-main: #f8fafc;
|
|
288
|
+
--text-muted: #94a3b8;
|
|
289
|
+
--border-color: #334155;
|
|
290
|
+
--pass-color: #10b981;
|
|
291
|
+
--fail-color: #ef4444;
|
|
292
|
+
--block-color: #f59e0b;
|
|
293
|
+
--accent-blue: #3b82f6;
|
|
294
|
+
}}
|
|
295
|
+
body {{
|
|
296
|
+
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
|
|
297
|
+
background-color: var(--bg-color);
|
|
298
|
+
color: var(--text-main);
|
|
299
|
+
margin: 0;
|
|
300
|
+
padding: 24px;
|
|
301
|
+
line-height: 1.5;
|
|
302
|
+
}}
|
|
303
|
+
.container {{
|
|
304
|
+
max-width: 1200px;
|
|
305
|
+
margin: 0 auto;
|
|
306
|
+
}}
|
|
307
|
+
.verdict-banner {{
|
|
308
|
+
border-radius: 12px;
|
|
309
|
+
padding: 20px 24px;
|
|
310
|
+
margin-bottom: 24px;
|
|
311
|
+
font-weight: 600;
|
|
312
|
+
}}
|
|
313
|
+
.verdict-blocked {{
|
|
314
|
+
background: rgba(239, 68, 68, 0.15);
|
|
315
|
+
border: 2px solid var(--fail-color);
|
|
316
|
+
color: #fca5a5;
|
|
317
|
+
}}
|
|
318
|
+
.verdict-approved {{
|
|
319
|
+
background: rgba(16, 185, 129, 0.15);
|
|
320
|
+
border: 2px solid var(--pass-color);
|
|
321
|
+
color: #6ee7b7;
|
|
322
|
+
}}
|
|
323
|
+
.verdict-banner h2 {{ margin: 0 0 4px 0; font-size: 22px; }}
|
|
324
|
+
.verdict-banner p {{ margin: 0; font-size: 14px; opacity: 0.9; }}
|
|
325
|
+
header {{
|
|
326
|
+
background: var(--card-bg);
|
|
327
|
+
border: 1px solid var(--border-color);
|
|
328
|
+
border-radius: 12px;
|
|
329
|
+
padding: 24px;
|
|
330
|
+
margin-bottom: 24px;
|
|
331
|
+
}}
|
|
332
|
+
h1 {{
|
|
333
|
+
margin: 0 0 12px 0;
|
|
334
|
+
font-size: 24px;
|
|
335
|
+
color: #ffffff;
|
|
336
|
+
}}
|
|
337
|
+
.controls-bar {{
|
|
338
|
+
display: flex;
|
|
339
|
+
gap: 16px;
|
|
340
|
+
align-items: center;
|
|
341
|
+
margin-bottom: 24px;
|
|
342
|
+
flex-wrap: wrap;
|
|
343
|
+
}}
|
|
344
|
+
.search-input {{
|
|
345
|
+
background: #1e293b;
|
|
346
|
+
border: 1px solid var(--border-color);
|
|
347
|
+
color: #fff;
|
|
348
|
+
padding: 8px 14px;
|
|
349
|
+
border-radius: 6px;
|
|
350
|
+
font-size: 14px;
|
|
351
|
+
width: 260px;
|
|
352
|
+
}}
|
|
353
|
+
.filter-btn {{
|
|
354
|
+
background: #1e293b;
|
|
355
|
+
border: 1px solid var(--border-color);
|
|
356
|
+
color: var(--text-muted);
|
|
357
|
+
padding: 8px 16px;
|
|
358
|
+
border-radius: 6px;
|
|
359
|
+
cursor: pointer;
|
|
360
|
+
font-size: 13px;
|
|
361
|
+
font-weight: 600;
|
|
362
|
+
}}
|
|
363
|
+
.filter-btn.active {{
|
|
364
|
+
background: var(--accent-blue);
|
|
365
|
+
color: #fff;
|
|
366
|
+
border-color: var(--accent-blue);
|
|
367
|
+
}}
|
|
368
|
+
.meta-grid {{
|
|
369
|
+
display: grid;
|
|
370
|
+
grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
|
|
371
|
+
gap: 16px;
|
|
372
|
+
margin-top: 16px;
|
|
373
|
+
}}
|
|
374
|
+
.meta-item {{
|
|
375
|
+
background: #0f172a;
|
|
376
|
+
padding: 12px 16px;
|
|
377
|
+
border-radius: 8px;
|
|
378
|
+
border: 1px solid var(--border-color);
|
|
379
|
+
}}
|
|
380
|
+
.meta-label {{
|
|
381
|
+
font-size: 12px;
|
|
382
|
+
color: var(--text-muted);
|
|
383
|
+
text-transform: uppercase;
|
|
384
|
+
letter-spacing: 0.5px;
|
|
385
|
+
}}
|
|
386
|
+
.meta-val {{
|
|
387
|
+
font-size: 16px;
|
|
388
|
+
font-weight: 600;
|
|
389
|
+
margin-top: 4px;
|
|
390
|
+
word-break: break-all;
|
|
391
|
+
}}
|
|
392
|
+
.scenario-card {{
|
|
393
|
+
background: var(--card-bg);
|
|
394
|
+
border: 1px solid var(--border-color);
|
|
395
|
+
border-radius: 12px;
|
|
396
|
+
padding: 24px;
|
|
397
|
+
margin-bottom: 24px;
|
|
398
|
+
}}
|
|
399
|
+
.scenario-header {{
|
|
400
|
+
display: flex;
|
|
401
|
+
justify-content: space-between;
|
|
402
|
+
align-items: center;
|
|
403
|
+
border-bottom: 1px solid var(--border-color);
|
|
404
|
+
padding-bottom: 16px;
|
|
405
|
+
margin-bottom: 20px;
|
|
406
|
+
}}
|
|
407
|
+
.scenario-header h2 {{
|
|
408
|
+
margin: 0;
|
|
409
|
+
font-size: 20px;
|
|
410
|
+
}}
|
|
411
|
+
.badges {{
|
|
412
|
+
display: flex;
|
|
413
|
+
gap: 8px;
|
|
414
|
+
align-items: center;
|
|
415
|
+
}}
|
|
416
|
+
.badge {{
|
|
417
|
+
padding: 4px 12px;
|
|
418
|
+
border-radius: 20px;
|
|
419
|
+
font-size: 13px;
|
|
420
|
+
font-weight: 600;
|
|
421
|
+
}}
|
|
422
|
+
.badge-pass {{ background: rgba(16, 185, 129, 0.2); color: var(--pass-color); border: 1px solid var(--pass-color); }}
|
|
423
|
+
.badge-fail {{ background: rgba(239, 68, 68, 0.2); color: var(--fail-color); border: 1px solid var(--fail-color); }}
|
|
424
|
+
.badge-block {{ background: rgba(245, 158, 11, 0.2); color: var(--block-color); border: 1px solid var(--block-color); }}
|
|
425
|
+
.badge-regressed {{ background: rgba(239, 68, 68, 0.3); color: #fca5a5; border: 1px solid var(--fail-color); }}
|
|
426
|
+
.overall-score-pill {{
|
|
427
|
+
background: var(--accent-blue);
|
|
428
|
+
color: #fff;
|
|
429
|
+
padding: 4px 12px;
|
|
430
|
+
border-radius: 20px;
|
|
431
|
+
font-size: 13px;
|
|
432
|
+
font-weight: 600;
|
|
433
|
+
}}
|
|
434
|
+
.highlight-card {{
|
|
435
|
+
background: rgba(245, 158, 11, 0.1);
|
|
436
|
+
border: 1px solid var(--block-color);
|
|
437
|
+
border-radius: 6px;
|
|
438
|
+
padding: 10px 14px;
|
|
439
|
+
font-size: 13px;
|
|
440
|
+
margin-bottom: 16px;
|
|
441
|
+
}}
|
|
442
|
+
.section-card {{
|
|
443
|
+
background: #0f172a;
|
|
444
|
+
border: 1px solid var(--border-color);
|
|
445
|
+
border-radius: 8px;
|
|
446
|
+
padding: 16px;
|
|
447
|
+
margin-bottom: 16px;
|
|
448
|
+
}}
|
|
449
|
+
.section-card h3 {{
|
|
450
|
+
margin-top: 0;
|
|
451
|
+
font-size: 16px;
|
|
452
|
+
color: var(--text-muted);
|
|
453
|
+
}}
|
|
454
|
+
.delta-summary-grid {{
|
|
455
|
+
display: grid;
|
|
456
|
+
grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
|
|
457
|
+
gap: 12px;
|
|
458
|
+
margin-bottom: 12px;
|
|
459
|
+
font-size: 14px;
|
|
460
|
+
}}
|
|
461
|
+
.score-bar-container {{
|
|
462
|
+
background: #334155;
|
|
463
|
+
height: 8px;
|
|
464
|
+
border-radius: 4px;
|
|
465
|
+
overflow: hidden;
|
|
466
|
+
margin-bottom: 4px;
|
|
467
|
+
width: 100%;
|
|
468
|
+
}}
|
|
469
|
+
.score-bar-fill {{
|
|
470
|
+
background: var(--accent-blue);
|
|
471
|
+
height: 100%;
|
|
472
|
+
}}
|
|
473
|
+
table.data-table, table.trace-table {{
|
|
474
|
+
width: 100%;
|
|
475
|
+
border-collapse: collapse;
|
|
476
|
+
margin-top: 12px;
|
|
477
|
+
}}
|
|
478
|
+
table.data-table th, table.data-table td, table.trace-table th, table.trace-table td {{
|
|
479
|
+
text-align: left;
|
|
480
|
+
padding: 10px 12px;
|
|
481
|
+
border-bottom: 1px solid var(--border-color);
|
|
482
|
+
}}
|
|
483
|
+
table.data-table th {{
|
|
484
|
+
color: var(--text-muted);
|
|
485
|
+
font-size: 13px;
|
|
486
|
+
}}
|
|
487
|
+
.score-high {{ color: var(--pass-color); font-weight: 600; }}
|
|
488
|
+
.score-med {{ color: var(--block-color); font-weight: 600; }}
|
|
489
|
+
.score-low {{ color: var(--fail-color); font-weight: 600; }}
|
|
490
|
+
.violations-box {{
|
|
491
|
+
background: rgba(239, 68, 68, 0.1);
|
|
492
|
+
border: 1px solid var(--fail-color);
|
|
493
|
+
border-radius: 6px;
|
|
494
|
+
padding: 12px 16px;
|
|
495
|
+
margin-top: 12px;
|
|
496
|
+
}}
|
|
497
|
+
.violations-box h4 {{ margin: 0 0 8px 0; color: var(--fail-color); }}
|
|
498
|
+
.violations-box ul {{ margin: 0; padding-left: 20px; }}
|
|
499
|
+
.disc-amt {{ color: var(--block-color); font-size: 13px; }}
|
|
500
|
+
.raw-output-details {{
|
|
501
|
+
margin-top: 16px;
|
|
502
|
+
background: #0f172a;
|
|
503
|
+
border: 1px solid var(--border-color);
|
|
504
|
+
border-radius: 8px;
|
|
505
|
+
padding: 12px;
|
|
506
|
+
}}
|
|
507
|
+
.raw-output-details summary {{
|
|
508
|
+
cursor: pointer;
|
|
509
|
+
font-weight: 600;
|
|
510
|
+
color: var(--accent-blue);
|
|
511
|
+
}}
|
|
512
|
+
.raw-output-box {{
|
|
513
|
+
margin-top: 12px;
|
|
514
|
+
padding: 12px;
|
|
515
|
+
background: #1e293b;
|
|
516
|
+
border-radius: 6px;
|
|
517
|
+
font-family: monospace;
|
|
518
|
+
font-size: 12px;
|
|
519
|
+
white-space: pre-wrap;
|
|
520
|
+
word-break: break-word;
|
|
521
|
+
max-height: 400px;
|
|
522
|
+
overflow-y: auto;
|
|
523
|
+
}}
|
|
524
|
+
.jump-bar {{
|
|
525
|
+
margin-top: 12px;
|
|
526
|
+
display: flex;
|
|
527
|
+
gap: 8px;
|
|
528
|
+
align-items: center;
|
|
529
|
+
flex-wrap: wrap;
|
|
530
|
+
}}
|
|
531
|
+
.jump-title {{
|
|
532
|
+
font-size: 13px;
|
|
533
|
+
font-weight: 700;
|
|
534
|
+
color: #ffffff;
|
|
535
|
+
}}
|
|
536
|
+
.jump-pill {{
|
|
537
|
+
background: rgba(15, 23, 42, 0.6);
|
|
538
|
+
border: 1px solid var(--border-color);
|
|
539
|
+
color: #f8fafc;
|
|
540
|
+
text-decoration: none;
|
|
541
|
+
padding: 4px 10px;
|
|
542
|
+
border-radius: 4px;
|
|
543
|
+
font-size: 12px;
|
|
544
|
+
font-weight: 600;
|
|
545
|
+
}}
|
|
546
|
+
.jump-pill:hover {{
|
|
547
|
+
border-color: var(--accent-blue);
|
|
548
|
+
color: var(--accent-blue);
|
|
549
|
+
}}
|
|
550
|
+
code {{
|
|
551
|
+
background: #1e293b;
|
|
552
|
+
padding: 2px 6px;
|
|
553
|
+
border-radius: 4px;
|
|
554
|
+
font-family: monospace;
|
|
555
|
+
}}
|
|
556
|
+
footer {{
|
|
557
|
+
text-align: center;
|
|
558
|
+
color: var(--text-muted);
|
|
559
|
+
font-size: 12px;
|
|
560
|
+
margin-top: 40px;
|
|
561
|
+
}}
|
|
562
|
+
</style>
|
|
563
|
+
</head>
|
|
564
|
+
<body>
|
|
565
|
+
<div class="container">
|
|
566
|
+
<div class="verdict-banner {verdict_class}">
|
|
567
|
+
<h2>{verdict_title}</h2>
|
|
568
|
+
<p>{verdict_sub}</p>
|
|
569
|
+
{jump_bar_html}
|
|
570
|
+
</div>
|
|
571
|
+
|
|
572
|
+
<header>
|
|
573
|
+
<h1>evalrun Benchmark Evaluation Report</h1>
|
|
574
|
+
<div class="meta-grid">
|
|
575
|
+
<div class="meta-item">
|
|
576
|
+
<div class="meta-label">Run ID</div>
|
|
577
|
+
<div class="meta-val">{html.escape(manifest.get('run_id', 'N/A'))}</div>
|
|
578
|
+
</div>
|
|
579
|
+
<div class="meta-item">
|
|
580
|
+
<div class="meta-label">Target Model</div>
|
|
581
|
+
<div class="meta-val">{html.escape(manifest.get('target_model', {}).get('model_name', 'N/A'))}</div>
|
|
582
|
+
</div>
|
|
583
|
+
<div class="meta-item">
|
|
584
|
+
<div class="meta-label">Judge Model</div>
|
|
585
|
+
<div class="meta-val">{html.escape(manifest.get('judge_model', {}).get('model_name', 'N/A'))}</div>
|
|
586
|
+
</div>
|
|
587
|
+
<div class="meta-item">
|
|
588
|
+
<div class="meta-label">Average Score</div>
|
|
589
|
+
<div class="meta-val">{avg_score:.2f} / 100</div>
|
|
590
|
+
</div>
|
|
591
|
+
<div class="meta-item">
|
|
592
|
+
<div class="meta-label">Pass / Fail / Block</div>
|
|
593
|
+
<div class="meta-val">{passed_count} / {failed_count} / {blocked_count}</div>
|
|
594
|
+
</div>
|
|
595
|
+
</div>
|
|
596
|
+
</header>
|
|
597
|
+
|
|
598
|
+
<div class="controls-bar">
|
|
599
|
+
<input type="text" id="searchInput" class="search-input" placeholder="Search scenarios..." onkeyup="filterCards()">
|
|
600
|
+
<button class="filter-btn active" onclick="setFilter('all', this)">All ({total_runs})</button>
|
|
601
|
+
<button class="filter-btn" onclick="setFilter('passed', this)">Passed ({passed_count})</button>
|
|
602
|
+
<button class="filter-btn" onclick="setFilter('failed', this)">Failed ({failed_count})</button>
|
|
603
|
+
<button class="filter-btn" onclick="setFilter('blocked', this)">Blocked ({blocked_count})</button>
|
|
604
|
+
{regressed_btn_html}
|
|
605
|
+
</div>
|
|
606
|
+
|
|
607
|
+
<div id="cardsContainer">
|
|
608
|
+
{''.join(cards_html)}
|
|
609
|
+
</div>
|
|
610
|
+
|
|
611
|
+
<footer>
|
|
612
|
+
Generated by evalrun Agent Evaluation Platform • {html.escape(manifest.get('timestamp_utc', ''))}
|
|
613
|
+
</footer>
|
|
614
|
+
</div>
|
|
615
|
+
|
|
616
|
+
<script>
|
|
617
|
+
let currentFilter = 'all';
|
|
618
|
+
function setFilter(filter, btn) {{
|
|
619
|
+
currentFilter = filter;
|
|
620
|
+
document.querySelectorAll('.filter-btn').forEach(b => b.classList.remove('active'));
|
|
621
|
+
btn.classList.add('active');
|
|
622
|
+
filterCards();
|
|
623
|
+
}}
|
|
624
|
+
function filterCards() {{
|
|
625
|
+
const search = document.getElementById('searchInput').value.toLowerCase();
|
|
626
|
+
const cards = document.querySelectorAll('.scenario-card');
|
|
627
|
+
cards.forEach(card => {{
|
|
628
|
+
const status = card.getAttribute('data-status');
|
|
629
|
+
const name = card.getAttribute('data-name');
|
|
630
|
+
const matchesFilter = (currentFilter === 'all') || (status === currentFilter);
|
|
631
|
+
const matchesSearch = name.includes(search);
|
|
632
|
+
if (matchesFilter && matchesSearch) {{
|
|
633
|
+
card.style.display = 'block';
|
|
634
|
+
}} else {{
|
|
635
|
+
card.style.display = 'none';
|
|
636
|
+
}}
|
|
637
|
+
}});
|
|
638
|
+
}}
|
|
639
|
+
</script>
|
|
640
|
+
</body>
|
|
641
|
+
</html>
|
|
642
|
+
"""
|
|
643
|
+
|
|
644
|
+
with open(output_path, "w", encoding="utf-8") as f:
|
|
645
|
+
f.write(html_content)
|
|
646
|
+
|
|
647
|
+
return str(output_path)
|