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,11 @@
1
+ #
2
+ # Copyright IBM Corp. 2025 - 2026
3
+ # SPDX-License-Identifier: Apache-2.0
4
+ #
5
+
6
+ class BasePipeline:
7
+ def run_pipeline(self, input_data):
8
+ raise NotImplementedError("Subclasses should implement this method.")
9
+
10
+ def get_results(self):
11
+ raise NotImplementedError("Subclasses should implement this method.")
@@ -0,0 +1,372 @@
1
+ #
2
+ # Copyright IBM Corp. 2025 - 2026
3
+ # SPDX-License-Identifier: Apache-2.0
4
+ #
5
+
6
+ """
7
+ This module provides a pipeline for generating SQL queries from natural language utterances using IBM watsonx.ai foundation models.
8
+
9
+ Classes:
10
+ LLMApiClient:
11
+ A client for interacting with IBM watsonx.ai to generate SQL queries based on provided database schemas and user utterances.
12
+ - Initializes with model credentials and parameters.
13
+ - Verbalizes database schemas for prompt construction.
14
+ - Generates SQL queries from natural language questions.
15
+
16
+ LLMSQLGenerationPipeline (inherits from BasePipeline):
17
+ Orchestrates the process of loading benchmark data, schemas, and generating SQL predictions using LLMApiClient.
18
+ - Loads benchmark metadata, schema, and data.
19
+ - Manages prediction storage and avoids duplicate predictions for the same model and parameters.
20
+ - Saves generated SQL predictions to a specified file.
21
+
22
+ Usage:
23
+ - Set required IBM watsonx.ai credentials in environment variables: WATSONX_APIKEY, WATSONX_API_BASE, WATSONX_PROJECTID.
24
+ - Use LLMSQLGenerationPipeline to run the SQL generation pipeline for a specified benchmark, model, and parameters.
25
+ """
26
+
27
+ import asyncio
28
+ import os
29
+ import json
30
+ import time
31
+ from text2sql_eval_toolkit.logging import get_logger
32
+ from text2sql_eval_toolkit.inference.base_pipeline import BasePipeline
33
+ from text2sql_eval_toolkit.inference.inference_tools import (
34
+ Text2SQLPrompt,
35
+ WXAIClientChatAPI,
36
+ VLLMClientChatAPI,
37
+ ClaudeClientChatAPI,
38
+ OpenAIClientChatAPI,
39
+ )
40
+ from text2sql_eval_toolkit.utils import (
41
+ get_benchmark_info,
42
+ get_question_id,
43
+ get_utterance,
44
+ )
45
+
46
+
47
+ logger = get_logger(__name__)
48
+
49
+
50
+ class LLMSQLGenerationPipelineSimple(BasePipeline):
51
+ def __init__(self):
52
+ super().__init__()
53
+
54
+ def run_pipeline(
55
+ self,
56
+ benchmark_id: str,
57
+ pipeline_id: str,
58
+ model_name: str,
59
+ model_parameters: dict,
60
+ ):
61
+ benchmark_info = get_benchmark_info(benchmark_id)
62
+ db_type = benchmark_info["db_engine"]["db_type"]
63
+ # Load schema
64
+ with open(benchmark_info["schema_json_path"], "r") as f:
65
+ schema = json.load(f)
66
+
67
+ # Load benchmark data
68
+ with open(benchmark_info["benchmark_json_path"], "r") as fin:
69
+ data = json.load(fin)
70
+
71
+ # Load or initialize predictions file
72
+ if os.path.exists(benchmark_info["predictions_path"]):
73
+ with open(benchmark_info["predictions_path"], "r") as pf:
74
+ predictions_data = json.load(pf)
75
+ else:
76
+ predictions_data = []
77
+
78
+ if model_name.startswith("wxai:"):
79
+ client = WXAIClientChatAPI(model_name[5:], model_parameters)
80
+ elif model_name.startswith("vllm"):
81
+ client = VLLMClientChatAPI(model_name[5:], model_parameters)
82
+ else:
83
+ raise NotImplementedError(
84
+ f"Model {model_name} is not supported. Only 'wxai:' models are currently implemented."
85
+ )
86
+
87
+ # Extend predictions_data with new predictions
88
+ for idx, record in enumerate(data):
89
+ question_id = get_question_id(record)
90
+ utterance = get_utterance(record)
91
+ db_schema = None
92
+ if "tables" not in schema:
93
+ # If schema does not have 'tables', assume it's a multi-schema benchmark
94
+ db_id = record.get("db_id", None)
95
+ if db_id is None:
96
+ raise ValueError(
97
+ f"Object id={question_id} (line {idx}) does not contain 'db_id' for multi-schema benchmarks."
98
+ )
99
+ db_schema = schema.get(db_id)
100
+ logger.debug(f"Using schema for db_id={db_id} from the benchmark.")
101
+ logger.debug(f"Schema: {db_schema}")
102
+ else:
103
+ db_schema = schema
104
+ prompt = record.get("chat_prompt", None)
105
+ generation_prompt = prompt
106
+ if prompt is None:
107
+ prompt = Text2SQLPrompt(utterance, db_schema, db_type)
108
+ generation_prompt = prompt.prompt
109
+ # Find existing record with the same id
110
+ existing = next(
111
+ (p for p in predictions_data if p.get("id") == question_id), None
112
+ )
113
+ if existing:
114
+ # Add or update predictions for this model_name and model_parameters
115
+ if "predictions" not in existing:
116
+ existing["predictions"] = {}
117
+ # Check if this exact model_name and model_parameters already exist
118
+ pred = existing["predictions"].get(pipeline_id)
119
+ if pred:
120
+ logger.info(
121
+ f"Prediction for id={question_id}, pipeline={pipeline_id} prompt already exists. Skipping."
122
+ )
123
+ continue
124
+ # Add/update the prediction for this model_name
125
+ sql = client.generate_sql(prompt)
126
+ existing["predictions"][pipeline_id] = {
127
+ "predicted_sql": sql,
128
+ "prompt": generation_prompt,
129
+ "model_name": model_name,
130
+ "model_parameters": model_parameters,
131
+ }
132
+ else:
133
+ # New object, add predictions field
134
+ sql = client.generate_sql(prompt)
135
+ record["predictions"] = {
136
+ pipeline_id: {
137
+ "predicted_sql": sql,
138
+ "prompt": generation_prompt,
139
+ "model_name": model_name,
140
+ "model_parameters": model_parameters,
141
+ }
142
+ }
143
+ predictions_data.append(record)
144
+
145
+ # Save updated predictions
146
+ with open(benchmark_info["predictions_path"], "w") as fout:
147
+ json.dump(predictions_data, fout, ensure_ascii=False, indent=2)
148
+
149
+ logger.info(f"Predictions written to {benchmark_info['predictions_path']}")
150
+
151
+
152
+ class LLMSQLGenerationPipeline(BasePipeline):
153
+ def __init__(self):
154
+ super().__init__()
155
+
156
+ async def generate_sql(
157
+ self,
158
+ idx,
159
+ record,
160
+ schema,
161
+ db_type,
162
+ pipeline_id,
163
+ model_name,
164
+ model_parameters,
165
+ client,
166
+ predictions_data,
167
+ semaphore,
168
+ timeout=1200, # timeout in seconds
169
+ force_rerun=False,
170
+ skip_inference_error_retries=False,
171
+ ):
172
+ async with semaphore:
173
+ question_id = get_question_id(record)
174
+ try:
175
+ utterance = get_utterance(record)
176
+ evidence = record.get("evidence", None)
177
+ db_schema = (
178
+ schema.get(record.get("db_id"))
179
+ if "tables" not in schema
180
+ else schema
181
+ )
182
+ prompt = record.get("chat_prompt", None)
183
+ generation_prompt = prompt
184
+ if prompt is None:
185
+ prompt = Text2SQLPrompt(utterance, db_schema, db_type, evidence)
186
+ generation_prompt = prompt.prompt
187
+
188
+ existing = next(
189
+ (p for p in predictions_data if p.get("id") == question_id), None
190
+ )
191
+ if existing:
192
+ if "predictions" not in existing:
193
+ existing["predictions"] = {}
194
+ pred = existing["predictions"].get(pipeline_id)
195
+ if pred:
196
+ # Always retry if there was an inference error (unless skip flag is set)
197
+ if "inference_error" in pred and not skip_inference_error_retries:
198
+ logger.info(
199
+ f"Retrying failed inference for id={question_id}, pipeline={pipeline_id}"
200
+ )
201
+ # Continue with inference (don't return)
202
+ elif not force_rerun:
203
+ logger.info(
204
+ f"Prediction for id={question_id}, pipeline={pipeline_id} already exists. Skipping..."
205
+ )
206
+ return
207
+ logger.debug(f"Starting inference for record #{idx}")
208
+ inference_start = time.perf_counter()
209
+ sql, token_usage = await asyncio.wait_for(
210
+ asyncio.to_thread(client.generate_sql, prompt),
211
+ timeout=timeout,
212
+ )
213
+ inference_end = time.perf_counter()
214
+ inference_time_ms = (inference_end - inference_start) * 1000
215
+ logger.debug(f"Finished generating SQL for record #{idx}")
216
+ existing["predictions"][pipeline_id] = {
217
+ "predicted_sql": sql,
218
+ "prompt": generation_prompt,
219
+ "model_name": model_name,
220
+ "model_parameters": model_parameters,
221
+ "token_usage": token_usage,
222
+ "inference_time_ms": round(inference_time_ms, 2),
223
+ }
224
+ else:
225
+ logger.debug(f"Starting inference for record #{idx}")
226
+ inference_start = time.perf_counter()
227
+ sql, token_usage = await asyncio.wait_for(
228
+ asyncio.to_thread(client.generate_sql, prompt),
229
+ timeout=timeout,
230
+ )
231
+ inference_end = time.perf_counter()
232
+ inference_time_ms = (inference_end - inference_start) * 1000
233
+ logger.debug(f"Finished generating SQL for record #{idx}")
234
+ record["predictions"] = {
235
+ pipeline_id: {
236
+ "predicted_sql": sql,
237
+ "prompt": generation_prompt,
238
+ "model_name": model_name,
239
+ "model_parameters": model_parameters,
240
+ "token_usage": token_usage,
241
+ "inference_time_ms": round(inference_time_ms, 2),
242
+ }
243
+ }
244
+ predictions_data.append(record)
245
+
246
+ except TimeoutError as e:
247
+ logger.error(
248
+ f"Timeout in record {idx} (question id={question_id}): {e}"
249
+ )
250
+ # Create prediction record with timeout error
251
+ error_record = {
252
+ "predicted_sql": None,
253
+ "prompt": generation_prompt,
254
+ "model_name": model_name,
255
+ "model_parameters": model_parameters,
256
+ "inference_error": f"TimeoutError: {str(e)}",
257
+ "inference_time_ms": timeout * 1000, # Max time reached
258
+ }
259
+ if existing:
260
+ existing["predictions"][pipeline_id] = error_record
261
+ else:
262
+ record["predictions"] = {pipeline_id: error_record}
263
+ predictions_data.append(record)
264
+
265
+ except Exception as e:
266
+ logger.error(f"Record {idx} (question id={question_id}) failed: {e}")
267
+ # Create prediction record with inference error
268
+ error_record = {
269
+ "predicted_sql": None,
270
+ "prompt": generation_prompt,
271
+ "model_name": model_name,
272
+ "model_parameters": model_parameters,
273
+ "inference_error": str(e),
274
+ }
275
+ # Try to capture serializable response info if available
276
+ if hasattr(e, 'response'):
277
+ try:
278
+ response = e.response
279
+ error_record["response_info"] = {
280
+ "status_code": getattr(response, 'status_code', None),
281
+ "reason": getattr(response, 'reason', None),
282
+ "text": getattr(response, 'text', None)[:1000] if hasattr(response, 'text') else None, # Limit text length
283
+ }
284
+ except Exception:
285
+ # If we can't serialize the response, just skip it
286
+ pass
287
+ if existing:
288
+ existing["predictions"][pipeline_id] = error_record
289
+ else:
290
+ record["predictions"] = {pipeline_id: error_record}
291
+ predictions_data.append(record)
292
+
293
+ def run_pipeline(
294
+ self,
295
+ benchmark_id: str,
296
+ model_name: str,
297
+ model_parameters: dict,
298
+ max_num_threads: int = 16,
299
+ force_rerun: bool = False,
300
+ skip_inference_error_retries: bool = False,
301
+ ):
302
+ pipeline_id = model_name + "-greedy-zero-shot-chatapi"
303
+ logger.debug(
304
+ f"🚀 Running inference for benchmark {benchmark_id}, pipeline: {pipeline_id}"
305
+ )
306
+ benchmark_info = get_benchmark_info(benchmark_id)
307
+ db_type = benchmark_info["db_engine"]["db_type"]
308
+
309
+ with open(benchmark_info["schema_json_path"], "r") as f:
310
+ schema = json.load(f)
311
+ with open(benchmark_info["benchmark_json_path"], "r") as fin:
312
+ data = json.load(fin)
313
+ if os.path.exists(benchmark_info["predictions_path"]):
314
+ with open(benchmark_info["predictions_path"], "r") as pf:
315
+ predictions_data = json.load(pf)
316
+ else:
317
+ predictions_data = []
318
+
319
+ if model_name.startswith("wxai:"):
320
+ client = WXAIClientChatAPI(model_name[5:], model_parameters)
321
+ elif model_name.startswith("anthropic:"):
322
+ client = ClaudeClientChatAPI(model_name[10:], model_parameters)
323
+ elif model_name.startswith("vllm:"):
324
+ client = VLLMClientChatAPI(model_name[5:], model_parameters)
325
+ elif model_name.startswith("ollama:"):
326
+ # Ollama uses OpenAI-compatible API with custom base URL
327
+ client = OpenAIClientChatAPI(model_name[7:], model_parameters)
328
+ elif model_name.startswith("openai:"):
329
+ client = OpenAIClientChatAPI(model_name[7:], model_parameters)
330
+ elif model_name.startswith("rits"):
331
+ logger.info(f"Getting RITS model endpoint for {model_name}")
332
+ model_id = model_name.split("/")[-1].replace(".", "-").lower()
333
+ rits_api_key = os.environ.get("RITS_API_KEY")
334
+ if rits_api_key is None:
335
+ raise ValueError("Missing RITS_API_KEY environment variable")
336
+ os.environ["VLLM_API_BASE"] = (
337
+ f"https://inference-3scale-apicast-production.apps.rits.fmaas.res.ibm.com/{model_id}/v1"
338
+ )
339
+ client = VLLMClientChatAPI(model_name[5:], model_parameters)
340
+ else:
341
+ raise NotImplementedError(f"Model {model_name} is not supported.")
342
+
343
+ async def run_all():
344
+ semaphore = asyncio.Semaphore(max_num_threads)
345
+ tasks = [
346
+ self.generate_sql(
347
+ idx,
348
+ obj,
349
+ schema,
350
+ db_type,
351
+ pipeline_id,
352
+ model_name,
353
+ model_parameters,
354
+ client,
355
+ predictions_data,
356
+ semaphore,
357
+ force_rerun=force_rerun,
358
+ skip_inference_error_retries=skip_inference_error_retries,
359
+ )
360
+ for idx, obj in enumerate(data)
361
+ ]
362
+ await asyncio.gather(*tasks)
363
+
364
+ asyncio.run(run_all())
365
+
366
+ with open(benchmark_info["predictions_path"], "w") as fout:
367
+ json.dump(predictions_data, fout, ensure_ascii=False, indent=2)
368
+
369
+ logger.debug(
370
+ f"✅ Inference completed for benchmark '{benchmark_id}', pipeline: {pipeline_id}."
371
+ )
372
+ logger.info(f"Predictions written to {benchmark_info['predictions_path']}")