themis-eval 0.1.0__py3-none-any.whl → 0.1.1__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.
- themis/cli/__init__.py +5 -0
- themis/cli/__main__.py +6 -0
- themis/cli/commands/__init__.py +19 -0
- themis/cli/commands/benchmarks.py +221 -0
- themis/cli/commands/comparison.py +394 -0
- themis/cli/commands/config_commands.py +244 -0
- themis/cli/commands/cost.py +214 -0
- themis/cli/commands/demo.py +68 -0
- themis/cli/commands/info.py +90 -0
- themis/cli/commands/leaderboard.py +362 -0
- themis/cli/commands/math_benchmarks.py +318 -0
- themis/cli/commands/mcq_benchmarks.py +207 -0
- themis/cli/commands/sample_run.py +244 -0
- themis/cli/commands/visualize.py +299 -0
- themis/cli/main.py +93 -0
- themis/cli/new_project.py +33 -0
- themis/cli/utils.py +51 -0
- themis/config/__init__.py +19 -0
- themis/config/loader.py +27 -0
- themis/config/registry.py +34 -0
- themis/config/runtime.py +214 -0
- themis/config/schema.py +112 -0
- themis/core/__init__.py +5 -0
- themis/core/conversation.py +354 -0
- themis/core/entities.py +164 -0
- themis/core/serialization.py +231 -0
- themis/core/tools.py +393 -0
- themis/core/types.py +141 -0
- themis/datasets/__init__.py +273 -0
- themis/datasets/base.py +264 -0
- themis/datasets/commonsense_qa.py +174 -0
- themis/datasets/competition_math.py +265 -0
- themis/datasets/coqa.py +133 -0
- themis/datasets/gpqa.py +190 -0
- themis/datasets/gsm8k.py +123 -0
- themis/datasets/gsm_symbolic.py +124 -0
- themis/datasets/math500.py +122 -0
- themis/datasets/med_qa.py +179 -0
- themis/datasets/medmcqa.py +169 -0
- themis/datasets/mmlu_pro.py +262 -0
- themis/datasets/piqa.py +146 -0
- themis/datasets/registry.py +201 -0
- themis/datasets/schema.py +245 -0
- themis/datasets/sciq.py +150 -0
- themis/datasets/social_i_qa.py +151 -0
- themis/datasets/super_gpqa.py +263 -0
- themis/evaluation/__init__.py +1 -0
- themis/evaluation/conditional.py +410 -0
- themis/evaluation/extractors/__init__.py +19 -0
- themis/evaluation/extractors/error_taxonomy_extractor.py +80 -0
- themis/evaluation/extractors/exceptions.py +7 -0
- themis/evaluation/extractors/identity_extractor.py +29 -0
- themis/evaluation/extractors/json_field_extractor.py +45 -0
- themis/evaluation/extractors/math_verify_extractor.py +37 -0
- themis/evaluation/extractors/regex_extractor.py +43 -0
- themis/evaluation/math_verify_utils.py +87 -0
- themis/evaluation/metrics/__init__.py +21 -0
- themis/evaluation/metrics/composite_metric.py +47 -0
- themis/evaluation/metrics/consistency_metric.py +80 -0
- themis/evaluation/metrics/exact_match.py +51 -0
- themis/evaluation/metrics/length_difference_tolerance.py +33 -0
- themis/evaluation/metrics/math_verify_accuracy.py +40 -0
- themis/evaluation/metrics/pairwise_judge_metric.py +141 -0
- themis/evaluation/metrics/response_length.py +33 -0
- themis/evaluation/metrics/rubric_judge_metric.py +134 -0
- themis/evaluation/pipeline.py +49 -0
- themis/evaluation/pipelines/__init__.py +15 -0
- themis/evaluation/pipelines/composable_pipeline.py +357 -0
- themis/evaluation/pipelines/standard_pipeline.py +288 -0
- themis/evaluation/reports.py +293 -0
- themis/evaluation/statistics/__init__.py +53 -0
- themis/evaluation/statistics/bootstrap.py +79 -0
- themis/evaluation/statistics/confidence_intervals.py +121 -0
- themis/evaluation/statistics/distributions.py +207 -0
- themis/evaluation/statistics/effect_sizes.py +124 -0
- themis/evaluation/statistics/hypothesis_tests.py +305 -0
- themis/evaluation/statistics/types.py +139 -0
- themis/evaluation/strategies/__init__.py +13 -0
- themis/evaluation/strategies/attempt_aware_evaluation_strategy.py +51 -0
- themis/evaluation/strategies/default_evaluation_strategy.py +25 -0
- themis/evaluation/strategies/evaluation_strategy.py +24 -0
- themis/evaluation/strategies/judge_evaluation_strategy.py +64 -0
- themis/experiment/__init__.py +5 -0
- themis/experiment/builder.py +151 -0
- themis/experiment/cache_manager.py +129 -0
- themis/experiment/comparison.py +631 -0
- themis/experiment/cost.py +310 -0
- themis/experiment/definitions.py +62 -0
- themis/experiment/export.py +690 -0
- themis/experiment/export_csv.py +159 -0
- themis/experiment/integration_manager.py +104 -0
- themis/experiment/math.py +192 -0
- themis/experiment/mcq.py +169 -0
- themis/experiment/orchestrator.py +373 -0
- themis/experiment/pricing.py +317 -0
- themis/experiment/storage.py +255 -0
- themis/experiment/visualization.py +588 -0
- themis/generation/__init__.py +1 -0
- themis/generation/agentic_runner.py +420 -0
- themis/generation/batching.py +254 -0
- themis/generation/clients.py +143 -0
- themis/generation/conversation_runner.py +236 -0
- themis/generation/plan.py +456 -0
- themis/generation/providers/litellm_provider.py +221 -0
- themis/generation/providers/vllm_provider.py +135 -0
- themis/generation/router.py +34 -0
- themis/generation/runner.py +207 -0
- themis/generation/strategies.py +98 -0
- themis/generation/templates.py +71 -0
- themis/generation/turn_strategies.py +393 -0
- themis/generation/types.py +9 -0
- themis/integrations/__init__.py +0 -0
- themis/integrations/huggingface.py +61 -0
- themis/integrations/wandb.py +65 -0
- themis/interfaces/__init__.py +83 -0
- themis/project/__init__.py +20 -0
- themis/project/definitions.py +98 -0
- themis/project/patterns.py +230 -0
- themis/providers/__init__.py +5 -0
- themis/providers/registry.py +39 -0
- themis/utils/api_generator.py +379 -0
- themis/utils/cost_tracking.py +376 -0
- themis/utils/dashboard.py +452 -0
- themis/utils/logging_utils.py +41 -0
- themis/utils/progress.py +58 -0
- themis/utils/tracing.py +320 -0
- {themis_eval-0.1.0.dist-info → themis_eval-0.1.1.dist-info}/METADATA +1 -1
- themis_eval-0.1.1.dist-info/RECORD +134 -0
- themis_eval-0.1.0.dist-info/RECORD +0 -8
- {themis_eval-0.1.0.dist-info → themis_eval-0.1.1.dist-info}/WHEEL +0 -0
- {themis_eval-0.1.0.dist-info → themis_eval-0.1.1.dist-info}/licenses/LICENSE +0 -0
- {themis_eval-0.1.0.dist-info → themis_eval-0.1.1.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,452 @@
|
|
|
1
|
+
"""Dashboard generator for experiment results, costs, and statistics.
|
|
2
|
+
|
|
3
|
+
This module provides HTML dashboard generation for visualizing experiment
|
|
4
|
+
results, cost breakdowns, and statistical analysis.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
from typing import Dict, List
|
|
11
|
+
|
|
12
|
+
from themis.evaluation import reports as eval_reports
|
|
13
|
+
from themis.evaluation import statistics as eval_stats
|
|
14
|
+
from themis.utils import cost_tracking
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def generate_html_dashboard(
|
|
18
|
+
evaluation_report: eval_reports.EvaluationReport,
|
|
19
|
+
cost_summary: cost_tracking.CostSummary | None = None,
|
|
20
|
+
statistical_summaries: Dict[str, eval_stats.StatisticalSummary] | None = None,
|
|
21
|
+
output_path: str | Path = "dashboard.html",
|
|
22
|
+
title: str = "Themis Experiment Dashboard",
|
|
23
|
+
) -> None:
|
|
24
|
+
"""Generate HTML dashboard with evaluation results, costs, and statistics.
|
|
25
|
+
|
|
26
|
+
Args:
|
|
27
|
+
evaluation_report: Evaluation report with metric results
|
|
28
|
+
cost_summary: Optional cost summary
|
|
29
|
+
statistical_summaries: Optional dictionary mapping metric names to statistical summaries
|
|
30
|
+
output_path: Path to output HTML file
|
|
31
|
+
title: Dashboard title
|
|
32
|
+
"""
|
|
33
|
+
output_path = Path(output_path)
|
|
34
|
+
output_path.parent.mkdir(parents=True, exist_ok=True)
|
|
35
|
+
|
|
36
|
+
html_content = _generate_html(
|
|
37
|
+
evaluation_report,
|
|
38
|
+
cost_summary,
|
|
39
|
+
statistical_summaries,
|
|
40
|
+
title,
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
with open(output_path, "w") as f:
|
|
44
|
+
f.write(html_content)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def _generate_html(
|
|
48
|
+
evaluation_report: eval_reports.EvaluationReport,
|
|
49
|
+
cost_summary: cost_tracking.CostSummary | None,
|
|
50
|
+
statistical_summaries: Dict[str, eval_stats.StatisticalSummary] | None,
|
|
51
|
+
title: str,
|
|
52
|
+
) -> str:
|
|
53
|
+
"""Generate complete HTML dashboard content."""
|
|
54
|
+
|
|
55
|
+
# Build sections
|
|
56
|
+
metrics_section = _build_metrics_section(evaluation_report.metrics)
|
|
57
|
+
|
|
58
|
+
stats_section = ""
|
|
59
|
+
if statistical_summaries:
|
|
60
|
+
stats_section = _build_statistics_section(statistical_summaries)
|
|
61
|
+
|
|
62
|
+
cost_section = ""
|
|
63
|
+
if cost_summary:
|
|
64
|
+
cost_section = _build_cost_section(cost_summary)
|
|
65
|
+
|
|
66
|
+
failures_section = ""
|
|
67
|
+
if evaluation_report.failures:
|
|
68
|
+
failures_section = _build_failures_section(evaluation_report.failures)
|
|
69
|
+
|
|
70
|
+
# Compose full HTML
|
|
71
|
+
html = f"""<!DOCTYPE html>
|
|
72
|
+
<html lang="en">
|
|
73
|
+
<head>
|
|
74
|
+
<meta charset="UTF-8">
|
|
75
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
76
|
+
<title>{title}</title>
|
|
77
|
+
<style>
|
|
78
|
+
* {{
|
|
79
|
+
margin: 0;
|
|
80
|
+
padding: 0;
|
|
81
|
+
box-sizing: border-box;
|
|
82
|
+
}}
|
|
83
|
+
|
|
84
|
+
body {{
|
|
85
|
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Helvetica', 'Arial', sans-serif;
|
|
86
|
+
line-height: 1.6;
|
|
87
|
+
color: #333;
|
|
88
|
+
background: #f5f5f5;
|
|
89
|
+
padding: 20px;
|
|
90
|
+
}}
|
|
91
|
+
|
|
92
|
+
.container {{
|
|
93
|
+
max-width: 1200px;
|
|
94
|
+
margin: 0 auto;
|
|
95
|
+
background: white;
|
|
96
|
+
padding: 30px;
|
|
97
|
+
border-radius: 8px;
|
|
98
|
+
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
|
99
|
+
}}
|
|
100
|
+
|
|
101
|
+
h1 {{
|
|
102
|
+
color: #2c3e50;
|
|
103
|
+
margin-bottom: 30px;
|
|
104
|
+
padding-bottom: 10px;
|
|
105
|
+
border-bottom: 3px solid #3498db;
|
|
106
|
+
}}
|
|
107
|
+
|
|
108
|
+
h2 {{
|
|
109
|
+
color: #34495e;
|
|
110
|
+
margin-top: 30px;
|
|
111
|
+
margin-bottom: 15px;
|
|
112
|
+
padding-bottom: 8px;
|
|
113
|
+
border-bottom: 2px solid #ecf0f1;
|
|
114
|
+
}}
|
|
115
|
+
|
|
116
|
+
h3 {{
|
|
117
|
+
color: #7f8c8d;
|
|
118
|
+
margin-top: 20px;
|
|
119
|
+
margin-bottom: 10px;
|
|
120
|
+
}}
|
|
121
|
+
|
|
122
|
+
.metric-card {{
|
|
123
|
+
background: #f8f9fa;
|
|
124
|
+
border-left: 4px solid #3498db;
|
|
125
|
+
padding: 15px;
|
|
126
|
+
margin-bottom: 15px;
|
|
127
|
+
border-radius: 4px;
|
|
128
|
+
}}
|
|
129
|
+
|
|
130
|
+
.metric-name {{
|
|
131
|
+
font-size: 18px;
|
|
132
|
+
font-weight: 600;
|
|
133
|
+
color: #2c3e50;
|
|
134
|
+
margin-bottom: 8px;
|
|
135
|
+
}}
|
|
136
|
+
|
|
137
|
+
.metric-value {{
|
|
138
|
+
font-size: 32px;
|
|
139
|
+
font-weight: 700;
|
|
140
|
+
color: #3498db;
|
|
141
|
+
margin: 10px 0;
|
|
142
|
+
}}
|
|
143
|
+
|
|
144
|
+
.metric-details {{
|
|
145
|
+
display: grid;
|
|
146
|
+
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
|
|
147
|
+
gap: 10px;
|
|
148
|
+
margin-top: 10px;
|
|
149
|
+
}}
|
|
150
|
+
|
|
151
|
+
.detail-item {{
|
|
152
|
+
background: white;
|
|
153
|
+
padding: 10px;
|
|
154
|
+
border-radius: 4px;
|
|
155
|
+
}}
|
|
156
|
+
|
|
157
|
+
.detail-label {{
|
|
158
|
+
font-size: 12px;
|
|
159
|
+
color: #7f8c8d;
|
|
160
|
+
text-transform: uppercase;
|
|
161
|
+
letter-spacing: 0.5px;
|
|
162
|
+
}}
|
|
163
|
+
|
|
164
|
+
.detail-value {{
|
|
165
|
+
font-size: 16px;
|
|
166
|
+
font-weight: 600;
|
|
167
|
+
color: #2c3e50;
|
|
168
|
+
margin-top: 4px;
|
|
169
|
+
}}
|
|
170
|
+
|
|
171
|
+
.cost-summary {{
|
|
172
|
+
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
|
173
|
+
color: white;
|
|
174
|
+
padding: 20px;
|
|
175
|
+
border-radius: 8px;
|
|
176
|
+
margin-bottom: 20px;
|
|
177
|
+
}}
|
|
178
|
+
|
|
179
|
+
.cost-total {{
|
|
180
|
+
font-size: 48px;
|
|
181
|
+
font-weight: 700;
|
|
182
|
+
margin: 10px 0;
|
|
183
|
+
}}
|
|
184
|
+
|
|
185
|
+
.cost-breakdown {{
|
|
186
|
+
display: grid;
|
|
187
|
+
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
|
188
|
+
gap: 15px;
|
|
189
|
+
margin-top: 15px;
|
|
190
|
+
}}
|
|
191
|
+
|
|
192
|
+
.cost-item {{
|
|
193
|
+
background: rgba(255, 255, 255, 0.1);
|
|
194
|
+
padding: 12px;
|
|
195
|
+
border-radius: 4px;
|
|
196
|
+
}}
|
|
197
|
+
|
|
198
|
+
.cost-item-name {{
|
|
199
|
+
font-size: 14px;
|
|
200
|
+
opacity: 0.9;
|
|
201
|
+
}}
|
|
202
|
+
|
|
203
|
+
.cost-item-value {{
|
|
204
|
+
font-size: 20px;
|
|
205
|
+
font-weight: 600;
|
|
206
|
+
margin-top: 4px;
|
|
207
|
+
}}
|
|
208
|
+
|
|
209
|
+
.failures {{
|
|
210
|
+
background: #fee;
|
|
211
|
+
border-left: 4px solid #e74c3c;
|
|
212
|
+
padding: 15px;
|
|
213
|
+
border-radius: 4px;
|
|
214
|
+
}}
|
|
215
|
+
|
|
216
|
+
.failure-item {{
|
|
217
|
+
margin: 10px 0;
|
|
218
|
+
padding: 10px;
|
|
219
|
+
background: white;
|
|
220
|
+
border-radius: 4px;
|
|
221
|
+
}}
|
|
222
|
+
|
|
223
|
+
.confidence-interval {{
|
|
224
|
+
font-size: 14px;
|
|
225
|
+
color: #7f8c8d;
|
|
226
|
+
font-family: 'Courier New', monospace;
|
|
227
|
+
}}
|
|
228
|
+
|
|
229
|
+
.badge {{
|
|
230
|
+
display: inline-block;
|
|
231
|
+
padding: 4px 8px;
|
|
232
|
+
border-radius: 4px;
|
|
233
|
+
font-size: 12px;
|
|
234
|
+
font-weight: 600;
|
|
235
|
+
}}
|
|
236
|
+
|
|
237
|
+
.badge-success {{
|
|
238
|
+
background: #d4edda;
|
|
239
|
+
color: #155724;
|
|
240
|
+
}}
|
|
241
|
+
|
|
242
|
+
.badge-info {{
|
|
243
|
+
background: #d1ecf1;
|
|
244
|
+
color: #0c5460;
|
|
245
|
+
}}
|
|
246
|
+
</style>
|
|
247
|
+
</head>
|
|
248
|
+
<body>
|
|
249
|
+
<div class="container">
|
|
250
|
+
<h1>{title}</h1>
|
|
251
|
+
|
|
252
|
+
{metrics_section}
|
|
253
|
+
|
|
254
|
+
{stats_section}
|
|
255
|
+
|
|
256
|
+
{cost_section}
|
|
257
|
+
|
|
258
|
+
{failures_section}
|
|
259
|
+
</div>
|
|
260
|
+
</body>
|
|
261
|
+
</html>"""
|
|
262
|
+
|
|
263
|
+
return html
|
|
264
|
+
|
|
265
|
+
|
|
266
|
+
def _build_metrics_section(metrics: Dict[str, eval_reports.MetricAggregate]) -> str:
|
|
267
|
+
"""Build metrics overview section."""
|
|
268
|
+
if not metrics:
|
|
269
|
+
return "<p>No metrics available.</p>"
|
|
270
|
+
|
|
271
|
+
cards = []
|
|
272
|
+
for metric_name, aggregate in metrics.items():
|
|
273
|
+
card = f"""
|
|
274
|
+
<div class="metric-card">
|
|
275
|
+
<div class="metric-name">{metric_name}</div>
|
|
276
|
+
<div class="metric-value">{aggregate.mean:.4f}</div>
|
|
277
|
+
<div class="metric-details">
|
|
278
|
+
<div class="detail-item">
|
|
279
|
+
<div class="detail-label">Samples</div>
|
|
280
|
+
<div class="detail-value">{aggregate.count}</div>
|
|
281
|
+
</div>
|
|
282
|
+
</div>
|
|
283
|
+
</div>
|
|
284
|
+
"""
|
|
285
|
+
cards.append(card)
|
|
286
|
+
|
|
287
|
+
return f"""
|
|
288
|
+
<h2>📊 Metrics Overview</h2>
|
|
289
|
+
{"".join(cards)}
|
|
290
|
+
"""
|
|
291
|
+
|
|
292
|
+
|
|
293
|
+
def _build_statistics_section(
|
|
294
|
+
statistical_summaries: Dict[str, eval_stats.StatisticalSummary],
|
|
295
|
+
) -> str:
|
|
296
|
+
"""Build detailed statistics section."""
|
|
297
|
+
if not statistical_summaries:
|
|
298
|
+
return ""
|
|
299
|
+
|
|
300
|
+
cards = []
|
|
301
|
+
for metric_name, summary in statistical_summaries.items():
|
|
302
|
+
ci_text = ""
|
|
303
|
+
if summary.confidence_interval_95:
|
|
304
|
+
ci = summary.confidence_interval_95
|
|
305
|
+
ci_text = f"""
|
|
306
|
+
<div class="detail-item">
|
|
307
|
+
<div class="detail-label">95% CI</div>
|
|
308
|
+
<div class="detail-value confidence-interval">
|
|
309
|
+
[{ci.lower:.4f}, {ci.upper:.4f}]
|
|
310
|
+
</div>
|
|
311
|
+
</div>
|
|
312
|
+
"""
|
|
313
|
+
|
|
314
|
+
card = f"""
|
|
315
|
+
<div class="metric-card">
|
|
316
|
+
<div class="metric-name">{metric_name} - Statistical Analysis</div>
|
|
317
|
+
<div class="metric-details">
|
|
318
|
+
<div class="detail-item">
|
|
319
|
+
<div class="detail-label">Mean</div>
|
|
320
|
+
<div class="detail-value">{summary.mean:.4f}</div>
|
|
321
|
+
</div>
|
|
322
|
+
<div class="detail-item">
|
|
323
|
+
<div class="detail-label">Std Dev</div>
|
|
324
|
+
<div class="detail-value">{summary.std:.4f}</div>
|
|
325
|
+
</div>
|
|
326
|
+
<div class="detail-item">
|
|
327
|
+
<div class="detail-label">Median</div>
|
|
328
|
+
<div class="detail-value">{summary.median:.4f}</div>
|
|
329
|
+
</div>
|
|
330
|
+
<div class="detail-item">
|
|
331
|
+
<div class="detail-label">Min</div>
|
|
332
|
+
<div class="detail-value">{summary.min_value:.4f}</div>
|
|
333
|
+
</div>
|
|
334
|
+
<div class="detail-item">
|
|
335
|
+
<div class="detail-label">Max</div>
|
|
336
|
+
<div class="detail-value">{summary.max_value:.4f}</div>
|
|
337
|
+
</div>
|
|
338
|
+
{ci_text}
|
|
339
|
+
</div>
|
|
340
|
+
</div>
|
|
341
|
+
"""
|
|
342
|
+
cards.append(card)
|
|
343
|
+
|
|
344
|
+
return f"""
|
|
345
|
+
<h2>📈 Statistical Analysis</h2>
|
|
346
|
+
{"".join(cards)}
|
|
347
|
+
"""
|
|
348
|
+
|
|
349
|
+
|
|
350
|
+
def _build_cost_section(cost_summary: cost_tracking.CostSummary) -> str:
|
|
351
|
+
"""Build cost tracking section."""
|
|
352
|
+
|
|
353
|
+
# Model breakdown
|
|
354
|
+
model_items = []
|
|
355
|
+
for model, cost in sorted(
|
|
356
|
+
cost_summary.cost_by_model.items(),
|
|
357
|
+
key=lambda x: x[1],
|
|
358
|
+
reverse=True,
|
|
359
|
+
):
|
|
360
|
+
pct = (
|
|
361
|
+
(cost / cost_summary.total_cost * 100) if cost_summary.total_cost > 0 else 0
|
|
362
|
+
)
|
|
363
|
+
model_items.append(f"""
|
|
364
|
+
<div class="cost-item">
|
|
365
|
+
<div class="cost-item-name">{model}</div>
|
|
366
|
+
<div class="cost-item-value">${cost:.4f} ({pct:.1f}%)</div>
|
|
367
|
+
</div>
|
|
368
|
+
""")
|
|
369
|
+
|
|
370
|
+
# Provider breakdown
|
|
371
|
+
provider_items = []
|
|
372
|
+
for provider, cost in sorted(
|
|
373
|
+
cost_summary.cost_by_provider.items(),
|
|
374
|
+
key=lambda x: x[1],
|
|
375
|
+
reverse=True,
|
|
376
|
+
):
|
|
377
|
+
pct = (
|
|
378
|
+
(cost / cost_summary.total_cost * 100) if cost_summary.total_cost > 0 else 0
|
|
379
|
+
)
|
|
380
|
+
provider_items.append(f"""
|
|
381
|
+
<div class="cost-item">
|
|
382
|
+
<div class="cost-item-name">{provider}</div>
|
|
383
|
+
<div class="cost-item-value">${cost:.4f} ({pct:.1f}%)</div>
|
|
384
|
+
</div>
|
|
385
|
+
""")
|
|
386
|
+
|
|
387
|
+
return f"""
|
|
388
|
+
<h2>💰 Cost Tracking</h2>
|
|
389
|
+
<div class="cost-summary">
|
|
390
|
+
<h3 style="color: white; margin-top: 0;">Total Cost</h3>
|
|
391
|
+
<div class="cost-total">${cost_summary.total_cost:.4f}</div>
|
|
392
|
+
<div class="cost-breakdown">
|
|
393
|
+
<div class="cost-item">
|
|
394
|
+
<div class="cost-item-name">Total Tokens</div>
|
|
395
|
+
<div class="cost-item-value">{cost_summary.total_tokens:,}</div>
|
|
396
|
+
</div>
|
|
397
|
+
<div class="cost-item">
|
|
398
|
+
<div class="cost-item-name">Input Tokens</div>
|
|
399
|
+
<div class="cost-item-value">{cost_summary.total_input_tokens:,}</div>
|
|
400
|
+
</div>
|
|
401
|
+
<div class="cost-item">
|
|
402
|
+
<div class="cost-item-name">Output Tokens</div>
|
|
403
|
+
<div class="cost-item-value">{cost_summary.total_output_tokens:,}</div>
|
|
404
|
+
</div>
|
|
405
|
+
<div class="cost-item">
|
|
406
|
+
<div class="cost-item-name">API Requests</div>
|
|
407
|
+
<div class="cost-item-value">{cost_summary.num_requests:,}</div>
|
|
408
|
+
</div>
|
|
409
|
+
</div>
|
|
410
|
+
</div>
|
|
411
|
+
|
|
412
|
+
<h3>Cost by Model</h3>
|
|
413
|
+
<div class="cost-breakdown">
|
|
414
|
+
{"".join(model_items)}
|
|
415
|
+
</div>
|
|
416
|
+
|
|
417
|
+
<h3>Cost by Provider</h3>
|
|
418
|
+
<div class="cost-breakdown">
|
|
419
|
+
{"".join(provider_items)}
|
|
420
|
+
</div>
|
|
421
|
+
"""
|
|
422
|
+
|
|
423
|
+
|
|
424
|
+
def _build_failures_section(failures: List[eval_reports.EvaluationFailure]) -> str:
|
|
425
|
+
"""Build failures section."""
|
|
426
|
+
if not failures:
|
|
427
|
+
return ""
|
|
428
|
+
|
|
429
|
+
failure_items = []
|
|
430
|
+
for failure in failures[:20]: # Limit to first 20 failures
|
|
431
|
+
sample_id = failure.sample_id or "Unknown"
|
|
432
|
+
failure_items.append(f"""
|
|
433
|
+
<div class="failure-item">
|
|
434
|
+
<strong>Sample: {sample_id}</strong><br>
|
|
435
|
+
{failure.message}
|
|
436
|
+
</div>
|
|
437
|
+
""")
|
|
438
|
+
|
|
439
|
+
more_text = ""
|
|
440
|
+
if len(failures) > 20:
|
|
441
|
+
more_text = f"<p><em>...and {len(failures) - 20} more failures</em></p>"
|
|
442
|
+
|
|
443
|
+
return f"""
|
|
444
|
+
<h2>⚠️ Failures ({len(failures)})</h2>
|
|
445
|
+
<div class="failures">
|
|
446
|
+
{"".join(failure_items)}
|
|
447
|
+
{more_text}
|
|
448
|
+
</div>
|
|
449
|
+
"""
|
|
450
|
+
|
|
451
|
+
|
|
452
|
+
__all__ = ["generate_html_dashboard"]
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
"""Utility helpers for configuring package-wide logging."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import logging
|
|
6
|
+
from typing import Mapping
|
|
7
|
+
|
|
8
|
+
TRACE_LEVEL = 5
|
|
9
|
+
logging.addLevelName(TRACE_LEVEL, "TRACE")
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def _trace(self, message, *args, **kwargs):
|
|
13
|
+
if self.isEnabledFor(TRACE_LEVEL):
|
|
14
|
+
self._log(TRACE_LEVEL, message, args, **kwargs)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
logging.Logger.trace = _trace # type: ignore[attr-defined]
|
|
18
|
+
|
|
19
|
+
_LEVELS: Mapping[str, int] = {
|
|
20
|
+
"critical": logging.CRITICAL,
|
|
21
|
+
"error": logging.ERROR,
|
|
22
|
+
"warning": logging.WARNING,
|
|
23
|
+
"info": logging.INFO,
|
|
24
|
+
"debug": logging.DEBUG,
|
|
25
|
+
"trace": TRACE_LEVEL,
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def configure_logging(level: str = "info") -> None:
|
|
30
|
+
"""Configure root logging with human-friendly formatting."""
|
|
31
|
+
|
|
32
|
+
numeric_level = _LEVELS.get(level.lower(), logging.INFO)
|
|
33
|
+
logging.basicConfig(
|
|
34
|
+
level=numeric_level,
|
|
35
|
+
format="%(asctime)s | %(levelname)s | %(name)s | %(message)s",
|
|
36
|
+
datefmt="%H:%M:%S",
|
|
37
|
+
force=True,
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
__all__ = ["configure_logging", "TRACE_LEVEL"]
|
themis/utils/progress.py
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
"""Simple CLI-friendly progress reporter."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from contextlib import AbstractContextManager
|
|
6
|
+
from typing import Any, Callable
|
|
7
|
+
|
|
8
|
+
from tqdm import tqdm
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class ProgressReporter(AbstractContextManager["ProgressReporter"]):
|
|
12
|
+
def __init__(
|
|
13
|
+
self,
|
|
14
|
+
*,
|
|
15
|
+
total: int | None,
|
|
16
|
+
description: str = "Processing",
|
|
17
|
+
unit: str = "sample",
|
|
18
|
+
leave: bool = False,
|
|
19
|
+
) -> None:
|
|
20
|
+
self._total = total
|
|
21
|
+
self._description = description
|
|
22
|
+
self._unit = unit
|
|
23
|
+
self._leave = leave
|
|
24
|
+
self._pbar: tqdm | None = None
|
|
25
|
+
|
|
26
|
+
def __enter__(self) -> "ProgressReporter":
|
|
27
|
+
self.start()
|
|
28
|
+
return self
|
|
29
|
+
|
|
30
|
+
def __exit__(self, *_exc) -> None:
|
|
31
|
+
self.close()
|
|
32
|
+
|
|
33
|
+
def start(self) -> None:
|
|
34
|
+
if self._pbar is None:
|
|
35
|
+
self._pbar = tqdm(
|
|
36
|
+
total=self._total,
|
|
37
|
+
desc=self._description,
|
|
38
|
+
unit=self._unit,
|
|
39
|
+
leave=self._leave,
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
def close(self) -> None:
|
|
43
|
+
if self._pbar is not None:
|
|
44
|
+
self._pbar.close()
|
|
45
|
+
self._pbar = None
|
|
46
|
+
|
|
47
|
+
def increment(self, step: int = 1) -> None:
|
|
48
|
+
if self._pbar is not None:
|
|
49
|
+
self._pbar.update(step)
|
|
50
|
+
|
|
51
|
+
def on_result(self, _record: Any) -> None:
|
|
52
|
+
self.increment()
|
|
53
|
+
|
|
54
|
+
def as_callback(self) -> Callable[[Any], None]:
|
|
55
|
+
return self.on_result
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
__all__ = ["ProgressReporter"]
|