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,107 @@
|
|
|
1
|
+
#
|
|
2
|
+
# Copyright IBM Corp. 2025 - 2026
|
|
3
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
4
|
+
#
|
|
5
|
+
"""
|
|
6
|
+
Public API for the text2sql-eval-toolkit library.
|
|
7
|
+
|
|
8
|
+
This package exposes multiple levels of functionality:
|
|
9
|
+
|
|
10
|
+
- Low-level, record-based evaluation (`evaluate_prediction`)
|
|
11
|
+
- File-based evaluation over prediction JSON files (`evaluate_predictions`)
|
|
12
|
+
- Benchmark-based orchestration that discovers files from benchmark metadata (`run_evaluation`, `run_execution`)
|
|
13
|
+
- Inference pipelines for generating SQL (`LLMSQLGenerationPipeline`, `AgenticSQLGenerationPipeline`)
|
|
14
|
+
- Utilities for discovering and inspecting available benchmarks (`get_available_benchmarks`, etc.)
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
from .evaluation.evaluation_tools import (
|
|
18
|
+
evaluate_prediction,
|
|
19
|
+
async_evaluate_predictions,
|
|
20
|
+
evaluate_predictions,
|
|
21
|
+
compute_summary,
|
|
22
|
+
summary_to_df_csv,
|
|
23
|
+
print_summary,
|
|
24
|
+
run_evaluation,
|
|
25
|
+
)
|
|
26
|
+
from .evaluation.llm_as_judge import (
|
|
27
|
+
load_llm_judge_config,
|
|
28
|
+
evaluate_sql_prediction_with_llm,
|
|
29
|
+
)
|
|
30
|
+
from .evaluation import (
|
|
31
|
+
compare_result_dfs,
|
|
32
|
+
compare_dfs_bird_eval_logic,
|
|
33
|
+
is_sqlglot_parsable,
|
|
34
|
+
is_sqlparse_parsable,
|
|
35
|
+
sqlglot_parsed_queries_equivalent,
|
|
36
|
+
sqlglot_optimized_equivalence,
|
|
37
|
+
sqlparse_queries_equivalent,
|
|
38
|
+
sql_exact_match,
|
|
39
|
+
)
|
|
40
|
+
from .execution.execution_tools import run_execution
|
|
41
|
+
from .inference.baseline_llm_pipeline import (
|
|
42
|
+
LLMSQLGenerationPipelineSimple,
|
|
43
|
+
LLMSQLGenerationPipeline,
|
|
44
|
+
)
|
|
45
|
+
from .inference.agentic_pipeline import AgenticSQLGenerationPipeline
|
|
46
|
+
from .utils import (
|
|
47
|
+
get_available_benchmarks,
|
|
48
|
+
get_benchmarks_info,
|
|
49
|
+
get_benchmark_info,
|
|
50
|
+
run_with_timeout,
|
|
51
|
+
run_with_timeout_async,
|
|
52
|
+
parse_dataframe,
|
|
53
|
+
truncate_dataframe,
|
|
54
|
+
get_question_id,
|
|
55
|
+
get_utterance,
|
|
56
|
+
get_gt_sqls,
|
|
57
|
+
get_question,
|
|
58
|
+
get_default_eval_filename,
|
|
59
|
+
add_summary_json_suffix,
|
|
60
|
+
add_summary_csv_suffix,
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
__all__ = [
|
|
64
|
+
# Evaluation APIs
|
|
65
|
+
"evaluate_prediction",
|
|
66
|
+
"async_evaluate_predictions",
|
|
67
|
+
"evaluate_predictions",
|
|
68
|
+
"compute_summary",
|
|
69
|
+
"summary_to_df_csv",
|
|
70
|
+
"print_summary",
|
|
71
|
+
"run_evaluation",
|
|
72
|
+
# LLM-as-judge helpers
|
|
73
|
+
"load_llm_judge_config",
|
|
74
|
+
"evaluate_sql_prediction_with_llm",
|
|
75
|
+
# Low-level SQL equivalence / parsing helpers (from unitxt.text2sql_utils)
|
|
76
|
+
"compare_result_dfs",
|
|
77
|
+
"compare_dfs_bird_eval_logic",
|
|
78
|
+
"is_sqlglot_parsable",
|
|
79
|
+
"is_sqlparse_parsable",
|
|
80
|
+
"sqlglot_parsed_queries_equivalent",
|
|
81
|
+
"sqlglot_optimized_equivalence",
|
|
82
|
+
"sqlparse_queries_equivalent",
|
|
83
|
+
"sql_exact_match",
|
|
84
|
+
# Execution
|
|
85
|
+
"run_execution",
|
|
86
|
+
# Inference pipelines
|
|
87
|
+
"LLMSQLGenerationPipelineSimple",
|
|
88
|
+
"LLMSQLGenerationPipeline",
|
|
89
|
+
"AgenticSQLGenerationPipeline",
|
|
90
|
+
# Benchmark utilities
|
|
91
|
+
"get_available_benchmarks",
|
|
92
|
+
"get_benchmarks_info",
|
|
93
|
+
"get_benchmark_info",
|
|
94
|
+
# Misc utilities (advanced usage)
|
|
95
|
+
"run_with_timeout",
|
|
96
|
+
"run_with_timeout_async",
|
|
97
|
+
"parse_dataframe",
|
|
98
|
+
"truncate_dataframe",
|
|
99
|
+
"get_question_id",
|
|
100
|
+
"get_utterance",
|
|
101
|
+
"get_gt_sqls",
|
|
102
|
+
"get_question",
|
|
103
|
+
"get_default_eval_filename",
|
|
104
|
+
"add_summary_json_suffix",
|
|
105
|
+
"add_summary_csv_suffix",
|
|
106
|
+
]
|
|
107
|
+
|
|
@@ -0,0 +1,335 @@
|
|
|
1
|
+
#
|
|
2
|
+
# Copyright IBM Corp. 2025 - 2026
|
|
3
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
4
|
+
#
|
|
5
|
+
|
|
6
|
+
import json
|
|
7
|
+
import pandas as pd
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from text2sql_eval_toolkit.utils import parse_dataframe
|
|
10
|
+
from text2sql_eval_toolkit.logging import get_logger
|
|
11
|
+
|
|
12
|
+
logger = get_logger(__name__)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def get_pipeline_ids(records):
|
|
16
|
+
if len(records) < 1 or "predictions" not in records[0]:
|
|
17
|
+
return None
|
|
18
|
+
return list(records[0]["predictions"].keys())
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def get_failed_records(records, pipeline_id, metric="execution_accuracy"):
|
|
22
|
+
failed_records = []
|
|
23
|
+
for r in records:
|
|
24
|
+
if pipeline_id not in r["predictions"]:
|
|
25
|
+
failed_records.append(f"No predictions for {pipeline_id}")
|
|
26
|
+
elif r["predictions"][pipeline_id]["evaluation"].get(metric) == 0:
|
|
27
|
+
failed_records.append(r)
|
|
28
|
+
return failed_records
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def safe_snippet(text, head=4000, tail=4000):
|
|
32
|
+
if len(text) <= head + tail:
|
|
33
|
+
return text
|
|
34
|
+
return text[:head] + "\n…\n" + text[-tail:]
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def safe_code_block(text, max_length=10000):
|
|
38
|
+
"""
|
|
39
|
+
Safely display text in a code block, handling nested backticks.
|
|
40
|
+
Uses HTML pre tags to avoid markdown parsing issues.
|
|
41
|
+
"""
|
|
42
|
+
import html
|
|
43
|
+
|
|
44
|
+
# Truncate if too long
|
|
45
|
+
if len(text) > max_length:
|
|
46
|
+
text = text[:max_length] + "\n...(truncated)"
|
|
47
|
+
# Escape HTML and wrap in pre tags
|
|
48
|
+
escaped = html.escape(text)
|
|
49
|
+
return f"<pre>{escaped}</pre>"
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def head_tail_with_ellipsis(df: pd.DataFrame, k: int = 20) -> pd.DataFrame:
|
|
53
|
+
"""
|
|
54
|
+
Returns the top k and bottom k rows of a DataFrame with ellipsis rows in between.
|
|
55
|
+
|
|
56
|
+
Parameters:
|
|
57
|
+
df (pd.DataFrame): The input DataFrame.
|
|
58
|
+
k (int): Number of rows to show from the top and bottom. Default is 20.
|
|
59
|
+
|
|
60
|
+
Returns:
|
|
61
|
+
pd.DataFrame: A new DataFrame with top k rows, ellipsis, and bottom k rows.
|
|
62
|
+
"""
|
|
63
|
+
if len(df) <= 2 * k:
|
|
64
|
+
return df.copy()
|
|
65
|
+
|
|
66
|
+
top = df.head(k)
|
|
67
|
+
bottom = df.tail(k)
|
|
68
|
+
|
|
69
|
+
# Create ellipsis rows with same columns
|
|
70
|
+
ellipsis_rows = pd.DataFrame(
|
|
71
|
+
[
|
|
72
|
+
["..."] * df.shape[1],
|
|
73
|
+
["... (truncated)"] * df.shape[1],
|
|
74
|
+
["..."] * df.shape[1],
|
|
75
|
+
],
|
|
76
|
+
columns=df.columns,
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
return pd.concat([top, ellipsis_rows, bottom], ignore_index=True)
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def chat_prompt_to_html(prompt):
|
|
83
|
+
import html as html_module
|
|
84
|
+
|
|
85
|
+
html_output = """
|
|
86
|
+
## Full Chat Prompt Conversation
|
|
87
|
+
|
|
88
|
+
<div style="max-height: 400px; overflow-y: auto; border: 1px solid #ccc; padding: 10px; background-color: #f9f9f9; font-family: monospace;">
|
|
89
|
+
"""
|
|
90
|
+
|
|
91
|
+
for i, entry in enumerate(prompt):
|
|
92
|
+
role = entry.get("role", "unknown").capitalize()
|
|
93
|
+
content = entry.get("content", "").strip()
|
|
94
|
+
|
|
95
|
+
# Escape HTML to safely display any content (handles backticks, quotes, etc.)
|
|
96
|
+
escaped_content = html_module.escape(content)
|
|
97
|
+
|
|
98
|
+
# Add message with visual separator
|
|
99
|
+
html_output += f"<div style='margin-bottom: 15px;'>\n"
|
|
100
|
+
html_output += f"<strong>{role} Message {i + 1}:</strong>\n"
|
|
101
|
+
html_output += f"<pre style='margin: 5px 0; padding: 10px; background-color: #ffffff; border-left: 3px solid #007acc; white-space: pre-wrap; word-wrap: break-word;'>{escaped_content}</pre>\n"
|
|
102
|
+
html_output += f"</div>\n"
|
|
103
|
+
|
|
104
|
+
html_output += "</div>\n"
|
|
105
|
+
return html_output
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def format_inference_error_example(record, pred, example_index, total_failed):
|
|
109
|
+
"""Format a failed example where inference itself failed (no SQL generated)."""
|
|
110
|
+
question_id = record.get("id", record.get("_id", f"example_{example_index}"))
|
|
111
|
+
utterance = (
|
|
112
|
+
record.get("page_content")
|
|
113
|
+
or record.get("question")
|
|
114
|
+
or record.get("utterance", "")
|
|
115
|
+
)
|
|
116
|
+
|
|
117
|
+
md = []
|
|
118
|
+
md.append(
|
|
119
|
+
f"### ⚠️ Inference Failed - Question #{example_index} (of {total_failed} examples) - Question ID: `{question_id}`\n"
|
|
120
|
+
)
|
|
121
|
+
md.append(f"**Question**: {utterance}\n")
|
|
122
|
+
|
|
123
|
+
md.append("### ❌ Inference Error")
|
|
124
|
+
md.append(f"```\n{pred.get('inference_error', 'Unknown error')}\n```")
|
|
125
|
+
|
|
126
|
+
# Show raw response if available
|
|
127
|
+
if pred.get('raw_response'):
|
|
128
|
+
md.append("### 📄 Raw Model Response")
|
|
129
|
+
md.append(f"```\n{pred['raw_response']}\n```")
|
|
130
|
+
|
|
131
|
+
# Show prompt that was used
|
|
132
|
+
if pred.get('prompt'):
|
|
133
|
+
md.append("### 📝 Prompt Used")
|
|
134
|
+
md.append(f"```\n{safe_snippet(pred['prompt'], head=500, tail=500)}\n```")
|
|
135
|
+
|
|
136
|
+
md.append("---\n")
|
|
137
|
+
return "\n".join(md)
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def format_failed_example(record, pipeline_id, example_index, total_failed):
|
|
141
|
+
try:
|
|
142
|
+
pred = record["predictions"][pipeline_id]
|
|
143
|
+
except Exception as e:
|
|
144
|
+
logger.error(f"Error reading prediction in record: {record}")
|
|
145
|
+
return f"⚠️ Error reading prediction in record: {record}\n\n"
|
|
146
|
+
|
|
147
|
+
# Check for inference error
|
|
148
|
+
if "inference_error" in pred:
|
|
149
|
+
return format_inference_error_example(record, pred, example_index, total_failed)
|
|
150
|
+
gt_sqls = (
|
|
151
|
+
record.get("sql")
|
|
152
|
+
or record.get("SQL")
|
|
153
|
+
or record.get("metadata", {}).get("sql", [])
|
|
154
|
+
)
|
|
155
|
+
gt_sqls = [gt_sqls] if isinstance(gt_sqls, str) else gt_sqls
|
|
156
|
+
question_id = record.get("id", record.get("_id", f"example_{example_index}"))
|
|
157
|
+
utterance = (
|
|
158
|
+
record.get("page_content")
|
|
159
|
+
or record.get("question")
|
|
160
|
+
or record.get("utterance", "")
|
|
161
|
+
)
|
|
162
|
+
|
|
163
|
+
# Ground truth DFs
|
|
164
|
+
gt_dfs = []
|
|
165
|
+
raw_gt_dfs = record.get("gt_df", [])
|
|
166
|
+
if isinstance(raw_gt_dfs, str):
|
|
167
|
+
gt_dfs = [parse_dataframe(raw_gt_dfs)]
|
|
168
|
+
else:
|
|
169
|
+
for df in raw_gt_dfs:
|
|
170
|
+
try:
|
|
171
|
+
gt_dfs.append(parse_dataframe(df))
|
|
172
|
+
except Exception as e:
|
|
173
|
+
gt_dfs.append(f"⚠️ Error loading GT DF: {e}")
|
|
174
|
+
|
|
175
|
+
# Predicted DF
|
|
176
|
+
pred_df = None
|
|
177
|
+
pred_df_error = None
|
|
178
|
+
if "predicted_df" in pred:
|
|
179
|
+
try:
|
|
180
|
+
pred_df = parse_dataframe(pred["predicted_df"])
|
|
181
|
+
except Exception as e:
|
|
182
|
+
pred_df_error = f"⚠️ Error loading predicted_df: {e}"
|
|
183
|
+
|
|
184
|
+
# Build markdown string
|
|
185
|
+
md = []
|
|
186
|
+
md.append(
|
|
187
|
+
f"### ❓ Failed Question #{example_index} (of {total_failed} examples) - Question ID: `{question_id}`\n"
|
|
188
|
+
)
|
|
189
|
+
md.append(f"**Question**: {utterance}\n")
|
|
190
|
+
|
|
191
|
+
md.append("### ✅ Ground Truth SQL(s)")
|
|
192
|
+
for sql in gt_sqls:
|
|
193
|
+
md.append(f"```sql\n{sql}\n```")
|
|
194
|
+
|
|
195
|
+
md.append("### ❌ Predicted SQL")
|
|
196
|
+
md.append(f"```sql\n{pred.get('predicted_sql', '')}\n```")
|
|
197
|
+
|
|
198
|
+
md.append("### 📊 Evaluation Metrics")
|
|
199
|
+
eval_df = pd.DataFrame([pred.get("evaluation", {})])
|
|
200
|
+
llm_explanation = None
|
|
201
|
+
if "llm_explanation" in eval_df.columns:
|
|
202
|
+
llm_explanation = eval_df.at[0, "llm_explanation"]
|
|
203
|
+
columns_to_drop = ["gt_sql", "gt_df", "llm_explanation"]
|
|
204
|
+
eval_df.drop(columns=columns_to_drop, errors="ignore", inplace=True)
|
|
205
|
+
md.append(eval_df.to_markdown(index=False))
|
|
206
|
+
|
|
207
|
+
md.append("### 📘 Ground Truth Result(s)")
|
|
208
|
+
for i, df in enumerate(gt_dfs):
|
|
209
|
+
md.append(f"**Result {i + 1}:**")
|
|
210
|
+
if isinstance(df, pd.DataFrame):
|
|
211
|
+
md.append(head_tail_with_ellipsis(df).to_markdown(index=False))
|
|
212
|
+
else:
|
|
213
|
+
md.append(df)
|
|
214
|
+
|
|
215
|
+
md.append("### 📕 Predicted Result")
|
|
216
|
+
if pred_df is not None:
|
|
217
|
+
md.append(head_tail_with_ellipsis(pred_df).to_markdown(index=False))
|
|
218
|
+
elif pred_df_error:
|
|
219
|
+
md.append(pred_df_error)
|
|
220
|
+
|
|
221
|
+
# Display agent trace for agentic pipelines, or prompt for standard baseline
|
|
222
|
+
if "agent_trace" in pred and pred["agent_trace"]:
|
|
223
|
+
md.append("### 🤖 Agent Interaction Trace")
|
|
224
|
+
trace = pred["agent_trace"]
|
|
225
|
+
if isinstance(trace, list):
|
|
226
|
+
for i, interaction in enumerate(trace, 1):
|
|
227
|
+
step_name = interaction.get("step", f"step_{i}")
|
|
228
|
+
md.append(f"\n**Step {i}: {step_name}**\n")
|
|
229
|
+
|
|
230
|
+
# Show messages (prompts sent to LLM)
|
|
231
|
+
if "messages" in interaction:
|
|
232
|
+
md.append("<details>")
|
|
233
|
+
md.append("<summary>📝 Messages</summary>\n")
|
|
234
|
+
messages_html = chat_prompt_to_html(interaction["messages"])
|
|
235
|
+
md.append(messages_html)
|
|
236
|
+
md.append("</details>\n")
|
|
237
|
+
|
|
238
|
+
# Show response
|
|
239
|
+
if "response" in interaction:
|
|
240
|
+
md.append(
|
|
241
|
+
f"**Response:** `{safe_snippet(interaction['response'][:200])}`\n"
|
|
242
|
+
)
|
|
243
|
+
|
|
244
|
+
# Show parsed SQL if available
|
|
245
|
+
if "parsed_sql" in interaction:
|
|
246
|
+
md.append(
|
|
247
|
+
f"**Parsed SQL:** \n```sql\n{interaction['parsed_sql']}\n```\n"
|
|
248
|
+
)
|
|
249
|
+
|
|
250
|
+
# Show LLM judge verdict if this is a validation step
|
|
251
|
+
if "verdict" in interaction:
|
|
252
|
+
md.append(
|
|
253
|
+
f"**Verdict:** {interaction['verdict']} (Confidence: {interaction.get('confidence', 'N/A')})\n"
|
|
254
|
+
)
|
|
255
|
+
if "reasoning" in interaction:
|
|
256
|
+
md.append(
|
|
257
|
+
f"**Reasoning:** {safe_snippet(interaction['reasoning'][:300])}\n"
|
|
258
|
+
)
|
|
259
|
+
|
|
260
|
+
# Show error if any
|
|
261
|
+
if "error" in interaction:
|
|
262
|
+
md.append(f"**Error:** {interaction['error']}\n")
|
|
263
|
+
else:
|
|
264
|
+
md.append(safe_code_block(str(trace)))
|
|
265
|
+
|
|
266
|
+
# Also show number of attempts if available
|
|
267
|
+
if "agent_attempts" in pred:
|
|
268
|
+
md.append(f"\n**Total Attempts:** {pred['agent_attempts']}")
|
|
269
|
+
elif "agent_reasoning" in pred and pred["agent_reasoning"]:
|
|
270
|
+
# Fallback to agent_reasoning if trace not available
|
|
271
|
+
md.append("### 🤖 Agent Reasoning")
|
|
272
|
+
reasoning_list = pred["agent_reasoning"]
|
|
273
|
+
if isinstance(reasoning_list, list):
|
|
274
|
+
reasoning_text = "\n".join(
|
|
275
|
+
f"{i}. {reasoning}" for i, reasoning in enumerate(reasoning_list, 1)
|
|
276
|
+
)
|
|
277
|
+
md.append(safe_code_block(reasoning_text))
|
|
278
|
+
else:
|
|
279
|
+
md.append(safe_code_block(str(reasoning_list)))
|
|
280
|
+
|
|
281
|
+
# Also show number of attempts if available
|
|
282
|
+
if "agent_attempts" in pred:
|
|
283
|
+
md.append(f"\n**Attempts:** {pred['agent_attempts']}")
|
|
284
|
+
elif "prompt" in pred:
|
|
285
|
+
md.append("### 🧠 Prompt")
|
|
286
|
+
prompt = pred.get("prompt", "")
|
|
287
|
+
if isinstance(prompt, list):
|
|
288
|
+
html = chat_prompt_to_html(prompt)
|
|
289
|
+
md.append(html)
|
|
290
|
+
else:
|
|
291
|
+
# Use HTML pre tags to avoid issues with nested backticks in prompt
|
|
292
|
+
md.append(safe_code_block(prompt))
|
|
293
|
+
else:
|
|
294
|
+
md.append("### 🧠 Context")
|
|
295
|
+
md.append("_No prompt or agent trace available_")
|
|
296
|
+
|
|
297
|
+
if llm_explanation:
|
|
298
|
+
md.append(
|
|
299
|
+
f"### 🤖 LLM Judge Assessment\nLLM judge score: `{eval_df.at[0, 'llm_score']}`\n"
|
|
300
|
+
)
|
|
301
|
+
md.append("LLM judge explanation (if applicable):\n")
|
|
302
|
+
md.append(safe_code_block(llm_explanation))
|
|
303
|
+
|
|
304
|
+
return "\n\n".join(md)
|
|
305
|
+
|
|
306
|
+
|
|
307
|
+
def export_failed_examples_to_markdown(records, output_path, max_examples=20):
|
|
308
|
+
output_path = Path(output_path)
|
|
309
|
+
output_path.parent.mkdir(parents=True, exist_ok=True)
|
|
310
|
+
|
|
311
|
+
all_pipelines = get_pipeline_ids(records)
|
|
312
|
+
if not all_pipelines:
|
|
313
|
+
logger.error("No predictions found!")
|
|
314
|
+
return
|
|
315
|
+
markdown = "# ❌ Failed Examples by Pipeline\n\n"
|
|
316
|
+
|
|
317
|
+
for pipeline_id in all_pipelines:
|
|
318
|
+
all_failed = get_failed_records(records, pipeline_id)
|
|
319
|
+
failed = all_failed[:max_examples]
|
|
320
|
+
|
|
321
|
+
markdown += f"## 🔍 Pipeline/Model ID: `{pipeline_id}`\n\n"
|
|
322
|
+
if not failed:
|
|
323
|
+
markdown += "✅ No failed predictions found.\n\n"
|
|
324
|
+
else:
|
|
325
|
+
markdown += (
|
|
326
|
+
f"{len(failed)} failed predictions shown (out of {len(all_failed)})\n\n"
|
|
327
|
+
)
|
|
328
|
+
for idx, record in enumerate(failed):
|
|
329
|
+
markdown += format_failed_example(
|
|
330
|
+
record, pipeline_id, idx + 1, len(failed)
|
|
331
|
+
)
|
|
332
|
+
markdown += "\n\n---\n\n"
|
|
333
|
+
|
|
334
|
+
output_path.write_text(markdown)
|
|
335
|
+
logger.info(f"✅ Saved failed examples for error analysis to {output_path}")
|