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.
Files changed (28) hide show
  1. text2sql_eval_toolkit/__init__.py +107 -0
  2. text2sql_eval_toolkit/analysis/__init__.py +5 -0
  3. text2sql_eval_toolkit/analysis/error_analysis.py +335 -0
  4. text2sql_eval_toolkit/analysis/report_tools.py +719 -0
  5. text2sql_eval_toolkit/config_args.py +65 -0
  6. text2sql_eval_toolkit/data/__init__.py +4 -0
  7. text2sql_eval_toolkit/data/benchmarks.json +69 -0
  8. text2sql_eval_toolkit/data/test-benchmarks.json +69 -0
  9. text2sql_eval_toolkit/env_loader.py +55 -0
  10. text2sql_eval_toolkit/evaluation/__init__.py +26 -0
  11. text2sql_eval_toolkit/evaluation/evaluation_tools.py +759 -0
  12. text2sql_eval_toolkit/evaluation/llm_as_judge.py +90 -0
  13. text2sql_eval_toolkit/execution/__init__.py +5 -0
  14. text2sql_eval_toolkit/execution/execution_tools.py +1448 -0
  15. text2sql_eval_toolkit/execution/replace_select_tool.py +114 -0
  16. text2sql_eval_toolkit/inference/__init__.py +5 -0
  17. text2sql_eval_toolkit/inference/agentic_pipeline.py +2335 -0
  18. text2sql_eval_toolkit/inference/base_pipeline.py +11 -0
  19. text2sql_eval_toolkit/inference/baseline_llm_pipeline.py +372 -0
  20. text2sql_eval_toolkit/inference/inference_tools.py +769 -0
  21. text2sql_eval_toolkit/logging.py +54 -0
  22. text2sql_eval_toolkit/profiling/profiling_tools.py +185 -0
  23. text2sql_eval_toolkit/utils.py +302 -0
  24. text2sql_eval_toolkit-1.0.0.dist-info/METADATA +382 -0
  25. text2sql_eval_toolkit-1.0.0.dist-info/RECORD +28 -0
  26. text2sql_eval_toolkit-1.0.0.dist-info/WHEEL +5 -0
  27. text2sql_eval_toolkit-1.0.0.dist-info/licenses/LICENSE +201 -0
  28. 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,5 @@
1
+ #
2
+ # Copyright IBM Corp. 2025 - 2026
3
+ # SPDX-License-Identifier: Apache-2.0
4
+ #
5
+
@@ -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}")