Deadman922 commited on
Commit
8993e47
·
verified ·
1 Parent(s): e654d43

Delete inference.py

Browse files
Files changed (1) hide show
  1. inference.py +0 -501
inference.py DELETED
@@ -1,501 +0,0 @@
1
- """Inference module for SQL Quality Environment.
2
-
3
- This module provides an LLM-based AI agent interface for solving SQL data quality tasks.
4
- It uses OpenAI-compatible API (Groq/HuggingFace) to generate SQL queries and fixes dynamically.
5
-
6
- MANDATORY:
7
- - Before submitting, ensure the following variables are defined in your environment configuration:
8
- API_BASE_URL The API endpoint for the LLM.
9
- MODEL_NAME The model identifier to use for inference.
10
- HF_TOKEN Your Hugging Face / API key.
11
- - The inference script must be named `inference.py` and placed in the root directory of the project
12
- - Participants must use OpenAI Client for all LLM calls using above variables
13
- """
14
-
15
- import json
16
- import os
17
- import sys
18
- from typing import Any, Dict, List, Optional
19
-
20
- import requests
21
- from openai import OpenAI
22
- try:
23
- from dotenv import load_dotenv
24
- except Exception: # pragma: no cover - optional dependency guard
25
- load_dotenv = None
26
-
27
- if load_dotenv:
28
- load_dotenv()
29
-
30
- # Configuration from environment
31
- MAX_STEPS = int(os.getenv("MAX_STEPS", "10"))
32
- API_BASE_URL = os.getenv("API_BASE_URL", "<your-active-endpoint>")
33
- MODEL_NAME = os.getenv("MODEL_NAME", "<your-active-model>")
34
- HF_TOKEN = os.getenv("HF_TOKEN")
35
- API_KEY = os.getenv("GROQ_API_KEY", "") or HF_TOKEN or os.getenv("OPENAI_API_KEY", "")
36
-
37
-
38
- # Structured logging utilities for validator parsing
39
- def log_start(episode_id: str, task_id: str, timestamp: float = None):
40
- ts = timestamp or __import__('time').time()
41
- print(f"START episode_id={episode_id} task_id={task_id} timestamp={ts}")
42
-
43
-
44
- def log_step(step: int, action: str, reward: float, score: float = None, sql: str = None, column: str = None):
45
- extras = []
46
- if sql:
47
- extras.append(f'sql="{sql[:50]}..."' if len(sql) > 50 else f'sql="{sql}"')
48
- if column:
49
- extras.append(f'column="{column}"')
50
- if score is not None:
51
- extras.append(f'score={score:.4f}')
52
- extra_str = " " + " ".join(extras) if extras else ""
53
- print(f"STEP step={step} action={action} reward={reward:.4f}{extra_str}")
54
-
55
-
56
- def log_end(episode_id: str, final_score: float, total_steps: int):
57
- print(f"END episode_id={episode_id} final_score={final_score:.4f} total_steps={total_steps}")
58
-
59
-
60
- class SQLQualityAgent:
61
- """LLM-based agent for solving SQL data quality tasks."""
62
-
63
- def __init__(self, base_url: str = "http://localhost:7860"):
64
- self.base_url = base_url.rstrip("/")
65
- self.task_id: Optional[str] = None
66
- self.episode_id: Optional[str] = None
67
- self.step_count = 0
68
- self.trajectory: List[Dict[str, Any]] = []
69
-
70
- # Initialize OpenAI-compatible client (works with Groq, HF, OpenAI)
71
- self.client = OpenAI(
72
- api_key=API_KEY,
73
- base_url=API_BASE_URL
74
- )
75
-
76
- def health_check(self) -> bool:
77
- """Check if the environment is running."""
78
- try:
79
- r = requests.get(f"{self.base_url}/health", timeout=5)
80
- return r.status_code == 200 and r.json().get("status") == "ok"
81
- except Exception:
82
- return False
83
-
84
- def reset(self, task_id: str, seed: Optional[int] = None) -> Dict[str, Any]:
85
- """Initialize a new episode for the given task."""
86
- self.task_id = task_id
87
- self.step_count = 0
88
- self.trajectory = []
89
- payload = {"task_id": task_id}
90
- if seed is not None:
91
- payload["seed"] = seed
92
-
93
- r = requests.post(f"{self.base_url}/reset", json=payload, timeout=30)
94
- r.raise_for_status()
95
- data = r.json()
96
- self.episode_id = data.get("episode_id")
97
- return data
98
-
99
- def query(self, sql: str) -> Dict[str, Any]:
100
- """Execute a SELECT query to investigate the data."""
101
- if not self.task_id:
102
- raise ValueError("No active task. Call reset() first.")
103
-
104
- self.step_count += 1
105
- if self.step_count > MAX_STEPS:
106
- raise RuntimeError(f"Exceeded MAX_STEPS ({MAX_STEPS})")
107
-
108
- r = requests.post(
109
- f"{self.base_url}/step",
110
- json={
111
- "task_id": self.task_id,
112
- "action_type": "query",
113
- "sql": sql,
114
- },
115
- timeout=30,
116
- )
117
- r.raise_for_status()
118
- result = r.json()
119
- self.trajectory.append({"type": "query", "sql": sql, "result": result})
120
- return result
121
-
122
- def profile(self, column: str) -> Dict[str, Any]:
123
- """Profile a column to get statistics."""
124
- if not self.task_id:
125
- raise ValueError("No active task. Call reset() first.")
126
-
127
- self.step_count += 1
128
- if self.step_count > MAX_STEPS:
129
- raise RuntimeError(f"Exceeded MAX_STEPS ({MAX_STEPS})")
130
-
131
- r = requests.post(
132
- f"{self.base_url}/step",
133
- json={
134
- "task_id": self.task_id,
135
- "action_type": "profile",
136
- "column_name": column,
137
- },
138
- timeout=30,
139
- )
140
- r.raise_for_status()
141
- result = r.json()
142
- self.trajectory.append({"type": "profile", "column": column, "result": result})
143
- return result
144
-
145
- def fix(self, fix_sql: str) -> Dict[str, Any]:
146
- """Apply a fix SQL (UPDATE/INSERT/DELETE)."""
147
- if not self.task_id:
148
- raise ValueError("No active task. Call reset() first.")
149
-
150
- self.step_count += 1
151
- if self.step_count > MAX_STEPS:
152
- raise RuntimeError(f"Exceeded MAX_STEPS ({MAX_STEPS})")
153
-
154
- r = requests.post(
155
- f"{self.base_url}/step",
156
- json={
157
- "task_id": self.task_id,
158
- "action_type": "fix",
159
- "fix_query": fix_sql,
160
- },
161
- timeout=30,
162
- )
163
- r.raise_for_status()
164
- result = r.json()
165
- self.trajectory.append({"type": "fix", "sql": fix_sql, "result": result})
166
- return result
167
-
168
- def submit(
169
- self,
170
- answer: Optional[str] = None,
171
- fix_query: Optional[str] = None,
172
- root_cause_table: Optional[str] = None,
173
- offending_key: Optional[str] = None,
174
- ) -> Dict[str, Any]:
175
- """Submit final answer to complete the episode."""
176
- if not self.task_id:
177
- raise ValueError("No active task. Call reset() first.")
178
-
179
- payload: Dict[str, Any] = {
180
- "task_id": self.task_id,
181
- "action_type": "submit",
182
- }
183
- if answer:
184
- payload["answer"] = answer
185
- if fix_query:
186
- payload["fix_query"] = fix_query
187
- if root_cause_table:
188
- payload["root_cause_table"] = root_cause_table
189
- if offending_key:
190
- payload["offending_key"] = offending_key
191
-
192
- r = requests.post(f"{self.base_url}/step", json=payload, timeout=30)
193
- r.raise_for_status()
194
- return r.json()
195
-
196
- def llm_generate(self, prompt: str, system_context: str = "") -> str:
197
- """Generate text using LLM via OpenAI-compatible API."""
198
- messages = [
199
- {"role": "system", "content": system_context or "You are a SQL data quality expert. Generate valid SQLite SQL queries. Reply with ONLY the SQL, no explanations."},
200
- {"role": "user", "content": prompt}
201
- ]
202
-
203
- try:
204
- response = self.client.chat.completions.create(
205
- model=MODEL_NAME,
206
- messages=messages,
207
- temperature=0.1,
208
- max_tokens=500
209
- )
210
- text = response.choices[0].message.content.strip()
211
- # Extract SQL from markdown if present
212
- if "```sql" in text:
213
- text = text.split("```sql")[1].split("```")[0].strip()
214
- elif "```" in text:
215
- text = text.split("```")[1].split("```")[0].strip()
216
- return text
217
- except Exception as e:
218
- print(f"LLM generation failed: {e}")
219
- return ""
220
-
221
- def llm_answer(self, prompt: str, system_context: str = "") -> str:
222
- """Generate a short answer using LLM."""
223
- messages = [
224
- {"role": "system", "content": system_context or "You are a data quality expert. Reply with ONLY the answer, no explanations."},
225
- {"role": "user", "content": prompt}
226
- ]
227
-
228
- try:
229
- response = self.client.chat.completions.create(
230
- model=MODEL_NAME,
231
- messages=messages,
232
- temperature=0.0,
233
- max_tokens=50
234
- )
235
- return response.choices[0].message.content.strip().strip('"').strip("'")
236
- except Exception as e:
237
- print(f"LLM answer failed: {e}")
238
- return ""
239
-
240
-
241
- def run_agent(task_id: str, base_url: str = "http://localhost:7860") -> Dict[str, Any]:
242
- """Run LLM-based agent on a task with multi-step investigation."""
243
- import time
244
- agent = SQLQualityAgent(base_url)
245
-
246
- if not agent.health_check():
247
- print("Error: Environment not available")
248
- sys.exit(1)
249
-
250
- # Initialize episode
251
- print(f"Initializing task: {task_id}")
252
- obs = agent.reset(task_id)
253
- print(f"Episode ID: {agent.episode_id}")
254
- log_start(agent.episode_id, task_id, time.time())
255
-
256
- # Get schema and task info
257
- schema = obs.get("schema_info", [])
258
- task_desc = obs.get("task_description", "")
259
-
260
- print(f"Task: {task_desc}")
261
- print(f"Schema: {json.dumps(schema, indent=2)}")
262
-
263
- schema_text = json.dumps(schema, indent=2)
264
-
265
- try:
266
- if task_id == "null_audit":
267
- # Step 1: Query null percentages for each column
268
- sql = agent.llm_generate(
269
- f"""Given this schema: {schema_text}
270
-
271
- Write a SQL query that counts NULL values for each column in the 'users' table
272
- and calculates the null percentage. Return column_name, null_count, and pct_null.
273
- Return only the SQL query."""
274
- )
275
- if sql:
276
- result = agent.query(sql)
277
- reward = result.get('reward', {}).get('step_reward', 0.0)
278
- log_step(1, "query", reward, sql=sql)
279
- print(f"Null analysis: {json.dumps(result.get('reward', {}), indent=2)}")
280
-
281
- # Step 2: Profile individual columns
282
- step_num = 2
283
- for col in ["city", "email", "age", "signup_date"]:
284
- profile_result = agent.profile(col)
285
- reward = profile_result.get('reward', {}).get('step_reward', 0.0)
286
- log_step(step_num, "profile", reward, column=col)
287
- step_num += 1
288
- print(f"Profile {col}: {profile_result.get('reward', {}).get('feedback', '')}")
289
-
290
- # Step 3: Ask LLM to identify the answer based on profile results
291
- trajectory_summary = "\n".join([
292
- f"Step {i+1}: {t.get('type', 'unknown')} - {t.get('result', {}).get('reward', {}).get('feedback', '')[:100]}"
293
- for i, t in enumerate(agent.trajectory)
294
- ])
295
-
296
- answer = agent.llm_answer(
297
- f"""Based on these investigation results:
298
- {trajectory_summary}
299
-
300
- Which column name in the 'users' table has a null rate exceeding 50%?
301
- Return ONLY the column name (e.g., 'city'), nothing else.""",
302
- "You identify the problematic column. Return ONLY the column name."
303
- )
304
- if not answer:
305
- answer = "city" # fallback
306
-
307
- print(f"Identified column: {answer}")
308
- submit_result = agent.submit(answer=answer.strip())
309
- final_score = submit_result.get('reward', {}).get('score', 0.0)
310
- log_step(agent.step_count, "submit", final_score, score=final_score)
311
- log_end(agent.episode_id, final_score, agent.step_count)
312
- return submit_result
313
-
314
- elif task_id == "type_repair":
315
- # Step 1: Inspect the table
316
- result = agent.query("SELECT typeof(amount), amount FROM transactions LIMIT 10")
317
- print(f"Type inspection: {result.get('reward', {}).get('feedback', '')[:100]}")
318
-
319
- # Step 2: Generate fix using LLM
320
- fix_sql = agent.llm_generate(
321
- f"""The 'transactions' table has an 'amount' column stored as TEXT with numeric values like '12.50'.
322
- Write a SQLite script to:
323
- 1. Create a new table with amount as REAL
324
- 2. Copy all data, casting amount to REAL
325
- 3. Drop the old table
326
- 4. Rename the new table
327
-
328
- Return only the SQL statements."""
329
- )
330
-
331
- if fix_sql:
332
- print(f"Generated fix: {fix_sql[:200]}")
333
- fix_result = agent.fix(fix_sql)
334
- print(f"Fix preview: {fix_result.get('reward', {}).get('feedback', '')}")
335
-
336
- # Step 3: Verify the fix
337
- verify = agent.query("SELECT typeof(amount), amount FROM transactions LIMIT 5")
338
- print(f"Verification: {verify.get('reward', {}).get('feedback', '')[:100]}")
339
-
340
- submit_result = agent.submit(fix_query=fix_sql)
341
- final_score = submit_result.get('reward', {}).get('score', 0.0)
342
- log_step(agent.step_count, "submit", final_score, score=final_score)
343
- log_end(agent.episode_id, final_score, agent.step_count)
344
- return submit_result
345
- else:
346
- # Fallback fix
347
- fallback = """
348
- CREATE TABLE transactions_fixed AS
349
- SELECT id, user_id, CAST(amount AS REAL) AS amount, description, category, date, transaction_date
350
- FROM transactions;
351
- DROP TABLE transactions;
352
- ALTER TABLE transactions_fixed RENAME TO transactions;
353
- """
354
- agent.fix(fallback)
355
- submit_result = agent.submit(fix_query=fallback)
356
- final_score = submit_result.get('reward', {}).get('score', 0.0)
357
- log_step(agent.step_count, "submit", final_score, score=final_score)
358
- log_end(agent.episode_id, final_score, agent.step_count)
359
- return submit_result
360
-
361
- elif task_id == "dedup_bug":
362
- # Step 1: Count duplicates
363
- result = agent.query(
364
- "SELECT order_id, COUNT(*) as cnt FROM order_items GROUP BY order_id HAVING cnt > 1 LIMIT 10"
365
- )
366
- print(f"Duplicates: {result.get('reward', {}).get('feedback', '')[:100]}")
367
-
368
- # Step 2: Generate dedup fix
369
- fix_sql = agent.llm_generate(
370
- f"""The 'order_items' table has duplicate rows for the same order_id.
371
- Write a SQLite DELETE statement to remove duplicate rows, keeping only the row
372
- with the smallest rowid for each order_id.
373
- Return only the SQL DELETE statement."""
374
- )
375
-
376
- if fix_sql:
377
- fix_result = agent.fix(fix_sql)
378
- print(f"Fix preview: {fix_result.get('reward', {}).get('feedback', '')}")
379
-
380
- # Verify
381
- agent.query("SELECT order_id, COUNT(*) as cnt FROM order_items GROUP BY order_id HAVING cnt > 1")
382
-
383
- submit_result = agent.submit(
384
- fix_query=fix_sql,
385
- root_cause_table="order_items",
386
- offending_key="order_id"
387
- )
388
- final_score = submit_result.get('reward', {}).get('score', 0.0)
389
- log_step(agent.step_count, "submit", final_score, score=final_score)
390
- log_end(agent.episode_id, final_score, agent.step_count)
391
- return submit_result
392
- else:
393
- fallback = "DELETE FROM order_items WHERE rowid NOT IN (SELECT MIN(rowid) FROM order_items GROUP BY order_id)"
394
- agent.fix(fallback)
395
- submit_result = agent.submit(
396
- fix_query=fallback,
397
- root_cause_table="order_items",
398
- offending_key="order_id"
399
- )
400
- final_score = submit_result.get('reward', {}).get('score', 0.0)
401
- log_step(agent.step_count, "submit", final_score, score=final_score)
402
- log_end(agent.episode_id, final_score, agent.step_count)
403
- return submit_result
404
-
405
- elif task_id == "fk_integrity":
406
- # Step 1: Find orphans
407
- result = agent.query(
408
- "SELECT COUNT(*) FROM invoices i LEFT JOIN customers c ON i.customer_id = c.id WHERE c.id IS NULL"
409
- )
410
- print(f"Orphans: {result.get('reward', {}).get('feedback', '')[:100]}")
411
-
412
- # Step 2: Generate fix
413
- fix_sql = agent.llm_generate(
414
- f"""The 'invoices' table has orphan records where customer_id doesn't match any id in the 'customers' table.
415
- Write a SQLite DELETE statement to remove these orphan invoices.
416
- Return only the SQL DELETE statement."""
417
- )
418
-
419
- if fix_sql:
420
- agent.fix(fix_sql)
421
- agent.query("SELECT COUNT(*) FROM invoices i LEFT JOIN customers c ON i.customer_id = c.id WHERE c.id IS NULL")
422
- submit_result = agent.submit(fix_query=fix_sql)
423
- final_score = submit_result.get('reward', {}).get('score', 0.0)
424
- log_step(agent.step_count, "submit", final_score, score=final_score)
425
- log_end(agent.episode_id, final_score, agent.step_count)
426
- return submit_result
427
- else:
428
- fallback = "DELETE FROM invoices WHERE customer_id NOT IN (SELECT id FROM customers)"
429
- agent.fix(fallback)
430
- submit_result = agent.submit(fix_query=fallback)
431
- final_score = submit_result.get('reward', {}).get('score', 0.0)
432
- log_step(agent.step_count, "submit", final_score, score=final_score)
433
- log_end(agent.episode_id, final_score, agent.step_count)
434
- return submit_result
435
-
436
- elif task_id == "schema_drift":
437
- # Step 1: Inspect both tables
438
- agent.query("PRAGMA table_info(events_v1)")
439
- agent.query("PRAGMA table_info(events_v2)")
440
-
441
- # Step 2: Generate normalization
442
- fix_sql = agent.llm_generate(
443
- f"""Two tables exist:
444
- - events_v1: id, user_id, event_name, amount_text (TEXT), event_time
445
- - events_v2: event_id, account_id, event_type, amount (REAL), event_ts, source
446
-
447
- Create a canonical_events table with columns: event_id, user_id, event_type, amount (REAL), event_time
448
- by merging both tables. Map:
449
- events_v1: id→event_id, user_id→user_id, event_name→event_type, CAST(amount_text AS REAL)→amount, event_time→event_time
450
- events_v2: event_id→event_id, account_id→user_id, event_type→event_type, amount→amount, event_ts→event_time
451
-
452
- Return only the CREATE TABLE AS and INSERT statements."""
453
- )
454
-
455
- if fix_sql:
456
- agent.fix(fix_sql)
457
- agent.query("SELECT COUNT(*) FROM canonical_events")
458
- submit_result = agent.submit(fix_query=fix_sql)
459
- final_score = submit_result.get('reward', {}).get('score', 0.0)
460
- log_step(agent.step_count, "submit", final_score, score=final_score)
461
- log_end(agent.episode_id, final_score, agent.step_count)
462
- return submit_result
463
- else:
464
- fallback = """
465
- CREATE TABLE canonical_events AS
466
- SELECT id AS event_id, user_id, event_name AS event_type, CAST(amount_text AS REAL) AS amount, event_time
467
- FROM events_v1
468
- UNION ALL
469
- SELECT event_id, account_id AS user_id, event_type, amount, event_ts AS event_time
470
- FROM events_v2;
471
- """
472
- agent.fix(fallback)
473
- submit_result = agent.submit(fix_query=fallback)
474
- final_score = submit_result.get('reward', {}).get('score', 0.0)
475
- log_step(agent.step_count, "submit", final_score, score=final_score)
476
- log_end(agent.episode_id, final_score, agent.step_count)
477
- return submit_result
478
-
479
- else:
480
- raise ValueError(f"Unknown task: {task_id}")
481
-
482
- except RuntimeError as e:
483
- print(f"Agent failed: {e}")
484
- result = agent.submit()
485
- final_score = result.get('reward', {}).get('score', 0.0)
486
- log_end(agent.episode_id, final_score, agent.step_count)
487
- return result
488
-
489
-
490
- if __name__ == "__main__":
491
- import argparse
492
-
493
- parser = argparse.ArgumentParser(description="Run SQL Quality Agent")
494
- parser.add_argument("--task", required=True, help="Task ID to run")
495
- parser.add_argument("--url", default="http://localhost:7860", help="Base URL")
496
- parser.add_argument("--seed", type=int, help="Random seed")
497
-
498
- args = parser.parse_args()
499
-
500
- result = run_agent(args.task, args.url)
501
- print(f"\nFinal result: {json.dumps(result, indent=2)}")