text2sql-eval-toolkit 1.0.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.
- text2sql_eval_toolkit/__init__.py +107 -0
- text2sql_eval_toolkit/analysis/__init__.py +5 -0
- text2sql_eval_toolkit/analysis/error_analysis.py +335 -0
- text2sql_eval_toolkit/analysis/report_tools.py +719 -0
- text2sql_eval_toolkit/config_args.py +65 -0
- text2sql_eval_toolkit/data/__init__.py +4 -0
- text2sql_eval_toolkit/data/benchmarks.json +69 -0
- text2sql_eval_toolkit/data/test-benchmarks.json +69 -0
- text2sql_eval_toolkit/env_loader.py +55 -0
- text2sql_eval_toolkit/evaluation/__init__.py +26 -0
- text2sql_eval_toolkit/evaluation/evaluation_tools.py +759 -0
- text2sql_eval_toolkit/evaluation/llm_as_judge.py +90 -0
- text2sql_eval_toolkit/execution/__init__.py +5 -0
- text2sql_eval_toolkit/execution/execution_tools.py +1448 -0
- text2sql_eval_toolkit/execution/replace_select_tool.py +114 -0
- text2sql_eval_toolkit/inference/__init__.py +5 -0
- text2sql_eval_toolkit/inference/agentic_pipeline.py +2335 -0
- text2sql_eval_toolkit/inference/base_pipeline.py +11 -0
- text2sql_eval_toolkit/inference/baseline_llm_pipeline.py +372 -0
- text2sql_eval_toolkit/inference/inference_tools.py +769 -0
- text2sql_eval_toolkit/logging.py +54 -0
- text2sql_eval_toolkit/profiling/profiling_tools.py +185 -0
- text2sql_eval_toolkit/utils.py +302 -0
- text2sql_eval_toolkit-1.0.0.dist-info/METADATA +382 -0
- text2sql_eval_toolkit-1.0.0.dist-info/RECORD +28 -0
- text2sql_eval_toolkit-1.0.0.dist-info/WHEEL +5 -0
- text2sql_eval_toolkit-1.0.0.dist-info/licenses/LICENSE +201 -0
- text2sql_eval_toolkit-1.0.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,719 @@
|
|
|
1
|
+
#
|
|
2
|
+
# Copyright IBM Corp. 2025 - 2026
|
|
3
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
4
|
+
#
|
|
5
|
+
|
|
6
|
+
import json
|
|
7
|
+
import pathlib
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
import re
|
|
10
|
+
import matplotlib.pyplot as plt
|
|
11
|
+
from collections import defaultdict
|
|
12
|
+
from statistics import mean
|
|
13
|
+
from tabulate import tabulate
|
|
14
|
+
from text2sql_eval_toolkit.utils import get_benchmarks_info
|
|
15
|
+
from text2sql_eval_toolkit.analysis.error_analysis import (
|
|
16
|
+
export_failed_examples_to_markdown,
|
|
17
|
+
)
|
|
18
|
+
from text2sql_eval_toolkit.logging import get_logger
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
logger = get_logger(__name__)
|
|
22
|
+
DEFAULT_METRIC = "subset_non_empty_execution_accuracy"
|
|
23
|
+
DEFAULT_PRINT_METRICS = [
|
|
24
|
+
"subset_non_empty_execution_accuracy",
|
|
25
|
+
"execution_accuracy",
|
|
26
|
+
"llm_score",
|
|
27
|
+
"total_tokens",
|
|
28
|
+
"inference_time_ms",
|
|
29
|
+
"execution_time_ms",
|
|
30
|
+
]
|
|
31
|
+
COUNT_KEYS = ["num_records", "num_evaluated", "sum_total_tokens", "sum_inference_time_ms", "sum_execution_time_ms"]
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def prettify(metric_name):
|
|
35
|
+
"""Convert snake_case to Title Case."""
|
|
36
|
+
return " ".join(word.capitalize() for word in metric_name.split("_"))
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def abbreviate(metric_name):
|
|
40
|
+
"""Return an abbreviation only if prettified name is too long."""
|
|
41
|
+
pretty = prettify(metric_name)
|
|
42
|
+
if len(pretty) > 14:
|
|
43
|
+
abbr = "".join(word[0].upper() for word in metric_name.split("_"))
|
|
44
|
+
return abbr, pretty
|
|
45
|
+
return pretty, None
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def print_summary_results_by_category(
|
|
49
|
+
records, sort_by=DEFAULT_METRIC, metrics_to_print=None
|
|
50
|
+
):
|
|
51
|
+
"""
|
|
52
|
+
Print evaluation summaries (overall and per category), plus comparison across categories for sort_by metric.
|
|
53
|
+
"""
|
|
54
|
+
|
|
55
|
+
if metrics_to_print is None:
|
|
56
|
+
metrics_to_print = DEFAULT_PRINT_METRICS
|
|
57
|
+
|
|
58
|
+
def collect_metrics(records):
|
|
59
|
+
"""Aggregate average metrics and record counts per pipeline and per category."""
|
|
60
|
+
category_metrics = defaultdict(lambda: defaultdict(lambda: defaultdict(list)))
|
|
61
|
+
all_metrics = defaultdict(lambda: defaultdict(list))
|
|
62
|
+
|
|
63
|
+
for rec in records:
|
|
64
|
+
categories = rec.get("meta", {}).get("categories", [])
|
|
65
|
+
predictions = rec.get("predictions", {})
|
|
66
|
+
|
|
67
|
+
for pipeline, pred_info in predictions.items():
|
|
68
|
+
eval_metrics = pred_info.get("evaluation", {})
|
|
69
|
+
for metric_name, metric_value in eval_metrics.items():
|
|
70
|
+
if isinstance(metric_value, (int, float)):
|
|
71
|
+
all_metrics[pipeline][metric_name].append(metric_value)
|
|
72
|
+
for cat in categories:
|
|
73
|
+
category_metrics[cat][pipeline][metric_name].append(
|
|
74
|
+
metric_value
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
all_metrics[pipeline]["num_records"].append(1)
|
|
78
|
+
for cat in categories:
|
|
79
|
+
category_metrics[cat][pipeline]["num_records"].append(1)
|
|
80
|
+
|
|
81
|
+
def avg_metrics(metrics_dict):
|
|
82
|
+
return {
|
|
83
|
+
pipeline: {
|
|
84
|
+
metric: (sum(values) if metric == "num_records" else mean(values))
|
|
85
|
+
for metric, values in metric_dict.items()
|
|
86
|
+
}
|
|
87
|
+
for pipeline, metric_dict in metrics_dict.items()
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
return avg_metrics(all_metrics), {
|
|
91
|
+
cat: avg_metrics(p) for cat, p in category_metrics.items()
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
def print_abbreviation_legend(name_map):
|
|
95
|
+
if not name_map:
|
|
96
|
+
return
|
|
97
|
+
for abbr, full in name_map.items():
|
|
98
|
+
print(f"- {abbr}: {full}")
|
|
99
|
+
print()
|
|
100
|
+
|
|
101
|
+
def print_table(metrics_dict, label=None):
|
|
102
|
+
if label:
|
|
103
|
+
print(f"\n=== {label} ===")
|
|
104
|
+
|
|
105
|
+
pipelines_sorted = sorted(
|
|
106
|
+
metrics_dict.items(),
|
|
107
|
+
key=lambda x: x[1].get(sort_by, 0) or 0,
|
|
108
|
+
reverse=True,
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
headers = ["Pipeline"]
|
|
112
|
+
abbrev_legend = {}
|
|
113
|
+
column_keys = ["num_records"] + list(metrics_to_print)
|
|
114
|
+
for key in column_keys:
|
|
115
|
+
label, full = abbreviate(key)
|
|
116
|
+
headers.append(label)
|
|
117
|
+
if full:
|
|
118
|
+
abbrev_legend[label] = full
|
|
119
|
+
|
|
120
|
+
print_abbreviation_legend(abbrev_legend)
|
|
121
|
+
|
|
122
|
+
rows = []
|
|
123
|
+
for pipeline, m in pipelines_sorted:
|
|
124
|
+
row = [pipeline]
|
|
125
|
+
row.append(str(int(m.get("num_records", 0))))
|
|
126
|
+
for metric in metrics_to_print:
|
|
127
|
+
val = m.get(metric)
|
|
128
|
+
row.append(f"{val:.3f}" if val is not None else "-")
|
|
129
|
+
rows.append(row)
|
|
130
|
+
|
|
131
|
+
print(
|
|
132
|
+
tabulate(
|
|
133
|
+
rows,
|
|
134
|
+
headers=headers,
|
|
135
|
+
tablefmt="github",
|
|
136
|
+
numalign="right",
|
|
137
|
+
stralign="left",
|
|
138
|
+
)
|
|
139
|
+
)
|
|
140
|
+
|
|
141
|
+
def print_per_pipeline_tables(sort_metric, all_avg, cat_avg):
|
|
142
|
+
"""Print one table per pipeline, showing the metric and num_records across categories."""
|
|
143
|
+
print(f"\n=== Per-Pipeline Comparison of `{sort_metric}` Across Categories ===")
|
|
144
|
+
|
|
145
|
+
pipelines = sorted(
|
|
146
|
+
{p for cat_metrics in cat_avg.values() for p in cat_metrics.keys()}
|
|
147
|
+
)
|
|
148
|
+
categories = sorted(cat_avg.keys())
|
|
149
|
+
|
|
150
|
+
for pipeline in pipelines:
|
|
151
|
+
print(f"\n--- Pipeline: `{pipeline}` ---")
|
|
152
|
+
metrics = all_avg.get(pipeline, {})
|
|
153
|
+
score = metrics.get(sort_metric)
|
|
154
|
+
count = int(metrics.get("num_records", 0))
|
|
155
|
+
rows = [
|
|
156
|
+
"All Categories",
|
|
157
|
+
str(count) if count else "-",
|
|
158
|
+
f"{score:.3f}" if score is not None else "-",
|
|
159
|
+
]
|
|
160
|
+
for cat in categories:
|
|
161
|
+
metrics = cat_avg[cat].get(pipeline, {})
|
|
162
|
+
score = metrics.get(sort_metric)
|
|
163
|
+
count = int(metrics.get("num_records", 0))
|
|
164
|
+
rows.append(
|
|
165
|
+
[
|
|
166
|
+
cat,
|
|
167
|
+
str(count) if count else "-",
|
|
168
|
+
f"{score:.3f}" if score is not None else "-",
|
|
169
|
+
]
|
|
170
|
+
)
|
|
171
|
+
print(
|
|
172
|
+
tabulate(
|
|
173
|
+
rows,
|
|
174
|
+
headers=["Category", "# Records", prettify(sort_metric)],
|
|
175
|
+
tablefmt="github",
|
|
176
|
+
numalign="right",
|
|
177
|
+
stralign="left",
|
|
178
|
+
)
|
|
179
|
+
)
|
|
180
|
+
|
|
181
|
+
# Aggregate
|
|
182
|
+
all_avg, cat_avg = collect_metrics(records)
|
|
183
|
+
|
|
184
|
+
# Print tables
|
|
185
|
+
print_table(all_avg, label="Overall (All Categories Combined)")
|
|
186
|
+
for cat, metrics in sorted(cat_avg.items()):
|
|
187
|
+
print_table(metrics, label=f"Category: {cat}")
|
|
188
|
+
|
|
189
|
+
# Per-pipeline compact comparison
|
|
190
|
+
print_per_pipeline_tables(sort_by, all_avg, cat_avg)
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
def get_benchmark_statistics(benchmark_id: str, benchmarks_info: dict, pipeline_metrics: dict) -> dict:
|
|
194
|
+
"""
|
|
195
|
+
Extract statistics for a benchmark including:
|
|
196
|
+
- Description from benchmarks.json
|
|
197
|
+
- Database type (sqlite/mysql)
|
|
198
|
+
- Number of records in benchmark data file
|
|
199
|
+
- Number of pipelines with predictions
|
|
200
|
+
|
|
201
|
+
Args:
|
|
202
|
+
benchmark_id: Benchmark identifier
|
|
203
|
+
benchmarks_info: Full benchmark metadata from get_benchmarks_info()
|
|
204
|
+
pipeline_metrics: Evaluation metrics for all pipelines
|
|
205
|
+
|
|
206
|
+
Returns:
|
|
207
|
+
dict with keys: description, db_type, num_records, num_pipelines
|
|
208
|
+
"""
|
|
209
|
+
stats = {
|
|
210
|
+
"description": "N/A",
|
|
211
|
+
"db_type": "N/A",
|
|
212
|
+
"num_records": 0,
|
|
213
|
+
"num_pipelines": 0,
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
# Get benchmark metadata
|
|
217
|
+
if benchmark_id in benchmarks_info:
|
|
218
|
+
benchmark_info = benchmarks_info[benchmark_id]
|
|
219
|
+
stats["description"] = benchmark_info.get("description", "N/A")
|
|
220
|
+
|
|
221
|
+
# Extract db_type from db_engine
|
|
222
|
+
db_engine = benchmark_info.get("db_engine", {})
|
|
223
|
+
stats["db_type"] = db_engine.get("db_type", "N/A")
|
|
224
|
+
|
|
225
|
+
# Count records from benchmark data file
|
|
226
|
+
try:
|
|
227
|
+
benchmark_data_path = benchmark_info.get("benchmark_json_path")
|
|
228
|
+
if benchmark_data_path and Path(benchmark_data_path).exists():
|
|
229
|
+
with open(benchmark_data_path, "r") as f:
|
|
230
|
+
data = json.load(f)
|
|
231
|
+
stats["num_records"] = len(data) if isinstance(data, list) else 0
|
|
232
|
+
except Exception as e:
|
|
233
|
+
logger.warning(f"Could not count records for {benchmark_id}: {e}")
|
|
234
|
+
|
|
235
|
+
# Count pipelines (exclude llm_judge_config if present)
|
|
236
|
+
if pipeline_metrics:
|
|
237
|
+
stats["num_pipelines"] = len([k for k in pipeline_metrics.keys() if k != "llm_judge_config"])
|
|
238
|
+
|
|
239
|
+
return stats
|
|
240
|
+
|
|
241
|
+
|
|
242
|
+
def generate_toc_section(results: dict, benchmarks_info: dict, sort_by: str = DEFAULT_METRIC) -> str:
|
|
243
|
+
"""
|
|
244
|
+
Generate table of contents with benchmark summaries.
|
|
245
|
+
|
|
246
|
+
Args:
|
|
247
|
+
results: Dict mapping benchmark_id to (eval_path, eval_relpath, metrics)
|
|
248
|
+
benchmarks_info: Full benchmark metadata
|
|
249
|
+
sort_by: Metric used for sorting (for anchor link generation)
|
|
250
|
+
|
|
251
|
+
Returns:
|
|
252
|
+
Markdown string for TOC section
|
|
253
|
+
"""
|
|
254
|
+
toc_lines = []
|
|
255
|
+
|
|
256
|
+
# Table header
|
|
257
|
+
toc_lines.append("| Benchmark | Description | DB Type | Records | Pipelines |")
|
|
258
|
+
toc_lines.append("|-----------|-------------|---------|---------|-----------|")
|
|
259
|
+
|
|
260
|
+
# Generate rows for each benchmark
|
|
261
|
+
for benchmark_id, (eval_results_path, eval_results_relpath, pipeline_metrics) in results.items():
|
|
262
|
+
stats = get_benchmark_statistics(benchmark_id, benchmarks_info, pipeline_metrics)
|
|
263
|
+
|
|
264
|
+
# Create anchor link (matches GitHub's automatic heading anchor generation)
|
|
265
|
+
# GitHub converts: lowercase, removes special chars, keeps underscores, replaces spaces with hyphens
|
|
266
|
+
# Format: #benchmark-{benchmark_id}
|
|
267
|
+
anchor = f"#benchmark-{benchmark_id}".lower()
|
|
268
|
+
|
|
269
|
+
# Create table row
|
|
270
|
+
row = [
|
|
271
|
+
f"[{benchmark_id}]({anchor})",
|
|
272
|
+
stats["description"],
|
|
273
|
+
stats["db_type"],
|
|
274
|
+
str(stats["num_records"]),
|
|
275
|
+
str(stats["num_pipelines"]),
|
|
276
|
+
]
|
|
277
|
+
toc_lines.append("| " + " | ".join(row) + " |")
|
|
278
|
+
|
|
279
|
+
toc_lines.append("\n---\n")
|
|
280
|
+
|
|
281
|
+
return "\n".join(toc_lines)
|
|
282
|
+
|
|
283
|
+
|
|
284
|
+
def collect_results(output_folder: Path, is_test: bool = False):
|
|
285
|
+
benchmarks_info = get_benchmarks_info(is_test=is_test)
|
|
286
|
+
results = {}
|
|
287
|
+
for benchmark_id, benchmark_info in benchmarks_info.items():
|
|
288
|
+
# Skip test benchmarks in production mode, but include all in test mode
|
|
289
|
+
if not is_test and "test" in benchmark_id:
|
|
290
|
+
continue
|
|
291
|
+
eval_summary_path = Path(benchmark_info["eval_summary_path"]).resolve()
|
|
292
|
+
eval_results_path = Path(benchmark_info["eval_results_path"]).resolve()
|
|
293
|
+
eval_results_relpath = eval_results_path.relative_to(output_folder.resolve())
|
|
294
|
+
if eval_results_path.exists():
|
|
295
|
+
with open(eval_summary_path, "r") as f:
|
|
296
|
+
data = json.load(f)
|
|
297
|
+
data.pop("llm_judge_config", None)
|
|
298
|
+
results[benchmark_id] = (eval_results_path, eval_results_relpath, data)
|
|
299
|
+
return results, benchmarks_info
|
|
300
|
+
|
|
301
|
+
|
|
302
|
+
def generate_bar_chart(output_file_path, pipeline_metrics, title, chart_filename):
|
|
303
|
+
pipelines = list(pipeline_metrics.keys())
|
|
304
|
+
subset_scores = [
|
|
305
|
+
pipeline_metrics[m]
|
|
306
|
+
.get("subset_non_empty_execution_accuracy", {})
|
|
307
|
+
.get("average", 0)
|
|
308
|
+
for m in pipelines
|
|
309
|
+
]
|
|
310
|
+
non_empty_scores = [
|
|
311
|
+
pipeline_metrics[m].get("non_empty_execution_accuracy", {}).get("average", 0)
|
|
312
|
+
for m in pipelines
|
|
313
|
+
]
|
|
314
|
+
|
|
315
|
+
has_llm_score = any("llm_score" in pipeline_metrics[p] for p in pipelines)
|
|
316
|
+
if has_llm_score:
|
|
317
|
+
llm_scores = [
|
|
318
|
+
pipeline_metrics[p].get("llm_score", {}).get("average", 0)
|
|
319
|
+
for p in pipelines
|
|
320
|
+
]
|
|
321
|
+
|
|
322
|
+
x = range(len(pipelines))
|
|
323
|
+
width = 0.25 if has_llm_score else 0.35
|
|
324
|
+
|
|
325
|
+
fig, ax = plt.subplots(figsize=(12, 6))
|
|
326
|
+
ax.bar(
|
|
327
|
+
[i - width for i in x],
|
|
328
|
+
subset_scores,
|
|
329
|
+
width,
|
|
330
|
+
label="Subset Non-Empty Exec Acc",
|
|
331
|
+
)
|
|
332
|
+
ax.bar(x, non_empty_scores, width, label="Non-Empty Exec Acc")
|
|
333
|
+
|
|
334
|
+
if has_llm_score:
|
|
335
|
+
ax.bar([i + width for i in x], llm_scores, width, label="LLM Score")
|
|
336
|
+
|
|
337
|
+
ax.set_ylabel("Accuracy")
|
|
338
|
+
ax.set_title(title)
|
|
339
|
+
ax.set_xticks(x)
|
|
340
|
+
ax.set_xticklabels(pipelines, rotation=45, ha="right")
|
|
341
|
+
ax.legend()
|
|
342
|
+
ax.set_ylim(0, 1)
|
|
343
|
+
|
|
344
|
+
charts_dir = Path(output_file_path).parent / "charts"
|
|
345
|
+
pathlib.Path(charts_dir).mkdir(exist_ok=True)
|
|
346
|
+
chart_path = charts_dir / chart_filename
|
|
347
|
+
plt.tight_layout()
|
|
348
|
+
plt.savefig(chart_path)
|
|
349
|
+
plt.close()
|
|
350
|
+
return chart_path
|
|
351
|
+
|
|
352
|
+
|
|
353
|
+
def generate_markdown_table(
|
|
354
|
+
output_file_path,
|
|
355
|
+
benchmark,
|
|
356
|
+
eval_results_path,
|
|
357
|
+
eval_results_relpath,
|
|
358
|
+
pipeline_metrics,
|
|
359
|
+
sort_by,
|
|
360
|
+
):
|
|
361
|
+
rows = []
|
|
362
|
+
header = [
|
|
363
|
+
"Rank",
|
|
364
|
+
"Model / Pipeline",
|
|
365
|
+
"Execution Acc",
|
|
366
|
+
"Non-Empty Exec Acc",
|
|
367
|
+
"Subset Non-Empty Exec Acc",
|
|
368
|
+
"BIRD Exec Acc",
|
|
369
|
+
"LLM Judge Score",
|
|
370
|
+
"Parsable SQL",
|
|
371
|
+
"SQL Syntactic Match",
|
|
372
|
+
"Eval Err",
|
|
373
|
+
"DF Err",
|
|
374
|
+
"Avg Tokens/Q",
|
|
375
|
+
"Avg Inference (ms)",
|
|
376
|
+
"Avg Execution (ms)",
|
|
377
|
+
"Total Tokens",
|
|
378
|
+
"Total Inference (ms)",
|
|
379
|
+
"Total Execution (ms)",
|
|
380
|
+
"#Records",
|
|
381
|
+
"#Predictions",
|
|
382
|
+
"#Evaluated",
|
|
383
|
+
"#Correct Non-Empty Exec Acc",
|
|
384
|
+
"#Correct Subset Non-Empty Exec Acc",
|
|
385
|
+
"#Correct As Per LLM Judge",
|
|
386
|
+
]
|
|
387
|
+
metric_keys = {
|
|
388
|
+
"Execution Acc": "execution_accuracy",
|
|
389
|
+
"Non-Empty Exec Acc": "non_empty_execution_accuracy",
|
|
390
|
+
"Subset Non-Empty Exec Acc": "subset_non_empty_execution_accuracy",
|
|
391
|
+
"BIRD Exec Acc": "bird_execution_accuracy",
|
|
392
|
+
"LLM Judge Score": "llm_score",
|
|
393
|
+
"Parsable SQL": "is_sqlparse_parsable",
|
|
394
|
+
"SQL Syntactic Match": "sql_syntactic_equivalence",
|
|
395
|
+
"Eval Err": "eval_error",
|
|
396
|
+
"DF Err": "df_error",
|
|
397
|
+
"Avg Tokens/Q": "total_tokens",
|
|
398
|
+
"Avg Inference (ms)": "inference_time_ms",
|
|
399
|
+
"Avg Execution (ms)": "execution_time_ms",
|
|
400
|
+
}
|
|
401
|
+
count_keys = {
|
|
402
|
+
"Total Tokens": "sum_total_tokens",
|
|
403
|
+
"Total Inference (ms)": "sum_inference_time_ms",
|
|
404
|
+
"Total Execution (ms)": "sum_execution_time_ms",
|
|
405
|
+
"#Records": "num_records",
|
|
406
|
+
"#Predictions": "num_predictions",
|
|
407
|
+
"#Evaluated": "num_evaluated",
|
|
408
|
+
"#Correct Non-Empty Exec Acc": "num_correct_non_empty_execution_accuracy",
|
|
409
|
+
"#Correct Subset Non-Empty Exec Acc": "num_correct_subset_non_empty_execution_accuracy",
|
|
410
|
+
"#Correct As Per LLM Judge": "num_correct_llm",
|
|
411
|
+
}
|
|
412
|
+
if "llm_judge_config" in pipeline_metrics:
|
|
413
|
+
pipeline_metrics.pop("llm_judge_config")
|
|
414
|
+
sorted_pipelines = sorted(
|
|
415
|
+
pipeline_metrics.items(),
|
|
416
|
+
key=lambda x: x[1].get(sort_by, {}).get("average", 0.0),
|
|
417
|
+
reverse=True,
|
|
418
|
+
)
|
|
419
|
+
|
|
420
|
+
for rank, (pipeline, metrics) in enumerate(sorted_pipelines, start=1):
|
|
421
|
+
row = [str(rank), pipeline]
|
|
422
|
+
for label, key in metric_keys.items():
|
|
423
|
+
score = metrics.get(key, {}).get("average", None)
|
|
424
|
+
row.append(f"{score:.2f}" if score is not None else "N/A")
|
|
425
|
+
for label, key in count_keys.items():
|
|
426
|
+
val = metrics.get(key, None)
|
|
427
|
+
row.append(str(val) if val is not None else "N/A")
|
|
428
|
+
rows.append(row)
|
|
429
|
+
|
|
430
|
+
table_md = f"### Benchmark: {benchmark}\n\n"
|
|
431
|
+
table_md += f"_Results sorted by default on `{sort_by}` (higher is better)_\n\n"
|
|
432
|
+
with eval_results_path.open("r") as eval_results_file:
|
|
433
|
+
records = json.load(eval_results_file)
|
|
434
|
+
summary_md_path = eval_results_path.with_name(
|
|
435
|
+
eval_results_path.stem + "_summary.md"
|
|
436
|
+
)
|
|
437
|
+
summary_md_relpath = summary_md_path.relative_to(
|
|
438
|
+
Path(output_file_path).parent.resolve()
|
|
439
|
+
)
|
|
440
|
+
export_summary_results_by_category_to_markdown(records, summary_md_path)
|
|
441
|
+
errors_path = eval_results_path.with_name(eval_results_path.stem + "_errors.md")
|
|
442
|
+
errors_relpath = errors_path.relative_to(
|
|
443
|
+
Path(output_file_path).parent.resolve()
|
|
444
|
+
)
|
|
445
|
+
export_failed_examples_to_markdown(records, errors_path)
|
|
446
|
+
# table_md += f"📄 [View Evaluation Results JSON]({eval_results_relpath})\n\n"
|
|
447
|
+
# table_md += f"📄 [View In-Depth Summary Results Across Categories]({summary_md_relpath}) - [View Examples of Errors for Error Analysis]({errors_relpath}) - [View Full Results JSON]({eval_results_relpath})\n\n"
|
|
448
|
+
|
|
449
|
+
gitignore_path = Path(".gitignore")
|
|
450
|
+
|
|
451
|
+
# Default: include full results
|
|
452
|
+
show_full_results = True
|
|
453
|
+
|
|
454
|
+
if gitignore_path.exists():
|
|
455
|
+
ignored_files = {
|
|
456
|
+
Path(line.strip()).name
|
|
457
|
+
for line in gitignore_path.read_text().splitlines()
|
|
458
|
+
if line.strip() and not line.strip().startswith("#")
|
|
459
|
+
}
|
|
460
|
+
if Path(eval_results_relpath).name in ignored_files:
|
|
461
|
+
show_full_results = False
|
|
462
|
+
|
|
463
|
+
table_md += (
|
|
464
|
+
f"📄 [View In-Depth Summary Results Across Categories]({summary_md_relpath})"
|
|
465
|
+
f" - [View Examples of Errors for Error Analysis]({errors_relpath})"
|
|
466
|
+
)
|
|
467
|
+
|
|
468
|
+
if show_full_results:
|
|
469
|
+
table_md += f" - [View Full Results JSON]({eval_results_relpath})"
|
|
470
|
+
|
|
471
|
+
table_md += "\n\n"
|
|
472
|
+
|
|
473
|
+
table_md += "| " + " | ".join(header) + " |\n"
|
|
474
|
+
table_md += "| " + " | ".join(["---"] * len(header)) + " |\n"
|
|
475
|
+
for row in rows:
|
|
476
|
+
table_md += "| " + " | ".join(row) + " |\n"
|
|
477
|
+
table_md += "\n"
|
|
478
|
+
|
|
479
|
+
chart_filename = summary_md_relpath.stem + ".png"
|
|
480
|
+
chart_path = generate_bar_chart(
|
|
481
|
+
output_file_path,
|
|
482
|
+
pipeline_metrics,
|
|
483
|
+
f"Overall Accuracy - {benchmark}",
|
|
484
|
+
chart_filename,
|
|
485
|
+
)
|
|
486
|
+
chart_rel_path = Path(chart_path).relative_to(Path(output_file_path).parent)
|
|
487
|
+
table_md += f"\n\n"
|
|
488
|
+
# table_md += f'<img src="{chart_rel_path}" alt="Chart for {benchmark}" width="50%"/>\n\n'
|
|
489
|
+
|
|
490
|
+
return table_md
|
|
491
|
+
|
|
492
|
+
|
|
493
|
+
def create_dashboard(output_file_path: str, results, benchmarks_info, sort_by=DEFAULT_METRIC):
|
|
494
|
+
markdown = "# Text-to-SQL Evaluation Results Dashboard\n\n"
|
|
495
|
+
|
|
496
|
+
# Add table of contents
|
|
497
|
+
markdown += generate_toc_section(results, benchmarks_info, sort_by)
|
|
498
|
+
|
|
499
|
+
for benchmark, (
|
|
500
|
+
eval_results_path,
|
|
501
|
+
eval_results_relpath,
|
|
502
|
+
pipeline_metrics,
|
|
503
|
+
) in results.items():
|
|
504
|
+
markdown += generate_markdown_table(
|
|
505
|
+
output_file_path,
|
|
506
|
+
benchmark,
|
|
507
|
+
eval_results_path,
|
|
508
|
+
eval_results_relpath,
|
|
509
|
+
pipeline_metrics,
|
|
510
|
+
sort_by,
|
|
511
|
+
)
|
|
512
|
+
|
|
513
|
+
return markdown
|
|
514
|
+
|
|
515
|
+
|
|
516
|
+
def export_summary_results_by_category_to_markdown(
|
|
517
|
+
records, category_summary_path, sort_by=DEFAULT_METRIC
|
|
518
|
+
):
|
|
519
|
+
"""
|
|
520
|
+
Creates a markdown file summarizing evaluation results overall and by category.
|
|
521
|
+
"""
|
|
522
|
+
|
|
523
|
+
def collect_metrics(records):
|
|
524
|
+
"""Aggregate average metrics and record counts per pipeline and per category."""
|
|
525
|
+
|
|
526
|
+
category_metrics = defaultdict(lambda: defaultdict(lambda: defaultdict(list)))
|
|
527
|
+
all_metrics = defaultdict(lambda: defaultdict(list))
|
|
528
|
+
|
|
529
|
+
for rec in records:
|
|
530
|
+
categories = rec.get("meta", {}).get("categories", [])
|
|
531
|
+
predictions = rec.get("predictions", {})
|
|
532
|
+
|
|
533
|
+
for pipeline, pred_info in predictions.items():
|
|
534
|
+
eval_metrics = pred_info.get("evaluation", {})
|
|
535
|
+
for metric_name, metric_value in eval_metrics.items():
|
|
536
|
+
if isinstance(metric_value, (int, float)):
|
|
537
|
+
all_metrics[pipeline][metric_name].append(metric_value)
|
|
538
|
+
for cat in categories:
|
|
539
|
+
category_metrics[cat][pipeline][metric_name].append(
|
|
540
|
+
metric_value
|
|
541
|
+
)
|
|
542
|
+
|
|
543
|
+
# Count one record per example
|
|
544
|
+
all_metrics[pipeline]["num_records"].append(1)
|
|
545
|
+
for cat in categories:
|
|
546
|
+
category_metrics[cat][pipeline]["num_records"].append(1)
|
|
547
|
+
|
|
548
|
+
def avg_metrics(metrics_dict):
|
|
549
|
+
"""Return dict[pipeline][metric_name] = average OR count"""
|
|
550
|
+
return {
|
|
551
|
+
pipeline: {
|
|
552
|
+
metric: (sum(values) if metric == "num_records" else mean(values))
|
|
553
|
+
for metric, values in metric_dict.items()
|
|
554
|
+
}
|
|
555
|
+
for pipeline, metric_dict in metrics_dict.items()
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
return avg_metrics(all_metrics), {
|
|
559
|
+
cat: avg_metrics(p) for cat, p in category_metrics.items()
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
def generate_table_md(pipeline_metrics, sort_by):
|
|
563
|
+
pipelines_sorted = sorted(
|
|
564
|
+
pipeline_metrics.items(),
|
|
565
|
+
key=lambda x: x[1].get(sort_by, 0) or 0,
|
|
566
|
+
reverse=True,
|
|
567
|
+
)
|
|
568
|
+
headers = [
|
|
569
|
+
"Rank",
|
|
570
|
+
"Pipeline",
|
|
571
|
+
"Records #",
|
|
572
|
+
"Predictions #",
|
|
573
|
+
"Exec Acc",
|
|
574
|
+
"Non-Empty Exec Acc",
|
|
575
|
+
"Subset Non-Empty Exec Acc",
|
|
576
|
+
"BIRD Exec Acc",
|
|
577
|
+
"Parsable SQL",
|
|
578
|
+
"Syntactic Equivalence Score",
|
|
579
|
+
"LLM Score",
|
|
580
|
+
]
|
|
581
|
+
rows = []
|
|
582
|
+
for rank, (pipeline, m) in enumerate(pipelines_sorted, start=1):
|
|
583
|
+
rows.append(
|
|
584
|
+
[
|
|
585
|
+
rank,
|
|
586
|
+
pipeline,
|
|
587
|
+
f"{(m.get('num_records') or 0)}",
|
|
588
|
+
f"{(m.get('num_predictions') or 0)}",
|
|
589
|
+
f"{(m.get('execution_accuracy') or 0):.2f}",
|
|
590
|
+
f"{(m.get('non_empty_execution_accuracy') or 0):.2f}",
|
|
591
|
+
f"{(m.get('subset_non_empty_execution_accuracy') or 0):.2f}",
|
|
592
|
+
f"{(m.get('bird_execution_accuracy') or 0):.2f}",
|
|
593
|
+
f"{(m.get('is_sqlparse_parsable') or 0):.2f}",
|
|
594
|
+
f"{(m.get('sql_syntactic_equivalence') or 0):.2f}",
|
|
595
|
+
f"{(m.get('llm_score') or 0):.2f}",
|
|
596
|
+
]
|
|
597
|
+
)
|
|
598
|
+
md = "| " + " | ".join(headers) + " |\n"
|
|
599
|
+
md += "| " + " | ".join(["---"] * len(headers)) + " |\n"
|
|
600
|
+
for row in rows:
|
|
601
|
+
md += "| " + " | ".join(map(str, row)) + " |\n"
|
|
602
|
+
return md
|
|
603
|
+
|
|
604
|
+
def make_safe_filename(s: str, replacement="_"):
|
|
605
|
+
return re.sub(r"[^A-Za-z0-9_.-]", replacement, s)
|
|
606
|
+
|
|
607
|
+
def convert_avg_to_pipeline_metrics(avg_metrics):
|
|
608
|
+
"""
|
|
609
|
+
Convert from:
|
|
610
|
+
{pipeline: {metric_name: avg_value}}
|
|
611
|
+
To:
|
|
612
|
+
{pipeline: {metric_name: {"average": avg_value}}}
|
|
613
|
+
so that generate_bar_chart() works without changes.
|
|
614
|
+
"""
|
|
615
|
+
out = {}
|
|
616
|
+
for pipeline, metrics in avg_metrics.items():
|
|
617
|
+
out[pipeline] = {}
|
|
618
|
+
for mname, avg_val in metrics.items():
|
|
619
|
+
out[pipeline][mname] = {
|
|
620
|
+
"average": avg_val if avg_val is not None else 0
|
|
621
|
+
}
|
|
622
|
+
return out
|
|
623
|
+
|
|
624
|
+
# --- Aggregate ---
|
|
625
|
+
all_avg, cat_avg = collect_metrics(records)
|
|
626
|
+
|
|
627
|
+
# --- Build markdown ---
|
|
628
|
+
md_lines = ["# Summary Results\n"]
|
|
629
|
+
|
|
630
|
+
# Overall
|
|
631
|
+
md_lines.append("## Overall Average Accuracy Results\n")
|
|
632
|
+
overall_pipeline_metrics = convert_avg_to_pipeline_metrics(all_avg)
|
|
633
|
+
|
|
634
|
+
eval_summary_filename = Path(category_summary_path).stem
|
|
635
|
+
chart_filename = eval_summary_filename + ".png"
|
|
636
|
+
chart_path = generate_bar_chart(
|
|
637
|
+
category_summary_path,
|
|
638
|
+
overall_pipeline_metrics,
|
|
639
|
+
f"Overall Accuracy - {eval_summary_filename}",
|
|
640
|
+
chart_filename,
|
|
641
|
+
)
|
|
642
|
+
md_lines.append(generate_table_md(all_avg, sort_by))
|
|
643
|
+
md_lines.append(
|
|
644
|
+
f"\n.relative_to(Path(category_summary_path).parent)})\n"
|
|
645
|
+
)
|
|
646
|
+
|
|
647
|
+
# Per category
|
|
648
|
+
for cat, metrics in sorted(cat_avg.items()):
|
|
649
|
+
md_lines.append(f"\n## Category: `{cat}`\n")
|
|
650
|
+
cat_pipeline_metrics = convert_avg_to_pipeline_metrics(metrics)
|
|
651
|
+
category_chart_filename = (
|
|
652
|
+
eval_summary_filename + "-" + make_safe_filename(cat) + ".png"
|
|
653
|
+
)
|
|
654
|
+
chart_path = generate_bar_chart(
|
|
655
|
+
category_summary_path,
|
|
656
|
+
cat_pipeline_metrics,
|
|
657
|
+
f"Accuracy for Category: {cat} - {eval_summary_filename}",
|
|
658
|
+
category_chart_filename,
|
|
659
|
+
)
|
|
660
|
+
md_lines.append(generate_table_md(metrics, sort_by))
|
|
661
|
+
md_lines.append(
|
|
662
|
+
f"\n.relative_to(Path(category_summary_path).parent)})\n"
|
|
663
|
+
)
|
|
664
|
+
|
|
665
|
+
# Per-Pipeline Category Comparison
|
|
666
|
+
md_lines.append("\n# Per-Pipeline Comparison Across Categories\n")
|
|
667
|
+
|
|
668
|
+
SELECTED_METRICS = [
|
|
669
|
+
"execution_accuracy",
|
|
670
|
+
"non_empty_execution_accuracy",
|
|
671
|
+
"subset_non_empty_execution_accuracy",
|
|
672
|
+
"bird_execution_accuracy",
|
|
673
|
+
"llm_score",
|
|
674
|
+
]
|
|
675
|
+
|
|
676
|
+
def metric_label(name):
|
|
677
|
+
return {
|
|
678
|
+
"execution_accuracy": "Exec Acc",
|
|
679
|
+
"non_empty_execution_accuracy": "Non-Empty Exec Acc",
|
|
680
|
+
"subset_non_empty_execution_accuracy": "Subset Non-Empty Exec Acc",
|
|
681
|
+
"bird_execution_accuracy": "BIRD Exec Acc",
|
|
682
|
+
"llm_score": "LLM Score",
|
|
683
|
+
}.get(name, name)
|
|
684
|
+
|
|
685
|
+
all_pipelines = sorted(
|
|
686
|
+
{p for cat_data in cat_avg.values() for p in cat_data.keys()}
|
|
687
|
+
)
|
|
688
|
+
sorted_categories = sorted(cat_avg.keys())
|
|
689
|
+
|
|
690
|
+
for pipeline in all_pipelines:
|
|
691
|
+
md_lines.append(f"\n### Pipeline: `{pipeline}`")
|
|
692
|
+
headers = ["Category", "# Records", "# Predictions"] + [
|
|
693
|
+
metric_label(m) for m in SELECTED_METRICS
|
|
694
|
+
]
|
|
695
|
+
md_lines.append("| " + " | ".join(headers) + " |")
|
|
696
|
+
md_lines.append("|" + "|".join(["---"] * len(headers)) + "|")
|
|
697
|
+
|
|
698
|
+
p_metrics = all_avg.get(pipeline, {})
|
|
699
|
+
count = int(p_metrics.get("num_records", 0))
|
|
700
|
+
pred_count = int(p_metrics.get("num_predictions", 0))
|
|
701
|
+
row = ["All Categories", str(count), str(pred_count)]
|
|
702
|
+
for m in SELECTED_METRICS:
|
|
703
|
+
val = p_metrics.get(m)
|
|
704
|
+
row.append(f"{val:.3f}" if val is not None else "-")
|
|
705
|
+
md_lines.append("| " + " | ".join(row) + " |")
|
|
706
|
+
for cat in sorted_categories:
|
|
707
|
+
p_metrics = cat_avg[cat].get(pipeline, {})
|
|
708
|
+
count = int(p_metrics.get("num_records", 0))
|
|
709
|
+
pred_count = int(p_metrics.get("num_predictions", 0))
|
|
710
|
+
row = [cat, str(count), str(pred_count)]
|
|
711
|
+
for m in SELECTED_METRICS:
|
|
712
|
+
val = p_metrics.get(m)
|
|
713
|
+
row.append(f"{val:.3f}" if val is not None else "-")
|
|
714
|
+
md_lines.append("| " + " | ".join(row) + " |")
|
|
715
|
+
|
|
716
|
+
with open(category_summary_path, "w", encoding="utf-8") as f:
|
|
717
|
+
f.write("\n".join(md_lines))
|
|
718
|
+
|
|
719
|
+
logger.info(f"✅ Results summary markdown saved to {category_summary_path}")
|