multimodalart HF Staff commited on
Commit
c48f4e6
·
verified ·
1 Parent(s): 1b7dd08

TRUST-SQL agent demo: four-phase tool-integrated text-to-SQL over unknown schemas

Browse files
.gitattributes CHANGED
@@ -33,3 +33,8 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
33
  *.zip filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
 
 
 
 
 
 
33
  *.zip filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
36
+ databases/california_schools/california_schools.sqlite filter=lfs diff=lfs merge=lfs -text
37
+ databases/formula_1/formula_1.sqlite filter=lfs diff=lfs merge=lfs -text
38
+ databases/student_club/student_club.sqlite filter=lfs diff=lfs merge=lfs -text
39
+ databases/superhero/superhero.sqlite filter=lfs diff=lfs merge=lfs -text
40
+ databases/toxicology/toxicology.sqlite filter=lfs diff=lfs merge=lfs -text
README.md CHANGED
@@ -1,13 +1,65 @@
1
  ---
2
- title: Trust Sql Text2sql Demo
3
- emoji: 🚀
4
- colorFrom: pink
5
- colorTo: green
6
  sdk: gradio
7
  sdk_version: 6.27.0
8
- python_version: '3.12'
9
  app_file: app.py
 
 
10
  pinned: false
 
 
 
 
11
  ---
12
 
13
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  ---
2
+ title: TRUST-SQL
3
+ emoji: 🔎
4
+ colorFrom: gray
5
+ colorTo: indigo
6
  sdk: gradio
7
  sdk_version: 6.27.0
 
8
  app_file: app.py
9
+ python_version: "3.12"
10
+ startup_duration_timeout: 1h
11
  pinned: false
12
+ license: apache-2.0
13
+ short_description: Text-to-SQL over unknown schemas, via tool use
14
+ models:
15
+ - AIJian/TrustSQL-8B
16
  ---
17
 
18
+ # TRUST-SQL — Text-to-SQL over *unknown* schemas
19
+
20
+ Demo of [`AIJian/TrustSQL-8B`](https://huggingface.co/AIJian/TrustSQL-8B), from
21
+ [**TRUST-SQL: Tool-Integrated Multi-Turn Reinforcement Learning for Text-to-SQL over Unknown Schemas**](https://huggingface.co/papers/2603.16448)
22
+ (Jian et al., 2026). Code: [`JaneEyre0530/TrustSQL`](https://github.com/JaneEyre0530/TrustSQL).
23
+
24
+ Unlike ordinary text-to-SQL demos, **the schema is never put in the prompt**. The model is given
25
+ only the database *name*, the question, and a single read-only SQL tool, and has to discover the
26
+ schema itself by following the authors' four-phase action protocol:
27
+
28
+ 1. `explore_schema` — issue metadata queries (`PRAGMA`, `sqlite_master`, sampling rows)
29
+ 2. `propose_schema` — write down the tables/columns/joins it has actually verified
30
+ 3. `generate_sql` — draft the answer query and *execute* it to check it works
31
+ 4. `confirm_answer` — emit the final SQL
32
+
33
+ The agent may loop back at any point. The full trajectory (reasoning, tool calls, observations) is
34
+ streamed into the transcript so you can watch the schema being discovered.
35
+
36
+ ## Implementation notes
37
+
38
+ - The system prompt is `trustsql_eval/prompt_template.txt`, copied verbatim from the authors' repo.
39
+ - The user message reproduces the exact `**Task Configuration** / **Database Engine** /
40
+ **Database** / **External Knowledge** / **User Question**` format found in
41
+ [`AIJian/TrustSQL-data`](https://huggingface.co/datasets/AIJian/TrustSQL-data).
42
+ - The turn loop, progress prefixes, `<schema>` acknowledgement, malformed-output feedback and
43
+ observation truncation (2048 tokens) are ported from `trustsql_eval/message_processor.py`.
44
+ - Sampling defaults match `trustsql_eval/main.py` (temperature 0.7, top-p 0.9).
45
+ - Deviation: `WITH` is added to the `SELECT` / `PRAGMA` / `EXPLAIN` allow-list so CTE answers can be
46
+ executed. Every query still runs over a read-only SQLite connection.
47
+ - Runs on ZeroGPU with plain `transformers` generation rather than the vLLM path used for the
48
+ paper's benchmarks, so it is slower than the reported latency figures.
49
+
50
+ ## Credits & licensing
51
+
52
+ - Model and code: Apache-2.0, © the TRUST-SQL authors (Meituan / BUPT).
53
+ - Bundled sample databases and the example questions + `evidence` hints are from the
54
+ **BIRD** dev set ([bird-bench.github.io](https://bird-bench.github.io/)), licensed
55
+ **CC BY-SA 4.0**; SQLite files mirrored via
56
+ [`prem-research/birdbench`](https://huggingface.co/datasets/prem-research/birdbench).
57
+
58
+ ```bibtex
59
+ @article{jian2026trustsql,
60
+ title = {TRUST-SQL: Tool-Integrated Multi-Turn Reinforcement Learning for Text-to-SQL over Unknown Schemas},
61
+ author = {Jian, Ai and Zhang, Xiaoyun and Du, Wanrou and Ruan, Jingqing and Pei, Jiangbo and Zhang, Weipeng and Zeng, Ke and Cai, Xunliang},
62
+ journal= {arXiv preprint arXiv:2603.16448},
63
+ year = {2026}
64
+ }
65
+ ```
app.py ADDED
@@ -0,0 +1,726 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """TRUST-SQL — Text-to-SQL over *unknown* schemas, on ZeroGPU.
2
+
3
+ Faithful re-implementation of the four-phase tool-integrated agent loop from
4
+ `JaneEyre0530/TrustSQL` (`trustsql_eval/`): the model never sees the schema.
5
+ It must explore it with read-only metadata queries, propose a verified schema,
6
+ generate + execute a candidate SQL, and only then confirm the final answer.
7
+ """
8
+
9
+ import os
10
+
11
+ os.environ.setdefault("PYTORCH_CUDA_ALLOC_CONF", "expandable_segments:True")
12
+ os.environ.setdefault("TOKENIZERS_PARALLELISM", "false")
13
+
14
+ import spaces # noqa: E402 (must precede torch)
15
+
16
+ import json # noqa: E402
17
+ import re # noqa: E402
18
+ import sqlite3 # noqa: E402
19
+ import time # noqa: E402
20
+ from pathlib import Path # noqa: E402
21
+ from threading import Thread # noqa: E402
22
+
23
+ import gradio as gr # noqa: E402
24
+ import pandas as pd # noqa: E402
25
+ import torch # noqa: E402
26
+ from transformers import ( # noqa: E402
27
+ AutoModelForCausalLM,
28
+ AutoTokenizer,
29
+ StoppingCriteria,
30
+ StoppingCriteriaList,
31
+ TextIteratorStreamer,
32
+ )
33
+
34
+ # --------------------------------------------------------------------------------------
35
+ # Model
36
+ # --------------------------------------------------------------------------------------
37
+
38
+ MODEL_ID = "AIJian/TrustSQL-8B"
39
+ HERE = Path(__file__).parent
40
+ DB_ROOT = HERE / "databases"
41
+
42
+ # Exact system prompt shipped by the authors (trustsql_eval/prompt_template.txt).
43
+ SYSTEM_PROMPT = (HERE / "prompt_template.txt").read_text(encoding="utf-8").strip()
44
+
45
+ tokenizer = AutoTokenizer.from_pretrained(MODEL_ID)
46
+ model = AutoModelForCausalLM.from_pretrained(
47
+ MODEL_ID,
48
+ dtype=torch.bfloat16,
49
+ attn_implementation="sdpa",
50
+ ).to("cuda")
51
+ model.eval()
52
+
53
+ EOS_IDS = [151645, 151643] # <|im_end|>, <|endoftext|>
54
+ MAX_CONTEXT = 40960
55
+ MAX_OBS_TOKENS = 2048 # trustsql_eval default
56
+ SQL_TIMEOUT = 15.0
57
+ MAX_ROWS = 100 # trustsql_eval `_execute_sql_sync` default
58
+
59
+ # --------------------------------------------------------------------------------------
60
+ # Sample databases (BIRD-Dev, CC BY-SA 4.0)
61
+ # --------------------------------------------------------------------------------------
62
+
63
+ SAMPLE_DBS = {
64
+ "california_schools": "California public schools — SAT scores, free-meal rates (3 tables)",
65
+ "superhero": "Superhero attributes, powers, publishers (9 tables)",
66
+ "student_club": "University club members, events, budgets, expenses (8 tables)",
67
+ "toxicology": "Molecules, atoms, bonds and carcinogenicity labels (4 tables)",
68
+ "formula_1": "Formula 1 races, drivers, constructors, lap times (13 tables)",
69
+ }
70
+ DB_CHOICES = [f"{k} — {v}" for k, v in SAMPLE_DBS.items()]
71
+
72
+
73
+ def _db_id_from_choice(choice: str) -> str:
74
+ return (choice or DB_CHOICES[0]).split(" — ")[0].strip()
75
+
76
+
77
+ def _resolve_db(db_choice: str, uploaded_db):
78
+ """Return (db_id, sqlite_path). An uploaded file always wins."""
79
+ if uploaded_db:
80
+ path = uploaded_db if isinstance(uploaded_db, str) else getattr(uploaded_db, "name", None)
81
+ if path and os.path.exists(path):
82
+ return Path(path).stem, path
83
+ db_id = _db_id_from_choice(db_choice)
84
+ return db_id, str(DB_ROOT / db_id / f"{db_id}.sqlite")
85
+
86
+
87
+ # --------------------------------------------------------------------------------------
88
+ # The one tool the agent gets: read-only SQL execution
89
+ # --------------------------------------------------------------------------------------
90
+
91
+ ALLOWED_SQL_PREFIXES = ("SELECT", "PRAGMA", "EXPLAIN", "WITH")
92
+
93
+
94
+ def _strip_sql_comments(sql: str) -> str:
95
+ s = sql.strip()
96
+ while s.startswith("--") or s.startswith("/*"):
97
+ if s.startswith("--"):
98
+ nl = s.find("\n")
99
+ if nl == -1:
100
+ return ""
101
+ s = s[nl + 1 :].strip()
102
+ else:
103
+ end = s.find("*/")
104
+ if end == -1:
105
+ return ""
106
+ s = s[end + 2 :].strip()
107
+ return s
108
+
109
+
110
+ def _is_readonly(sql: str):
111
+ s = _strip_sql_comments(sql)
112
+ if not s:
113
+ return False, "Empty SQL query"
114
+ if s.upper().startswith(ALLOWED_SQL_PREFIXES):
115
+ return True, None
116
+ return False, f"SQL must start with {ALLOWED_SQL_PREFIXES}, got: {s.split()[0]}"
117
+
118
+
119
+ def run_sql(db_path: str, sql: str, max_rows: int = MAX_ROWS):
120
+ """Execute read-only SQL. Returns (text_result, column_names, rows)."""
121
+ ok, err = _is_readonly(sql)
122
+ if not ok:
123
+ return f"Error: {err}", [], []
124
+ if not os.path.exists(db_path):
125
+ return f"Error: Database file not found: {db_path}", [], []
126
+ conn = None
127
+ try:
128
+ conn = sqlite3.connect(f"file:{db_path}?mode=ro", uri=True, check_same_thread=False)
129
+ conn.execute(f"PRAGMA busy_timeout = {int(SQL_TIMEOUT * 1000)}")
130
+ cur = conn.cursor()
131
+ cur.execute(sql)
132
+ rows = cur.fetchall()
133
+ if not rows:
134
+ return "Query executed successfully. No results returned.", (
135
+ [d[0] for d in cur.description] if cur.description else []
136
+ ), []
137
+ cols = [d[0] for d in cur.description]
138
+ lines = ["\t".join(cols)]
139
+ for i, row in enumerate(rows):
140
+ if i >= max_rows:
141
+ lines.append(f"... ({len(rows) - max_rows} more rows)")
142
+ break
143
+ lines.append("\t".join("NULL" if v is None else str(v) for v in row))
144
+ return "\n".join(lines), cols, rows[:max_rows]
145
+ except sqlite3.Error as e:
146
+ return f"Error: SQLite error: {e}", [], []
147
+ except Exception as e: # pragma: no cover
148
+ return f"Error: Unexpected error: {e}", [], []
149
+ finally:
150
+ if conn is not None:
151
+ try:
152
+ conn.close()
153
+ except Exception:
154
+ pass
155
+
156
+
157
+ def db_schema_preview(db_choice: str, uploaded_db=None) -> str:
158
+ """Human-readable DDL dump of a database (for the UI only — never shown to the model)."""
159
+ db_id, path = _resolve_db(db_choice, uploaded_db)
160
+ if not os.path.exists(path):
161
+ return f"-- database `{db_id}` not found"
162
+ text, _, rows = run_sql(
163
+ path,
164
+ "SELECT name, sql FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%'",
165
+ max_rows=200,
166
+ )
167
+ if not rows:
168
+ return f"-- `{db_id}`: {text}"
169
+ out = [f"-- database: {db_id} ({len(rows)} tables)", ""]
170
+ for name, ddl in rows:
171
+ out.append((ddl or f"-- {name}").strip() + ";")
172
+ out.append("")
173
+ return "\n".join(out)
174
+
175
+
176
+ # --------------------------------------------------------------------------------------
177
+ # Prompt construction (matches AIJian/TrustSQL-data + trustsql_eval/prompt_builders.py)
178
+ # --------------------------------------------------------------------------------------
179
+
180
+
181
+ def build_user_message(db_id: str, question: str, external_knowledge: str) -> str:
182
+ parts = ["", "**Task Configuration**", "**Database Engine:** SQLite", f"**Database:** {db_id}"]
183
+ if external_knowledge and external_knowledge.strip():
184
+ parts.append(f"**External Knowledge:** {external_knowledge.strip()}")
185
+ parts.append(f"**User Question:** {question.strip()}?")
186
+ parts.append("")
187
+ return "\n".join(parts)
188
+
189
+
190
+ def progress_prefix(current_round: int, max_rounds: int) -> str:
191
+ """Verbatim port of MessageProcessor._format_progress_prefix."""
192
+ base = f"This is turn {current_round + 1} of {max_rounds}.\n\n"
193
+ remaining = max_rounds - (current_round + 1)
194
+ if remaining == 0:
195
+ return ""
196
+ if remaining == 1:
197
+ return base + (
198
+ "Only 1 turn remaining after this.\n"
199
+ "You MUST provide the final answer in the next turn.\n\n"
200
+ "Use <action>confirm_answer</action> with your best SQL query.\n"
201
+ "If you don't have a complete solution, provide your best attempt.\n\n"
202
+ )
203
+ if remaining == 2:
204
+ return base + ("Only 2 turns remaining after this.\nStart preparing your final SQL query.\n\n")
205
+ return base
206
+
207
+
208
+ FORMAT_HELP = (
209
+ "Invalid format detected. Your response is missing required components.\n\n"
210
+ "Option 1: EXPLORE SCHEMA\n"
211
+ "Purpose: Investigate database structure\n"
212
+ "Required format:\n"
213
+ "<think>Your reasoning process</think>\n"
214
+ "<action>explore_schema</action>\n"
215
+ '<tool_call>{"name": "execute_sql_query", "arguments": {"db_id": "...", "sql": "..."}}</tool_call>\n\n'
216
+ "Option 2: PROPOSE SCHEMA\n"
217
+ "Purpose: Document your understanding of relevant tables and columns\n"
218
+ "Required format:\n"
219
+ "<think>Your reasoning process</think>\n"
220
+ "<action>propose_schema</action>\n"
221
+ '<schema>{"tables": [...], "columns": {...}}</schema>\n\n'
222
+ "Option 3: GENERATE SQL\n"
223
+ "Purpose: Create SQL query and VERIFY it works by executing\n"
224
+ "<think>Your reasoning process</think>\n"
225
+ "<action>generate_sql</action>\n"
226
+ '<tool_call>{"name": "execute_sql_query", "arguments": {"db_id": "...", "sql": "..."}}</tool_call>\n\n'
227
+ "Option 4: FINAL ANSWER\n"
228
+ "Purpose: Provide verified SQL query as final result\n"
229
+ "ONLY use this AFTER successfully executing and verifying your SQL.\n"
230
+ "Required format:\n"
231
+ "<think>Your reasoning process</think>\n"
232
+ "<action>confirm_answer</action>\n"
233
+ "<answer>```sql\nYOUR_SQL\n```</answer>\n\n"
234
+ )
235
+
236
+
237
+ def fix_tool_tag(content: str) -> str:
238
+ content = re.sub(r"<tool>(.*?)</tool>", r"<tool_call>\1</tool_call>", content, flags=re.S)
239
+ content = re.sub(r"<tools>(.*?)</tools>", r"<tool_call>\1</tool_call>", content, flags=re.S)
240
+ return content
241
+
242
+
243
+ def extract_tag(text: str, tag: str):
244
+ m = re.search(rf"<{tag}>(.*?)</{tag}>", text, re.S | re.I)
245
+ return m.group(1) if m else None
246
+
247
+
248
+ def extract_final_sql(answer_body: str) -> str:
249
+ for pat in (r"```sql\s*(.*?)```", r"'''sql\s*(.*?)'''", r"```\s*(.*?)```", r"'''\s*(.*?)'''"):
250
+ m = re.search(pat, answer_body, re.S | re.I)
251
+ if m:
252
+ return m.group(1).strip()
253
+ return answer_body.strip()
254
+
255
+
256
+ def truncate_observation(text: str, max_tokens: int = MAX_OBS_TOKENS) -> str:
257
+ ids = tokenizer(text, add_special_tokens=False)["input_ids"]
258
+ if len(ids) <= max_tokens:
259
+ return text
260
+ return tokenizer.decode(ids[:max_tokens]) + "\n... (result truncated due to length)"
261
+
262
+
263
+ # --------------------------------------------------------------------------------------
264
+ # Pretty-printing a turn for the chat transcript
265
+ # --------------------------------------------------------------------------------------
266
+
267
+ ACTION_ICON = {
268
+ "explore_schema": "🔍",
269
+ "propose_schema": "📋",
270
+ "generate_sql": "🛠️",
271
+ "confirm_answer": "✅",
272
+ }
273
+
274
+
275
+ def _fence(text: str, lang: str = "") -> str:
276
+ return f"```{lang}\n{str(text).replace('```', '`` `')}\n```"
277
+
278
+
279
+ def _quote(text: str) -> str:
280
+ text = text.strip()
281
+ return "\n".join("> " + line for line in text.splitlines()) if text else ""
282
+
283
+
284
+ def render_assistant(raw: str) -> str:
285
+ body = fix_tool_tag(raw)
286
+ think = extract_tag(body, "think")
287
+ action = (extract_tag(body, "action") or "").strip().lower()
288
+ blocks = []
289
+ if action:
290
+ blocks.append(f"### {ACTION_ICON.get(action, '⚙️')} `{action}`")
291
+ elif not think:
292
+ blocks.append("### ⚙️ raw response")
293
+ if think:
294
+ blocks.append("💭 **Reasoning**\n\n" + _quote(think))
295
+
296
+ schema = extract_tag(body, "schema")
297
+ if schema is not None:
298
+ try:
299
+ pretty = json.dumps(json.loads(schema), indent=2)
300
+ except Exception:
301
+ pretty = schema.strip()
302
+ blocks.append("**Proposed schema**\n\n" + _fence(pretty, "json"))
303
+
304
+ answer = extract_tag(body, "answer")
305
+ if answer is not None:
306
+ blocks.append("**Final SQL**\n\n" + _fence(extract_final_sql(answer), "sql"))
307
+
308
+ tool_call = extract_tag(body, "tool_call")
309
+ if tool_call is not None:
310
+ sql = None
311
+ try:
312
+ payload = json.loads(tool_call.strip())
313
+ sql = (payload.get("arguments") or {}).get("sql")
314
+ except Exception:
315
+ pass
316
+ if sql:
317
+ blocks.append("**Tool call** · `execute_sql_query`\n\n" + _fence(sql, "sql"))
318
+ else:
319
+ blocks.append("**Tool call**\n\n" + _fence(tool_call.strip(), "json"))
320
+
321
+ if not blocks:
322
+ return _fence(raw)
323
+ return "\n\n".join(blocks)
324
+
325
+
326
+ def render_observation(text: str) -> str:
327
+ return "📥 **Observation**\n\n" + _fence(text)
328
+
329
+
330
+ # --------------------------------------------------------------------------------------
331
+ # Generation
332
+ # --------------------------------------------------------------------------------------
333
+
334
+
335
+ class _Deadline(StoppingCriteria):
336
+ def __init__(self, deadline: float):
337
+ self.deadline = deadline
338
+
339
+ def __call__(self, input_ids, scores, **kwargs) -> bool:
340
+ return time.time() > self.deadline
341
+
342
+
343
+ def stream_turn(messages, max_new_tokens: int, temperature: float, top_p: float, deadline: float):
344
+ """Yield incremental text for one assistant turn; last yield is the full turn."""
345
+ prompt = tokenizer.apply_chat_template(messages, tokenize=False, add_generation_prompt=True)
346
+ enc = tokenizer(prompt, return_tensors="pt", add_special_tokens=False)
347
+ n_in = enc["input_ids"].shape[-1]
348
+ budget = max(64, min(int(max_new_tokens), MAX_CONTEXT - n_in - 8))
349
+ enc = {k: v.to(model.device) for k, v in enc.items()}
350
+
351
+ streamer = TextIteratorStreamer(tokenizer, skip_prompt=True, skip_special_tokens=True)
352
+ do_sample = float(temperature) > 0.0
353
+ kwargs = dict(
354
+ **enc,
355
+ streamer=streamer,
356
+ max_new_tokens=budget,
357
+ do_sample=do_sample,
358
+ eos_token_id=EOS_IDS,
359
+ pad_token_id=151643,
360
+ stopping_criteria=StoppingCriteriaList([_Deadline(deadline)]),
361
+ )
362
+ if do_sample:
363
+ kwargs.update(temperature=float(temperature), top_p=float(top_p), top_k=20)
364
+
365
+ thread = Thread(target=model.generate, kwargs=kwargs)
366
+ thread.start()
367
+ acc = ""
368
+ last = 0.0
369
+ for chunk in streamer:
370
+ acc += chunk
371
+ now = time.time()
372
+ if now - last > 0.25:
373
+ last = now
374
+ yield acc, False
375
+ thread.join()
376
+ yield acc.strip(), True
377
+
378
+
379
+ def _estimate_duration(*args, **kwargs) -> int:
380
+ max_turns = 8
381
+ if len(args) >= 5:
382
+ try:
383
+ max_turns = int(args[4])
384
+ except Exception:
385
+ pass
386
+ return int(min(280, 40 + max_turns * 22))
387
+
388
+
389
+ # --------------------------------------------------------------------------------------
390
+ # The agent loop
391
+ # --------------------------------------------------------------------------------------
392
+
393
+
394
+ @spaces.GPU(duration=_estimate_duration)
395
+ def run_agent(
396
+ question: str,
397
+ db_choice: str = DB_CHOICES[0],
398
+ external_knowledge: str = "",
399
+ uploaded_db=None,
400
+ max_turns: int = 8,
401
+ max_new_tokens: int = 1536,
402
+ temperature: float = 0.7,
403
+ top_p: float = 0.9,
404
+ ):
405
+ """Run the TRUST-SQL agent on an unknown SQLite database and return the final SQL.
406
+
407
+ The model receives only the database *name* and the question — never the schema.
408
+ It explores metadata with read-only queries, proposes a verified schema, executes a
409
+ candidate query, and confirms the final SQL.
410
+
411
+ Args:
412
+ question: the natural-language question to answer.
413
+ db_choice: which bundled BIRD-Dev sample database to query.
414
+ external_knowledge: optional domain hint / evidence string (BIRD "evidence" field).
415
+ uploaded_db: optional path to a user-supplied SQLite file; overrides `db_choice`.
416
+ max_turns: maximum agent turns before giving up.
417
+ max_new_tokens: token budget per agent turn.
418
+ temperature: sampling temperature; 0 means greedy decoding.
419
+ top_p: nucleus sampling cutoff.
420
+ """
421
+ t0 = time.time()
422
+ budget = _estimate_duration(question, db_choice, external_knowledge, uploaded_db, max_turns)
423
+ deadline = t0 + budget - 18
424
+
425
+ max_turns = int(max_turns)
426
+ db_id, db_path = _resolve_db(db_choice, uploaded_db)
427
+
428
+ user_msg = build_user_message(db_id, question or "", external_knowledge or "")
429
+ messages = [
430
+ {"role": "system", "content": SYSTEM_PROMPT},
431
+ {"role": "user", "content": user_msg},
432
+ ]
433
+ chat = [{"role": "user", "content": f"**Question**\n\n{question}\n\n_Database: `{db_id}` (schema unknown to the model)_"}]
434
+ empty_df = pd.DataFrame()
435
+
436
+ if not (question or "").strip():
437
+ yield chat + [{"role": "assistant", "content": "Please enter a question."}], "", empty_df, "⚠️ No question provided."
438
+ return
439
+ if not os.path.exists(db_path):
440
+ yield chat, "", empty_df, f"❌ Database not found: `{db_path}`"
441
+ return
442
+
443
+ yield chat, "", empty_df, f"⏳ Turn 1/{max_turns} — exploring `{db_id}`…"
444
+
445
+ final_sql = ""
446
+ status = ""
447
+ for turn in range(max_turns):
448
+ if time.time() > deadline:
449
+ status = f"⏱️ Stopped after {turn} turn(s): GPU time budget reached."
450
+ break
451
+
452
+ base = list(chat)
453
+ raw = ""
454
+ for text, done in stream_turn(messages, max_new_tokens, temperature, top_p, deadline):
455
+ raw = text
456
+ chat = base + [
457
+ {
458
+ "role": "assistant",
459
+ "content": (render_assistant(text) if done else _fence(text)),
460
+ }
461
+ ]
462
+ yield chat, final_sql, empty_df, f"⏳ Turn {turn + 1}/{max_turns} — generating…"
463
+
464
+ if not raw:
465
+ status = "❌ The model returned an empty response."
466
+ break
467
+
468
+ raw = fix_tool_tag(raw)
469
+
470
+ # ---- confirm_answer -> terminate -------------------------------------------
471
+ answer = extract_tag(raw, "answer")
472
+ if answer is not None:
473
+ messages.append({"role": "assistant", "content": raw})
474
+ final_sql = extract_final_sql(answer)
475
+ status = f"✅ Confirmed after {turn + 1} turn(s)."
476
+ break
477
+
478
+ messages.append({"role": "assistant", "content": raw})
479
+ prefix = progress_prefix(turn, max_turns)
480
+
481
+ # ---- propose_schema -> acknowledgement --------------------------------------
482
+ schema = extract_tag(raw, "schema")
483
+ if schema is not None:
484
+ try:
485
+ data = json.loads(schema)
486
+ tables = data.get("tables", []) or []
487
+ cols = data.get("columns", {}) or {}
488
+ n_cols = sum(len(v) for v in cols.values()) if isinstance(cols, dict) else len(cols)
489
+ feedback = (
490
+ prefix
491
+ + f"Schema acknowledged: {len(tables)} table(s), {n_cols} column(s). "
492
+ "You may now proceed to generate SQL.\n"
493
+ )
494
+ except Exception:
495
+ feedback = prefix + "Schema acknowledged. You may proceed to generate SQL.\n"
496
+ messages.append({"role": "user", "content": feedback})
497
+ chat = chat + [{"role": "user", "content": render_observation(feedback)}]
498
+ yield chat, final_sql, empty_df, f"⏳ Turn {turn + 2}/{max_turns}…"
499
+ continue
500
+
501
+ # ---- tool call -> execute ----------------------------------------------------
502
+ tool_call = extract_tag(raw, "tool_call")
503
+ obs = None
504
+ if tool_call is None:
505
+ obs = prefix + FORMAT_HELP
506
+ else:
507
+ try:
508
+ payload = json.loads(tool_call.strip())
509
+ name = payload.get("name", "")
510
+ arguments = payload.get("arguments", {}) or {}
511
+ if name != "execute_sql_query":
512
+ obs = prefix + f"Error: Unknown function: {name}"
513
+ elif not str(arguments.get("sql", "")).strip():
514
+ obs = prefix + "Error: SQL query is empty"
515
+ else:
516
+ result, _, _ = run_sql(db_path, arguments["sql"])
517
+ obs = prefix + truncate_observation(result)
518
+ except json.JSONDecodeError as e:
519
+ obs = (
520
+ prefix
521
+ + f"Tool call parsing error:\nJSON parsing failed at line {e.lineno}, column {e.colno}: {e.msg}\n\n"
522
+ "Please fix the JSON format and try again.\n\n"
523
+ "Required format:\n"
524
+ '<tool_call>{"name": "execute_sql_query", "arguments": {"db_id": "...", "sql": "..."}}</tool_call>\n'
525
+ )
526
+ except Exception as e: # pragma: no cover
527
+ obs = prefix + f"Error: Tool execution error: {e}"
528
+
529
+ messages.append({"role": "user", "content": obs})
530
+ chat = chat + [{"role": "user", "content": render_observation(obs)}]
531
+ yield chat, final_sql, empty_df, f"⏳ Turn {turn + 2}/{max_turns}…"
532
+ else:
533
+ status = f"⚠️ Reached the {max_turns}-turn limit without a confirmed answer."
534
+
535
+ # ---- execute the confirmed SQL for display --------------------------------------
536
+ df = empty_df
537
+ if final_sql:
538
+ text, cols, rows = run_sql(db_path, final_sql, max_rows=100)
539
+ if cols and rows:
540
+ df = pd.DataFrame(rows, columns=cols)
541
+ elif cols:
542
+ df = pd.DataFrame(columns=cols)
543
+ else:
544
+ chat = chat + [{"role": "assistant", "content": "⚠️ Final SQL did not execute:\n\n" + _fence(text)}]
545
+ else:
546
+ status = status or "⚠️ No SQL was confirmed."
547
+
548
+ elapsed = time.time() - t0
549
+ yield chat, final_sql, df, f"{status} · {elapsed:.0f}s on GPU"
550
+
551
+
552
+ # --------------------------------------------------------------------------------------
553
+ # UI
554
+ # --------------------------------------------------------------------------------------
555
+
556
+ EXAMPLES = [
557
+ [
558
+ "What is the highest eligible free rate for K-12 students in the schools in Alameda County?",
559
+ DB_CHOICES[0],
560
+ "Eligible free rate for K-12 = `Free Meal Count (K-12)` / `Enrollment (K-12)`",
561
+ ],
562
+ [
563
+ "Among the schools with the SAT test takers of over 500, please list the schools that are magnet schools or offer a magnet program.",
564
+ DB_CHOICES[0],
565
+ "Magnet schools or offer a magnet program means that Magnet = 1",
566
+ ],
567
+ [
568
+ "How many superheroes have blue eyes?",
569
+ DB_CHOICES[1],
570
+ "blue eyes refers to colour = 'Blue' and eye_colour_id = colour.id",
571
+ ],
572
+ [
573
+ "Please list all the superpowers of 3-D Man.",
574
+ DB_CHOICES[1],
575
+ "3-D Man refers to superhero_name = '3-D Man'; superpowers refers to power_name",
576
+ ],
577
+ [
578
+ "What is the event that has the highest attendance of the students from the Student_Club?",
579
+ DB_CHOICES[2],
580
+ "event with highest attendance refers to MAX(COUNT(link_to_event))",
581
+ ],
582
+ [
583
+ "In the non-carcinogenic molecules, how many contain chlorine atoms?",
584
+ DB_CHOICES[3],
585
+ "non-carcinogenic molecules refers to label = '-'; chlorine atoms refers to element = 'cl'",
586
+ ],
587
+ [
588
+ "Please give the name of the race held on the circuits in Germany.",
589
+ DB_CHOICES[4],
590
+ "Germany is a name of country;",
591
+ ],
592
+ ]
593
+
594
+ CSS = """
595
+ #col-container { max-width: 1180px; margin: 0 auto; }
596
+ .dark .gradio-container { color: var(--body-text-color); }
597
+ """
598
+
599
+ INTRO = """# 🔎 TRUST-SQL — Text-to-SQL over **unknown** schemas
600
+
601
+ [`AIJian/TrustSQL-8B`](https://huggingface.co/AIJian/TrustSQL-8B) · [paper](https://huggingface.co/papers/2603.16448) · [code](https://github.com/JaneEyre0530/TrustSQL)
602
+
603
+ The schema is **not** in the prompt. The agent gets one tool — read-only SQL — and has to discover
604
+ the database itself, following the authors' four-phase protocol:
605
+ `explore_schema → propose_schema → generate_sql → confirm_answer`.
606
+ """
607
+
608
+ with gr.Blocks(theme=gr.themes.Citrus(), css=CSS, title="TRUST-SQL") as demo:
609
+ with gr.Column(elem_id="col-container"):
610
+ gr.Markdown(INTRO)
611
+
612
+ with gr.Row():
613
+ with gr.Column(scale=3):
614
+ question = gr.Textbox(
615
+ label="Question",
616
+ placeholder="e.g. Which school has the highest average SAT math score?",
617
+ lines=2,
618
+ )
619
+ with gr.Column(scale=1, min_width=140):
620
+ run_btn = gr.Button("Run agent", variant="primary", size="lg")
621
+
622
+ with gr.Row():
623
+ db_choice = gr.Dropdown(
624
+ label="Database (BIRD-Dev sample)",
625
+ choices=DB_CHOICES,
626
+ value=DB_CHOICES[0],
627
+ scale=2,
628
+ )
629
+ external_knowledge = gr.Textbox(
630
+ label="External knowledge (optional hint)",
631
+ placeholder="e.g. charter schools refers to `Charter School (Y/N)` = 1",
632
+ lines=1,
633
+ scale=3,
634
+ )
635
+
636
+ status = gr.Markdown("")
637
+
638
+ with gr.Row():
639
+ with gr.Column(scale=3):
640
+ chatbot = gr.Chatbot(
641
+ label="Agent trajectory",
642
+ type="messages",
643
+ height=620,
644
+ show_copy_button=True,
645
+ )
646
+ with gr.Column(scale=2):
647
+ final_sql = gr.Code(label="Confirmed SQL", language="sql", lines=8)
648
+ result_df = gr.Dataframe(label="Execution result", wrap=True)
649
+
650
+ with gr.Accordion("Peek at the database (the agent never sees this)", open=False):
651
+ schema_box = gr.Code(label="DDL", language="sql", lines=14)
652
+ peek_btn = gr.Button("Show schema", size="sm")
653
+
654
+ with gr.Accordion("Advanced settings", open=False):
655
+ uploaded_db = gr.File(
656
+ label="Use your own SQLite database (.sqlite / .db) — overrides the dropdown",
657
+ file_types=[".sqlite", ".db", ".sqlite3"],
658
+ type="filepath",
659
+ )
660
+ with gr.Row():
661
+ max_turns = gr.Slider(3, 12, value=8, step=1, label="Max agent turns")
662
+ max_new_tokens = gr.Slider(256, 3072, value=1536, step=128, label="Max new tokens / turn")
663
+ with gr.Row():
664
+ temperature = gr.Slider(0.0, 1.0, value=0.7, step=0.05, label="Temperature (0 = greedy)")
665
+ top_p = gr.Slider(0.1, 1.0, value=0.9, step=0.05, label="Top-p")
666
+ gr.Markdown(
667
+ "Defaults mirror `trustsql_eval` (temperature 0.7 / top-p 0.9). "
668
+ "Only `SELECT` / `PRAGMA` / `EXPLAIN` / `WITH` statements are ever executed, "
669
+ "against a read-only connection."
670
+ )
671
+
672
+ gr.Examples(
673
+ examples=EXAMPLES,
674
+ inputs=[question, db_choice, external_knowledge],
675
+ outputs=[chatbot, final_sql, result_df, status],
676
+ fn=run_agent,
677
+ cache_examples=True,
678
+ cache_mode="lazy",
679
+ label="BIRD-Dev examples (question + official evidence hint)",
680
+ )
681
+
682
+ gr.Markdown(
683
+ "Sample databases are the **BIRD-Dev** SQLite databases "
684
+ "([BIRD-SQL](https://bird-bench.github.io/), CC BY-SA 4.0); the example questions and "
685
+ "hints are their official dev-set questions and `evidence` strings. "
686
+ "The system prompt and agent loop are ported verbatim from "
687
+ "[`JaneEyre0530/TrustSQL`](https://github.com/JaneEyre0530/TrustSQL) (Apache-2.0)."
688
+ )
689
+
690
+ run_btn.click(
691
+ fn=run_agent,
692
+ inputs=[
693
+ question,
694
+ db_choice,
695
+ external_knowledge,
696
+ uploaded_db,
697
+ max_turns,
698
+ max_new_tokens,
699
+ temperature,
700
+ top_p,
701
+ ],
702
+ outputs=[chatbot, final_sql, result_df, status],
703
+ api_name="run_agent",
704
+ )
705
+ question.submit(
706
+ fn=run_agent,
707
+ inputs=[
708
+ question,
709
+ db_choice,
710
+ external_knowledge,
711
+ uploaded_db,
712
+ max_turns,
713
+ max_new_tokens,
714
+ temperature,
715
+ top_p,
716
+ ],
717
+ outputs=[chatbot, final_sql, result_df, status],
718
+ api_name=False,
719
+ )
720
+ peek_btn.click(
721
+ fn=db_schema_preview, inputs=[db_choice, uploaded_db], outputs=schema_box, api_name="schema"
722
+ )
723
+ db_choice.change(fn=db_schema_preview, inputs=[db_choice, uploaded_db], outputs=schema_box, api_name=False)
724
+
725
+ if __name__ == "__main__":
726
+ demo.launch(mcp_server=True)
databases/california_schools/california_schools.sqlite ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:986817d793479801ed55133e55aa27e335422c0cd3866b54a3d6317b7c5f09c1
3
+ size 11116544
databases/formula_1/formula_1.sqlite ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:17185981cd747f6cdc374cb02a6096db3130e6ec2ddc582fe1686a28fb4c4c8a
3
+ size 22360064
databases/student_club/student_club.sqlite ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:eb89bcfe97eefa386a27904ec5aa15159811a7eac894ec659a36e48fa9f76b77
3
+ size 2641920
databases/superhero/superhero.sqlite ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:75e94a2c3236ee3bb2c01fb97a1c4b4c1c269bcefd4eab1d04be323d2d0825b1
3
+ size 237568
databases/toxicology/toxicology.sqlite ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:f5fa7f21af1ad878ff8fef1b0582b8cb2d7ed63dbac65ff16d2ba05667650c5b
3
+ size 2678784
prompt_template.txt ADDED
@@ -0,0 +1,127 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Role
2
+ You are an expert SQL assistant working on an *unknown* database.
3
+ You must **never hallucinate** tables or columns.
4
+ All schema knowledge MUST come from metadata queries only.
5
+ You must operate strictly through the **Action Protocol**.
6
+
7
+ # Action Protocol
8
+ You must follow this sequence (can loop back if needed):
9
+ 1. **explore_schema** - Query database metadata
10
+ 2. **propose_schema** - Document verified schema
11
+ 3. **generate_sql** - Create SQL query
12
+ 4. **confirm_answer** - Output final SQL
13
+
14
+ ## ACTION: explore_schema
15
+ Used to query database metadata (tables, columns, foreign keys, etc.).
16
+ - Only metadata queries allowed.
17
+ - No user-intent SQL here.
18
+ - Verify relationships between tables when multi-table queries are needed
19
+
20
+ ## ACTION: propose_schema
21
+ Used to output the current verified schema knowledge.
22
+ - Include ONLY tables/columns actually verified through explore_schema
23
+ - Do NOT hallucinate or assume any unverified structures
24
+ - `joins` is optional; include only when relationships are explicitly verified
25
+ - Supports both single-table and multi-table structures
26
+
27
+ ## ACTION: generate_sql
28
+ Used to generate the SQL answer based on the latest <schema>.
29
+ - Use ONLY verified schema from propose_schema
30
+ - If required tables/columns are missing, switch back to explore_schema or propose_schema in the next message
31
+ - SQL must be syntactically valid and executable
32
+ - Consider query optimization (indexes, joins, filters)
33
+ - Validate the SQL logic matches user intent
34
+
35
+ ## ACTION: confirm_answer
36
+ Used when you have validated the generated SQL and confirmed it meets user requirements.
37
+ - Execute this action ONLY after generate_sql validation
38
+ - You MUST NOT return or describe any query results.
39
+ - You MUST NOT output anything other than SQL inside <answer>.
40
+ - The final output must be ONLY the SQL query in proper format
41
+
42
+ # Output Format
43
+ EVERY response must follow this exact structure:
44
+ <think>[Your reasoning process here]</think>
45
+ <action>[one of: explore_schema | propose_schema | generate_sql | confirm_answer]</action>
46
+ [Action-specific content below]
47
+
48
+ **explore_schema**
49
+ <think>reasoning</think>
50
+ <action>explore_schema</action>
51
+ <tool_call>
52
+ [Your metadata query]
53
+ </tool_call>
54
+
55
+ **propose_schema**
56
+ <think>reasoning</think>
57
+ <action>propose_schema</action>
58
+ <schema>
59
+ {
60
+ "tables": ["tableA"],
61
+ "columns": { "tableA": ["col1", "col2"] },
62
+ "joins": []
63
+ }
64
+ </schema>
65
+
66
+ Example 1: Single Table
67
+ <think>User asks about employee information. I've verified the employees table has id, name, department, and salary columns through explore_schema.</think>
68
+ <action>propose_schema</action>
69
+ <schema>
70
+ {
71
+ "tables": ["employees"],
72
+ "columns": {
73
+ "employees": ["id", "name", "department", "salary"]
74
+ },
75
+ "joins": []
76
+ }
77
+ </schema>
78
+
79
+ Example 2: Multiple Tables with Joins
80
+ <think>User asks about orders with customer details. I've verified both tables and their relationship: orders.customer_id references customers.id.</think>
81
+ <action>propose_schema</action>
82
+ <schema>
83
+ {
84
+ "tables": ["customers", "orders"],
85
+ "columns": {
86
+ "customers": ["id", "name", "email", "city"],
87
+ "orders": ["order_id", "customer_id", "order_date", "total_amount"]
88
+ },
89
+ "joins": [
90
+ {
91
+ "left_table": "customers",
92
+ "right_table": "orders",
93
+ "on": "customers.id = orders.customer_id",
94
+ "type": "INNER"
95
+ }
96
+ ]
97
+ }
98
+ </schema>
99
+
100
+ **generate_sql**
101
+ <think>reasoning</think>
102
+ <action>generate_sql</action>
103
+ <tool_call>
104
+ [Your SQL query]
105
+ </tool_call>
106
+
107
+ **confirm_answer**
108
+ <think>reasoning</think>
109
+ <action>confirm_answer</action>
110
+ <answer>
111
+ '''sql
112
+ ONLY the final SQL query - no explanations
113
+ '''
114
+ </answer>
115
+
116
+ # Tools
117
+
118
+ You may call one or more functions to assist with the user query.
119
+
120
+ <tools>
121
+ {"type": "function", "function": {"name": "execute_sql_query", "description": "Execute SQL query and return partial results containing column names.(maximum 30 records)", "parameters": {"type": "object", "properties": {"db_id": {"type": "string", "description": "The name of the database to query", "title": "Db ID"}, "sql": {"type": "string", "description": "The SQL query to execute", "title": "Sql"}}, "required": ["db_id", "sql"]}}}
122
+ </tools>
123
+
124
+ For each function call, return a json object with function name and arguments within <tool_call></tool_call> XML tags:
125
+ <tool_call>
126
+ {"name": <function-name>, "arguments": <args-json-object>}
127
+ </tool_call>
requirements.txt ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ transformers>=4.56.0
2
+ accelerate
3
+ pandas