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,54 @@
|
|
|
1
|
+
#
|
|
2
|
+
# Copyright IBM Corp. 2025 - 2026
|
|
3
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
4
|
+
#
|
|
5
|
+
|
|
6
|
+
import logging
|
|
7
|
+
from tqdm import tqdm
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class TqdmLoggingHandler(logging.Handler):
|
|
12
|
+
def emit(self, record):
|
|
13
|
+
try:
|
|
14
|
+
msg = self.format(record)
|
|
15
|
+
tqdm.write(msg)
|
|
16
|
+
self.flush()
|
|
17
|
+
except Exception:
|
|
18
|
+
self.handleError(record)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def get_logger(
|
|
22
|
+
name: str = "text2sql_eval_toolkit", level=logging.DEBUG, log_file: str = None
|
|
23
|
+
):
|
|
24
|
+
logger = logging.getLogger(name)
|
|
25
|
+
|
|
26
|
+
if not logger.handlers:
|
|
27
|
+
logger.setLevel(level)
|
|
28
|
+
|
|
29
|
+
console_handler = TqdmLoggingHandler()
|
|
30
|
+
console_formatter = logging.Formatter(
|
|
31
|
+
"%(asctime)s - %(levelname)s - %(message)s", datefmt="%H:%M:%S"
|
|
32
|
+
)
|
|
33
|
+
console_handler.setFormatter(console_formatter)
|
|
34
|
+
logger.addHandler(console_handler)
|
|
35
|
+
|
|
36
|
+
# Default log file path relative to the project root
|
|
37
|
+
if log_file is None:
|
|
38
|
+
project_root = (
|
|
39
|
+
Path(__file__).resolve().parents[2]
|
|
40
|
+
) # Go up from src/text2sql_eval_toolkit
|
|
41
|
+
log_file = project_root / "data" / "results" / "bak" / "log.txt"
|
|
42
|
+
|
|
43
|
+
log_file = Path(log_file)
|
|
44
|
+
log_file.parent.mkdir(parents=True, exist_ok=True) # Ensure directory exists
|
|
45
|
+
|
|
46
|
+
# File handler
|
|
47
|
+
file_handler = logging.FileHandler(log_file, mode="w")
|
|
48
|
+
file_formatter = logging.Formatter(
|
|
49
|
+
"%(asctime)s - %(levelname)s - %(message)s", datefmt="%Y-%m-%d %H:%M:%S"
|
|
50
|
+
)
|
|
51
|
+
file_handler.setFormatter(file_formatter)
|
|
52
|
+
logger.addHandler(file_handler)
|
|
53
|
+
|
|
54
|
+
return logger
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
#
|
|
2
|
+
# Copyright IBM Corp. 2025 - 2026
|
|
3
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
4
|
+
#
|
|
5
|
+
|
|
6
|
+
import json
|
|
7
|
+
import os
|
|
8
|
+
from sqlglot import parse_one, exp
|
|
9
|
+
import shutil
|
|
10
|
+
from tqdm import tqdm
|
|
11
|
+
from typing import Dict
|
|
12
|
+
from text2sql_eval_toolkit.utils import get_gt_sqls
|
|
13
|
+
from text2sql_eval_toolkit.logging import get_logger
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
logger = get_logger(__name__)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def analyze_sql_query(sql: str, dialect: str = "postgres") -> Dict:
|
|
20
|
+
"""
|
|
21
|
+
Analyze a SQL query and classify it into categories with structural features and descriptive tags.
|
|
22
|
+
|
|
23
|
+
Args:
|
|
24
|
+
sql (str): The SQL query string.
|
|
25
|
+
dialect (str): SQL dialect for parsing (default is 'postgres').
|
|
26
|
+
|
|
27
|
+
Returns:
|
|
28
|
+
Dict: A dictionary with structural features and a set of descriptive tags.
|
|
29
|
+
"""
|
|
30
|
+
parsed = parse_one(sql, dialect=dialect)
|
|
31
|
+
|
|
32
|
+
def count(exp_type):
|
|
33
|
+
return len(list(parsed.find_all(exp_type)))
|
|
34
|
+
|
|
35
|
+
def count_names(exp_type):
|
|
36
|
+
return len([e.name for e in parsed.find_all(exp_type)])
|
|
37
|
+
|
|
38
|
+
features = {
|
|
39
|
+
"query_table_count": count_names(exp.Table),
|
|
40
|
+
"query_column_count": count_names(exp.Column),
|
|
41
|
+
"query_nested_count": count(exp.Select)
|
|
42
|
+
+ count(exp.Delete)
|
|
43
|
+
+ count(exp.Insert)
|
|
44
|
+
- 1,
|
|
45
|
+
"query_aggregate_count": count(exp.AggFunc),
|
|
46
|
+
"query_sort_count": count(exp.Ordered),
|
|
47
|
+
"query_window_func_count": count(exp.Window),
|
|
48
|
+
"query_join_count": count(exp.Join),
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
# Classification logic
|
|
52
|
+
is_basic = (
|
|
53
|
+
features["query_table_count"] == 1
|
|
54
|
+
and features["query_nested_count"] == 0
|
|
55
|
+
and features["query_window_func_count"] == 0
|
|
56
|
+
and features["query_join_count"] == 0
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
is_multi_table = (
|
|
60
|
+
features["query_table_count"] > 1
|
|
61
|
+
and features["query_nested_count"] == 0
|
|
62
|
+
and features["query_window_func_count"] == 0
|
|
63
|
+
and features["query_join_count"] >= 1
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
is_advanced = features["query_table_count"] == 1 and (
|
|
67
|
+
features["query_nested_count"] > 0 or features["query_window_func_count"] > 0
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
# Tag generation
|
|
71
|
+
tags = set()
|
|
72
|
+
if is_basic:
|
|
73
|
+
tags.add("single_source_basic")
|
|
74
|
+
if is_multi_table:
|
|
75
|
+
tags.add("multi_table_simple")
|
|
76
|
+
if is_advanced:
|
|
77
|
+
tags.add("single_source_advanced")
|
|
78
|
+
|
|
79
|
+
if features["query_join_count"] > 0:
|
|
80
|
+
tags.add("has_join")
|
|
81
|
+
if features["query_nested_count"] > 0:
|
|
82
|
+
tags.add("has_nested_query")
|
|
83
|
+
if features["query_aggregate_count"] > 0:
|
|
84
|
+
tags.add("has_aggregation")
|
|
85
|
+
if features["query_sort_count"] > 0:
|
|
86
|
+
tags.add("has_sorting")
|
|
87
|
+
if features["query_window_func_count"] > 0:
|
|
88
|
+
tags.add("has_window_function")
|
|
89
|
+
|
|
90
|
+
return {"features": features, "categories": sorted(tags)}
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def merge_dictionaries(original_dict, new_dict):
|
|
94
|
+
"""
|
|
95
|
+
Merges new_dict into original_dict in-place with specific logic for 'features' and 'categories' keys.
|
|
96
|
+
|
|
97
|
+
Args:
|
|
98
|
+
original_dict (dict): The original dictionary (modified in-place)
|
|
99
|
+
new_dict (dict): The new dictionary to merge
|
|
100
|
+
|
|
101
|
+
Returns:
|
|
102
|
+
bool: True if any conflicts/overwrites occurred, False otherwise
|
|
103
|
+
"""
|
|
104
|
+
overwrite_occurred = False
|
|
105
|
+
|
|
106
|
+
for key, value in new_dict.items():
|
|
107
|
+
if key == "features":
|
|
108
|
+
# Initialize features if it doesn't exist
|
|
109
|
+
if key not in original_dict:
|
|
110
|
+
original_dict[key] = {}
|
|
111
|
+
|
|
112
|
+
# Merge features dictionaries
|
|
113
|
+
for feature_key, feature_value in value.items():
|
|
114
|
+
if (
|
|
115
|
+
feature_key in original_dict[key]
|
|
116
|
+
and original_dict[key][feature_key] != feature_value
|
|
117
|
+
):
|
|
118
|
+
overwrite_occurred = True
|
|
119
|
+
original_dict[key][feature_key] = feature_value
|
|
120
|
+
|
|
121
|
+
elif key == "categories":
|
|
122
|
+
# Initialize categories if it doesn't exist
|
|
123
|
+
if key not in original_dict:
|
|
124
|
+
original_dict[key] = []
|
|
125
|
+
|
|
126
|
+
# Add new categories that aren't already present
|
|
127
|
+
for category in value:
|
|
128
|
+
if category not in original_dict[key]:
|
|
129
|
+
original_dict[key].append(category)
|
|
130
|
+
|
|
131
|
+
else:
|
|
132
|
+
# For other keys, new dictionary takes precedence
|
|
133
|
+
if key in original_dict and original_dict[key] != value:
|
|
134
|
+
overwrite_occurred = True
|
|
135
|
+
original_dict[key] = value
|
|
136
|
+
|
|
137
|
+
return overwrite_occurred
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def profile_pred_or_eval_json_file(
|
|
141
|
+
json_file_path: str, dialect: str = "postgres"
|
|
142
|
+
) -> None:
|
|
143
|
+
# Load the JSON data
|
|
144
|
+
with open(json_file_path, "r", encoding="utf-8") as f:
|
|
145
|
+
data = json.load(f)
|
|
146
|
+
|
|
147
|
+
backup_file_path = json_file_path + ".bak"
|
|
148
|
+
shutil.copy2(json_file_path, backup_file_path)
|
|
149
|
+
|
|
150
|
+
# Ensure the data is a list
|
|
151
|
+
if not isinstance(data, list):
|
|
152
|
+
raise ValueError("JSON file must contain an array of objects.")
|
|
153
|
+
|
|
154
|
+
# Track if any overwrites occur
|
|
155
|
+
overwrite_occurred = False
|
|
156
|
+
# Process each record in the input
|
|
157
|
+
for record in tqdm(data):
|
|
158
|
+
gt_sqls = get_gt_sqls(record)
|
|
159
|
+
sql_query = gt_sqls[0]
|
|
160
|
+
if len(gt_sqls) > 1:
|
|
161
|
+
logger.warning(
|
|
162
|
+
f"More than on gt query in record in {json_file_path}. Profiling only the first one."
|
|
163
|
+
)
|
|
164
|
+
|
|
165
|
+
try:
|
|
166
|
+
analysis_result = analyze_sql_query(sql_query, dialect)
|
|
167
|
+
except Exception as e:
|
|
168
|
+
logger.error(f"Failed to profile SQL query: {sql_query}. Error: {repr(e)}")
|
|
169
|
+
continue
|
|
170
|
+
# Initialize or update the 'meta' field
|
|
171
|
+
if "meta" not in record:
|
|
172
|
+
record["meta"] = analysis_result
|
|
173
|
+
else:
|
|
174
|
+
overwrite_occurred = merge_dictionaries(record["meta"], analysis_result)
|
|
175
|
+
|
|
176
|
+
# Write the updated data back to the original file
|
|
177
|
+
with open(json_file_path, "w", encoding="utf-8") as f:
|
|
178
|
+
json.dump(data, f, indent=2)
|
|
179
|
+
|
|
180
|
+
if not overwrite_occurred:
|
|
181
|
+
os.remove(backup_file_path)
|
|
182
|
+
else:
|
|
183
|
+
print(f"Backup created at {backup_file_path} due to overwrites.")
|
|
184
|
+
|
|
185
|
+
logger.info(f"Profiling complete. Results written in {json_file_path}")
|
|
@@ -0,0 +1,302 @@
|
|
|
1
|
+
#
|
|
2
|
+
# Copyright IBM Corp. 2025 - 2026
|
|
3
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
4
|
+
#
|
|
5
|
+
|
|
6
|
+
import asyncio
|
|
7
|
+
import importlib.resources as resources
|
|
8
|
+
import json
|
|
9
|
+
import os
|
|
10
|
+
import time
|
|
11
|
+
import pandas as pd
|
|
12
|
+
from concurrent.futures import ThreadPoolExecutor, TimeoutError
|
|
13
|
+
from typing import Any, Dict
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
from text2sql_eval_toolkit.logging import get_logger
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
BENCHMARKS_FILE = resources.files("text2sql_eval_toolkit.data").joinpath(
|
|
19
|
+
"benchmarks.json"
|
|
20
|
+
)
|
|
21
|
+
TEST_BENCHMARKS_FILE = resources.files("text2sql_eval_toolkit.data").joinpath(
|
|
22
|
+
"test-benchmarks.json"
|
|
23
|
+
)
|
|
24
|
+
logger = get_logger(__name__)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def get_available_benchmarks(include_test: bool = True):
|
|
28
|
+
"""
|
|
29
|
+
Get list of available benchmark IDs.
|
|
30
|
+
|
|
31
|
+
Args:
|
|
32
|
+
include_test: If True, include test benchmarks from test-benchmarks.json
|
|
33
|
+
|
|
34
|
+
Returns:
|
|
35
|
+
List of benchmark IDs
|
|
36
|
+
"""
|
|
37
|
+
benchmarks = []
|
|
38
|
+
|
|
39
|
+
# Load production benchmarks
|
|
40
|
+
if BENCHMARKS_FILE.exists():
|
|
41
|
+
with open(BENCHMARKS_FILE, "r") as f:
|
|
42
|
+
data = json.load(f)
|
|
43
|
+
benchmarks.extend(list(data.keys()))
|
|
44
|
+
|
|
45
|
+
# Load test benchmarks if requested
|
|
46
|
+
if include_test and TEST_BENCHMARKS_FILE.exists():
|
|
47
|
+
with open(TEST_BENCHMARKS_FILE, "r") as f:
|
|
48
|
+
data = json.load(f)
|
|
49
|
+
benchmarks.extend(list(data.keys()))
|
|
50
|
+
|
|
51
|
+
return benchmarks
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def get_benchmarks_info(is_test: bool = False) -> Dict[str, Any]:
|
|
55
|
+
"""
|
|
56
|
+
Retrieves all the benchmarks' information.
|
|
57
|
+
|
|
58
|
+
Args:
|
|
59
|
+
is_test: If True, load test benchmarks from test-benchmarks.json
|
|
60
|
+
|
|
61
|
+
Returns:
|
|
62
|
+
Dict[str, Any]: Dictionary containing info and paths to benchmark files.
|
|
63
|
+
"""
|
|
64
|
+
benchmarks_file = TEST_BENCHMARKS_FILE if is_test else BENCHMARKS_FILE
|
|
65
|
+
benchmarks_info = {}
|
|
66
|
+
try:
|
|
67
|
+
with open(benchmarks_file, "r") as meta_file:
|
|
68
|
+
benchmarks_meta = json.load(meta_file)
|
|
69
|
+
except Exception as e:
|
|
70
|
+
logger.error(f"Error loading the benchmarks JSON file: {benchmarks_file}.")
|
|
71
|
+
raise e
|
|
72
|
+
for benchmark_id, benchmark_info in benchmarks_meta.items():
|
|
73
|
+
root = BENCHMARKS_FILE.parent
|
|
74
|
+
if benchmark_id not in benchmarks_meta:
|
|
75
|
+
raise ValueError(
|
|
76
|
+
f"Benchmark ID '{benchmark_id}' not found in benchmarks.json."
|
|
77
|
+
)
|
|
78
|
+
benchmark_info = benchmarks_meta[benchmark_id]
|
|
79
|
+
benchmark_info["benchmark_json_path"] = resolve_path(
|
|
80
|
+
root, benchmark_info["data"]
|
|
81
|
+
)
|
|
82
|
+
benchmark_info["schema_json_path"] = resolve_path(
|
|
83
|
+
root, benchmark_info["schema"]
|
|
84
|
+
)
|
|
85
|
+
benchmark_info["predictions_path"] = resolve_path(
|
|
86
|
+
root, benchmark_info["predictions"]
|
|
87
|
+
)
|
|
88
|
+
benchmark_info["eval_results_path"] = Path(
|
|
89
|
+
benchmark_info["predictions_path"].with_name(
|
|
90
|
+
benchmark_info["predictions_path"].stem + "_eval.json"
|
|
91
|
+
)
|
|
92
|
+
).resolve()
|
|
93
|
+
benchmark_info["eval_summary_path"] = Path(
|
|
94
|
+
benchmark_info["predictions_path"].with_name(
|
|
95
|
+
benchmark_info["predictions_path"].stem + "_eval_summary.json"
|
|
96
|
+
)
|
|
97
|
+
).resolve()
|
|
98
|
+
benchmarks_info[benchmark_id] = benchmark_info
|
|
99
|
+
return benchmarks_info
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def resolve_path(root, path_str):
|
|
103
|
+
"""
|
|
104
|
+
Resolves a given path string relative to a root directory.
|
|
105
|
+
If the provided path string is absolute, returns it as a Path object.
|
|
106
|
+
Otherwise, returns the path relative to the specified root directory.
|
|
107
|
+
Args:
|
|
108
|
+
root (Path): The root directory to resolve relative paths against.
|
|
109
|
+
path_str (str): The path string to resolve.
|
|
110
|
+
Returns:
|
|
111
|
+
Path: The resolved absolute or relative path as a Path object.
|
|
112
|
+
"""
|
|
113
|
+
p = Path(path_str)
|
|
114
|
+
if p.is_absolute():
|
|
115
|
+
return p
|
|
116
|
+
return root / p
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def get_benchmark_info(benchmark_id: str, is_test: bool = False) -> Dict[str, Any]:
|
|
120
|
+
"""
|
|
121
|
+
Retrieves the benchmark files for a given benchmark ID.
|
|
122
|
+
Automatically detects if benchmark is in test-benchmarks.json if not found in benchmarks.json.
|
|
123
|
+
|
|
124
|
+
Args:
|
|
125
|
+
benchmark_id (str): Identifier for the benchmark dataset.
|
|
126
|
+
is_test (bool): If True, load from test-benchmarks.json. If False, tries production first, then test.
|
|
127
|
+
|
|
128
|
+
Returns:
|
|
129
|
+
Dict[str, Any]: Dictionary containing info and paths to benchmark files.
|
|
130
|
+
"""
|
|
131
|
+
# If is_test is True, only look in test benchmarks
|
|
132
|
+
if is_test:
|
|
133
|
+
benchmarks_file = TEST_BENCHMARKS_FILE
|
|
134
|
+
with open(benchmarks_file, "r") as meta_file:
|
|
135
|
+
benchmarks_meta = json.load(meta_file)
|
|
136
|
+
if benchmark_id not in benchmarks_meta:
|
|
137
|
+
raise ValueError(f"Benchmark ID '{benchmark_id}' not found in test-benchmarks.json.")
|
|
138
|
+
else:
|
|
139
|
+
# Try production benchmarks first
|
|
140
|
+
benchmarks_file = BENCHMARKS_FILE
|
|
141
|
+
with open(benchmarks_file, "r") as meta_file:
|
|
142
|
+
benchmarks_meta = json.load(meta_file)
|
|
143
|
+
|
|
144
|
+
# If not found in production, try test benchmarks
|
|
145
|
+
if benchmark_id not in benchmarks_meta and TEST_BENCHMARKS_FILE.exists():
|
|
146
|
+
benchmarks_file = TEST_BENCHMARKS_FILE
|
|
147
|
+
with open(benchmarks_file, "r") as meta_file:
|
|
148
|
+
benchmarks_meta = json.load(meta_file)
|
|
149
|
+
if benchmark_id not in benchmarks_meta:
|
|
150
|
+
raise ValueError(f"Benchmark ID '{benchmark_id}' not found in benchmarks.json or test-benchmarks.json.")
|
|
151
|
+
elif benchmark_id not in benchmarks_meta:
|
|
152
|
+
raise ValueError(f"Benchmark ID '{benchmark_id}' not found in benchmarks.json.")
|
|
153
|
+
|
|
154
|
+
root = benchmarks_file.parent
|
|
155
|
+
benchmark_info = benchmarks_meta[benchmark_id]
|
|
156
|
+
benchmark_info["benchmark_json_path"] = resolve_path(root, benchmark_info["data"])
|
|
157
|
+
benchmark_info["schema_json_path"] = resolve_path(root, benchmark_info["schema"])
|
|
158
|
+
benchmark_info["predictions_path"] = resolve_path(
|
|
159
|
+
root, benchmark_info["predictions"]
|
|
160
|
+
)
|
|
161
|
+
return benchmark_info
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
def run_with_timeout(func, timeout=90, retries=2, wait=3, *args, **kwargs):
|
|
165
|
+
"""
|
|
166
|
+
Runs a function with a timeout and retries.
|
|
167
|
+
|
|
168
|
+
Parameters:
|
|
169
|
+
func (callable): The function to run.
|
|
170
|
+
timeout (int): Timeout in seconds for each attempt.
|
|
171
|
+
retries (int): Number of retries after the first attempt.
|
|
172
|
+
wait (int): Seconds to wait between retries.
|
|
173
|
+
*args, **kwargs: Arguments to pass to the function.
|
|
174
|
+
|
|
175
|
+
Returns:
|
|
176
|
+
The result of the function if successful.
|
|
177
|
+
|
|
178
|
+
Raises:
|
|
179
|
+
TimeoutError: If all attempts time out.
|
|
180
|
+
"""
|
|
181
|
+
for attempt in range(retries + 1):
|
|
182
|
+
with ThreadPoolExecutor(max_workers=1) as executor:
|
|
183
|
+
future = executor.submit(func, *args, **kwargs)
|
|
184
|
+
try:
|
|
185
|
+
return future.result(timeout=timeout)
|
|
186
|
+
except TimeoutError:
|
|
187
|
+
logger.info(f"⚠️ Attempt {attempt + 1} timed out.")
|
|
188
|
+
if attempt < retries:
|
|
189
|
+
time.sleep(wait)
|
|
190
|
+
else:
|
|
191
|
+
raise TimeoutError(
|
|
192
|
+
f"❗️ Function timed out after {retries + 1} attempts."
|
|
193
|
+
)
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
async def run_with_timeout_async(task, base_timeout=90, retries=2, wait=3):
|
|
197
|
+
for attempt in range(retries + 1):
|
|
198
|
+
timeout = base_timeout * (attempt + 1)
|
|
199
|
+
try:
|
|
200
|
+
return await asyncio.wait_for(task(), timeout=timeout)
|
|
201
|
+
except asyncio.TimeoutError:
|
|
202
|
+
logger.info(f"⚠️ Attempt {attempt + 1} timed out after {timeout} seconds.")
|
|
203
|
+
if attempt < retries:
|
|
204
|
+
logger.info(f"⏳ Retrying in {wait} seconds...")
|
|
205
|
+
await asyncio.sleep(wait)
|
|
206
|
+
else:
|
|
207
|
+
raise asyncio.TimeoutError(
|
|
208
|
+
f"❗️ Function timed out after {retries + 1} attempts."
|
|
209
|
+
)
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
def parse_dataframe(json_str):
|
|
213
|
+
"""Reconstruct a DataFrame from a JSON-encoded dictionary."""
|
|
214
|
+
try:
|
|
215
|
+
df_dict = json.loads(json_str)
|
|
216
|
+
return pd.DataFrame(
|
|
217
|
+
data=df_dict["data"], columns=df_dict["columns"], index=df_dict["index"]
|
|
218
|
+
)
|
|
219
|
+
except Exception as e:
|
|
220
|
+
raise ValueError(
|
|
221
|
+
f"Failed to parse DataFrame JSON. Error: {e}. JSON string: {json_str}"
|
|
222
|
+
)
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
def truncate_dataframe(
|
|
226
|
+
df: pd.DataFrame, head: int = 10, tail: int = 10
|
|
227
|
+
) -> pd.DataFrame:
|
|
228
|
+
"""
|
|
229
|
+
Truncate a DataFrame to show only the first `head` rows and last `tail` rows.
|
|
230
|
+
If the DataFrame has more rows than head + tail, insert a '...' row in between
|
|
231
|
+
with empty strings so print(df) looks clean.
|
|
232
|
+
"""
|
|
233
|
+
if len(df) <= head + tail:
|
|
234
|
+
return df
|
|
235
|
+
|
|
236
|
+
top = df.head(head)
|
|
237
|
+
bottom = df.tail(tail)
|
|
238
|
+
|
|
239
|
+
# Row of empty strings with index labeled "..."
|
|
240
|
+
ellipsis_row = pd.DataFrame([[""] * df.shape[1]], columns=df.columns, index=["..."])
|
|
241
|
+
|
|
242
|
+
return pd.concat([top, ellipsis_row, bottom])
|
|
243
|
+
|
|
244
|
+
|
|
245
|
+
def get_question_id(record):
|
|
246
|
+
"""Gets Question ID from benchmark or prediction data record"""
|
|
247
|
+
id_keys = ["id", "question_id", "qid", "_id"]
|
|
248
|
+
for key in id_keys:
|
|
249
|
+
question_id = record.get(key)
|
|
250
|
+
if question_id is not None:
|
|
251
|
+
record["id"] = question_id # Ensure 'id' is always set
|
|
252
|
+
return question_id
|
|
253
|
+
raise ValueError(f"Record has no ID field among {id_keys}: {record}")
|
|
254
|
+
|
|
255
|
+
|
|
256
|
+
def get_utterance(record):
|
|
257
|
+
"""Gets the question (utterance) from the benchmark or prediction data record"""
|
|
258
|
+
utterance_keys = ["utterance", "page_content", "question"]
|
|
259
|
+
for key in utterance_keys:
|
|
260
|
+
utterance = record.get(key)
|
|
261
|
+
if utterance:
|
|
262
|
+
record["utterance"] = utterance # Ensure 'utterance' is always set
|
|
263
|
+
return utterance
|
|
264
|
+
raise ValueError(f"Record has no utterance field among {utterance_keys}: {record}")
|
|
265
|
+
|
|
266
|
+
|
|
267
|
+
def get_gt_sqls(record):
|
|
268
|
+
gt_sql_keys = ["sql", "SQL", "target", "query"]
|
|
269
|
+
for key in gt_sql_keys:
|
|
270
|
+
gt_sqls = record.get(key)
|
|
271
|
+
if gt_sqls:
|
|
272
|
+
# Skip if it's a dict (structured SQL representation, not a string)
|
|
273
|
+
if isinstance(gt_sqls, dict):
|
|
274
|
+
continue
|
|
275
|
+
if not isinstance(gt_sqls, list):
|
|
276
|
+
gt_sqls = [gt_sqls]
|
|
277
|
+
record["sql"] = gt_sqls
|
|
278
|
+
return gt_sqls
|
|
279
|
+
if "metadata" in record and "sql" in record["metadata"]:
|
|
280
|
+
return [record["metadata"]["sql"]]
|
|
281
|
+
raise ValueError(f"Record has no ground truth SQL: {record}")
|
|
282
|
+
|
|
283
|
+
|
|
284
|
+
def get_question(record):
|
|
285
|
+
return (
|
|
286
|
+
record["page_content"]
|
|
287
|
+
if "page_content" in record
|
|
288
|
+
else (record["question"] if "question" in record else record["utterance"])
|
|
289
|
+
)
|
|
290
|
+
|
|
291
|
+
|
|
292
|
+
def get_default_eval_filename(predictions_file):
|
|
293
|
+
base_name, ext = os.path.splitext(predictions_file)
|
|
294
|
+
return f"{base_name}_eval{ext}"
|
|
295
|
+
|
|
296
|
+
|
|
297
|
+
def add_summary_json_suffix(path: str) -> str:
|
|
298
|
+
return os.path.splitext(path)[0] + "_summary.json"
|
|
299
|
+
|
|
300
|
+
|
|
301
|
+
def add_summary_csv_suffix(path: str) -> str:
|
|
302
|
+
return os.path.splitext(path)[0] + "_summary.csv"
|