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,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']}")
|