Deadman922 commited on
Commit
0997498
·
verified ·
1 Parent(s): e6bbaba

Upload inference.py

Browse files
Files changed (1) hide show
  1. server/inference.py +356 -0
server/inference.py ADDED
@@ -0,0 +1,356 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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 API to generate SQL queries and fixes dynamically.
5
+ """
6
+
7
+ import json
8
+ import os
9
+ import sys
10
+ from typing import Any, Dict, List, Optional
11
+
12
+ import requests
13
+ from openai import OpenAI
14
+
15
+ # Configuration from environment
16
+ MAX_STEPS = int(os.getenv("MAX_STEPS", "10"))
17
+ API_BASE_URL = os.getenv("API_BASE_URL", "https://api.openai.com/v1")
18
+ MODEL_NAME = os.getenv("MODEL_NAME", "gpt-4o-mini")
19
+ HF_TOKEN = os.getenv("HF_TOKEN", "")
20
+
21
+
22
+ class SQLQualityAgent:
23
+ """LLM-based agent for solving SQL data quality tasks."""
24
+
25
+ def __init__(self, base_url: str = "http://localhost:7860"):
26
+ self.base_url = base_url.rstrip("/")
27
+ self.task_id: Optional[str] = None
28
+ self.episode_id: Optional[str] = None
29
+ self.step_count = 0
30
+ self.trajectory: List[Dict[str, Any]] = []
31
+
32
+ # Initialize OpenAI client
33
+ api_key = HF_TOKEN or os.getenv("OPENAI_API_KEY", "")
34
+ self.client = OpenAI(
35
+ api_key=api_key,
36
+ base_url=API_BASE_URL if "openai" not in API_BASE_URL else None
37
+ )
38
+
39
+ def health_check(self) -> bool:
40
+ """Check if the environment is running."""
41
+ try:
42
+ r = requests.get(f"{self.base_url}/health", timeout=5)
43
+ return r.status_code == 200 and r.json().get("status") == "ok"
44
+ except Exception:
45
+ return False
46
+
47
+ def reset(self, task_id: str, seed: Optional[int] = None) -> Dict[str, Any]:
48
+ """Initialize a new episode for the given task."""
49
+ self.task_id = task_id
50
+ payload = {"task_id": task_id}
51
+ if seed is not None:
52
+ payload["seed"] = seed
53
+
54
+ r = requests.post(f"{self.base_url}/reset", json=payload, timeout=30)
55
+ r.raise_for_status()
56
+ data = r.json()
57
+ self.episode_id = data.get("episode_id")
58
+ return data
59
+
60
+ def query(self, sql: str) -> Dict[str, Any]:
61
+ """Execute a SELECT query to investigate the data."""
62
+ if not self.task_id:
63
+ raise ValueError("No active task. Call reset() first.")
64
+
65
+ self.step_count += 1
66
+ if self.step_count > MAX_STEPS:
67
+ raise RuntimeError(f"Exceeded MAX_STEPS ({MAX_STEPS})")
68
+
69
+ r = requests.post(
70
+ f"{self.base_url}/step",
71
+ json={
72
+ "task_id": self.task_id,
73
+ "action_type": "query",
74
+ "sql": sql,
75
+ },
76
+ timeout=30,
77
+ )
78
+ r.raise_for_status()
79
+ return r.json()
80
+
81
+ def profile(self, column: str) -> Dict[str, Any]:
82
+ """Profile a column to get statistics."""
83
+ if not self.task_id:
84
+ raise ValueError("No active task. Call reset() first.")
85
+
86
+ self.step_count += 1
87
+ if self.step_count > MAX_STEPS:
88
+ raise RuntimeError(f"Exceeded MAX_STEPS ({MAX_STEPS})")
89
+
90
+ r = requests.post(
91
+ f"{self.base_url}/step",
92
+ json={
93
+ "task_id": self.task_id,
94
+ "action_type": "profile",
95
+ "column_name": column,
96
+ },
97
+ timeout=30,
98
+ )
99
+ r.raise_for_status()
100
+ return r.json()
101
+
102
+ def fix(self, fix_sql: str) -> Dict[str, Any]:
103
+ """Apply a fix SQL (UPDATE/INSERT/DELETE)."""
104
+ if not self.task_id:
105
+ raise ValueError("No active task. Call reset() first.")
106
+
107
+ self.step_count += 1
108
+ if self.step_count > MAX_STEPS:
109
+ raise RuntimeError(f"Exceeded MAX_STEPS ({MAX_STEPS})")
110
+
111
+ r = requests.post(
112
+ f"{self.base_url}/step",
113
+ json={
114
+ "task_id": self.task_id,
115
+ "action_type": "fix",
116
+ "fix_query": fix_sql,
117
+ },
118
+ timeout=30,
119
+ )
120
+ r.raise_for_status()
121
+ return r.json()
122
+
123
+ def submit(
124
+ self,
125
+ answer: Optional[str] = None,
126
+ fix_query: Optional[str] = None,
127
+ root_cause_table: Optional[str] = None,
128
+ offending_key: Optional[str] = None,
129
+ ) -> Dict[str, Any]:
130
+ """Submit final answer to complete the episode."""
131
+ if not self.task_id:
132
+ raise ValueError("No active task. Call reset() first.")
133
+
134
+ payload: Dict[str, Any] = {
135
+ "task_id": self.task_id,
136
+ "action_type": "submit",
137
+ }
138
+ if answer:
139
+ payload["answer"] = answer
140
+ if fix_query:
141
+ payload["fix_query"] = fix_query
142
+ if root_cause_table:
143
+ payload["root_cause_table"] = root_cause_table
144
+ if offending_key:
145
+ payload["offending_key"] = offending_key
146
+
147
+ r = requests.post(f"{self.base_url}/step", json=payload, timeout=30)
148
+ r.raise_for_status()
149
+ return r.json()
150
+
151
+
152
+ def llm_generate_sql(self, prompt: str, system_context: str = "") -> str:
153
+ """Generate SQL using LLM."""
154
+ messages = [
155
+ {"role": "system", "content": system_context or "You are a SQL data quality expert. Generate valid SQLite SQL queries."},
156
+ {"role": "user", "content": prompt}
157
+ ]
158
+
159
+ try:
160
+ response = self.client.chat.completions.create(
161
+ model=MODEL_NAME,
162
+ messages=messages,
163
+ temperature=0.1,
164
+ max_tokens=500
165
+ )
166
+ sql = response.choices[0].message.content.strip()
167
+ # Extract SQL from markdown if present
168
+ if "```sql" in sql:
169
+ sql = sql.split("```sql")[1].split("```")[0].strip()
170
+ elif "```" in sql:
171
+ sql = sql.split("```")[1].split("```")[0].strip()
172
+ return sql
173
+ except Exception as e:
174
+ print(f"LLM generation failed: {e}")
175
+ return ""
176
+
177
+
178
+ def run_agent(task_id: str, base_url: str = "http://localhost:7860") -> Dict[str, Any]:
179
+ """Run LLM-based agent on a task."""
180
+ agent = SQLQualityAgent(base_url)
181
+
182
+ if not agent.health_check():
183
+ print("Error: Environment not available")
184
+ sys.exit(1)
185
+
186
+ # Initialize episode
187
+ print(f"Initializing task: {task_id}")
188
+ obs = agent.reset(task_id)
189
+ print(f"Episode ID: {agent.episode_id}")
190
+
191
+ # Get schema and task info
192
+ schema = obs.get("observation", {}).get("schema_info", [])
193
+ task_desc = obs.get("observation", {}).get("task_description", "")
194
+
195
+ print(f"Task: {task_desc}")
196
+ print(f"Schema: {json.dumps(schema, indent=2)}")
197
+
198
+ # Build context for LLM
199
+ schema_text = json.dumps(schema, indent=2)
200
+
201
+ try:
202
+ if task_id == "null_audit":
203
+ # Use LLM to find null percentages
204
+ prompt = f"""Given this schema: {schema_text}
205
+
206
+ Write a SQL query to find which column has the highest percentage of NULL values.
207
+ Return only the SQL query."""
208
+
209
+ sql = agent.llm_generate_sql(prompt, "You are a data quality expert.")
210
+ if sql:
211
+ result = agent.query(sql)
212
+ print(f"Query result: {result}")
213
+
214
+ # Ask LLM to identify the answer
215
+ answer_prompt = f"""Based on the query results above, which column name has null rate > 50%?
216
+ Return ONLY the column name (e.g., 'city'), nothing else."""
217
+
218
+ answer = agent.llm_generate_sql(answer_prompt, "You identify the problematic column.")
219
+ if not answer:
220
+ answer = "city" # fallback
221
+
222
+ submit_result = agent.submit(answer=answer.strip())
223
+ return submit_result
224
+
225
+ elif task_id == "type_repair":
226
+ # Check for type issues
227
+ prompt = f"""Given schema: {schema_text}
228
+
229
+ Task: {task_desc}
230
+ Write a SQL query to inspect the transactions table and identify type mismatches in the amount column.
231
+ Return only the SQL."""
232
+
233
+ sql = agent.llm_generate_sql(prompt)
234
+ if sql:
235
+ result = agent.query(sql)
236
+ print(f"Inspection result: {result}")
237
+
238
+ # Generate fix
239
+ fix_prompt = f"""Given the type mismatch in the amount column (stored as TEXT with '$' prefix),
240
+ write a SQLite UPDATE statement to convert it to REAL numeric values.
241
+ Return only the SQL UPDATE statement."""
242
+
243
+ fix_sql = agent.llm_generate_sql(fix_prompt, "You write data repair SQL.")
244
+ if fix_sql:
245
+ fix_result = agent.fix(fix_sql)
246
+ print(f"Fix result: {fix_result}")
247
+ submit_result = agent.submit(fix_query=fix_sql)
248
+ else:
249
+ submit_result = agent.submit()
250
+ return submit_result
251
+
252
+ elif task_id == "dedup_bug":
253
+ # Find duplicates
254
+ prompt = f"""Given schema: {schema_text}
255
+
256
+ Write a SQL query to find duplicate event_id values in the events table.
257
+ Return only the SQL."""
258
+
259
+ sql = agent.llm_generate_sql(prompt)
260
+ if sql:
261
+ result = agent.query(sql)
262
+ print(f"Duplicates found: {result}")
263
+
264
+ # Generate dedup fix
265
+ fix_prompt = f"""Write a SQLite DELETE statement to remove duplicate rows from the events table,
266
+ keeping only the first occurrence (minimum rowid) for each event_id.
267
+ Return only the SQL DELETE statement."""
268
+
269
+ fix_sql = agent.llm_generate_sql(fix_prompt)
270
+ if fix_sql:
271
+ fix_result = agent.fix(fix_sql)
272
+ print(f"Fix result: {fix_result}")
273
+ submit_result = agent.submit(
274
+ fix_query=fix_sql,
275
+ root_cause_table="events",
276
+ offending_key="event_id"
277
+ )
278
+ else:
279
+ submit_result = agent.submit()
280
+ return submit_result
281
+
282
+ elif task_id == "fk_integrity":
283
+ # Find orphans
284
+ prompt = f"""Given schema with tables: invoices and invoice_items
285
+ Write a SQL query to count orphan invoice_items (items with invoice_id not in invoices table).
286
+ Return only the SQL."""
287
+
288
+ sql = agent.llm_generate_sql(prompt)
289
+ if sql:
290
+ result = agent.query(sql)
291
+ print(f"Orphan count: {result}")
292
+
293
+ # Generate orphan removal
294
+ fix_prompt = f"""Write a SQLite DELETE statement to remove orphan records from invoice_items table
295
+ where invoice_id does not exist in the invoices table.
296
+ Return only the SQL DELETE statement."""
297
+
298
+ fix_sql = agent.llm_generate_sql(fix_prompt)
299
+ if fix_sql:
300
+ fix_result = agent.fix(fix_sql)
301
+ print(f"Fix result: {fix_result}")
302
+ submit_result = agent.submit(fix_query=fix_sql)
303
+ else:
304
+ submit_result = agent.submit()
305
+ return submit_result
306
+
307
+ elif task_id == "schema_drift":
308
+ # Check schema
309
+ prompt = f"""Write a SQL query: PRAGMA table_info(raw_events)
310
+ Return only the SQL."""
311
+
312
+ sql = agent.llm_generate_sql(prompt)
313
+ if sql:
314
+ result = agent.query(sql)
315
+ print(f"Schema: {result}")
316
+
317
+ # Generate normalization
318
+ fix_prompt = f"""Given the raw_events table with columns: event_id, timestamp, user_id, value, event_type
319
+ Write a SQLite CREATE TABLE AS statement to create analytics_events with:
320
+ - event_id (preserved)
321
+ - timestamp cast to INTEGER
322
+ - user_id (preserved)
323
+ - value cast to REAL
324
+ - event_type (preserved)
325
+ Return only the SQL CREATE TABLE AS statement."""
326
+
327
+ fix_sql = agent.llm_generate_sql(fix_prompt)
328
+ if fix_sql:
329
+ fix_result = agent.fix(fix_sql)
330
+ print(f"Fix result: {fix_result}")
331
+ submit_result = agent.submit(fix_query=fix_sql)
332
+ else:
333
+ submit_result = agent.submit()
334
+ return submit_result
335
+
336
+ else:
337
+ raise ValueError(f"Unknown task: {task_id}")
338
+
339
+ except RuntimeError as e:
340
+ print(f"Agent failed: {e}")
341
+ # Submit empty answer to end episode
342
+ return agent.submit()
343
+
344
+
345
+ if __name__ == "__main__":
346
+ import argparse
347
+
348
+ parser = argparse.ArgumentParser(description="Run SQL Quality Agent")
349
+ parser.add_argument("--task", required=True, help="Task ID to run")
350
+ parser.add_argument("--url", default="http://localhost:7860", help="Base URL")
351
+ parser.add_argument("--seed", type=int, help="Random seed")
352
+
353
+ args = parser.parse_args()
354
+
355
+ result = run_agent(args.task, args.url)
356
+ print(f"\nFinal result: {json.dumps(result, indent=2)}")