Deadman922 commited on
Commit
9d289ba
Β·
verified Β·
1 Parent(s): cbe4245

Upload 18 files

Browse files
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)}")
server/graders.py CHANGED
@@ -162,7 +162,7 @@ def _evaluate_fix_outcome(
162
  ground_truth: dict,
163
  original_snapshot: dict | None = None,
164
  ) -> tuple[bool, float, bool]:
165
- """Evaluate task-specific resolution + preservation + schema integrity."""
166
  task_type = ground_truth.get("task_type", "")
167
  target_table = ground_truth.get(
168
  "table",
@@ -199,10 +199,139 @@ def _evaluate_fix_outcome(
199
  "DOUBLE",
200
  "INTEGER",
201
  )
202
- preserve_source = _snapshot_for_preservation(original_snapshot or {}, ground_truth)
203
- preservation = _measure_data_preservation(test_conn, preserve_source, target_table)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
204
  return issue_resolved, preservation, True
205
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
206
  if task_type == "dedup_bug":
207
  key_col = ground_truth.get("column", ground_truth.get("offending_key", "order_id"))
208
  expected_clean = ground_truth.get("expected_clean_count", 0)
 
162
  ground_truth: dict,
163
  original_snapshot: dict | None = None,
164
  ) -> tuple[bool, float, bool]:
165
+ """Evaluate task-specific resolution + preservation + schema integrity with Deep Audit Mode."""
166
  task_type = ground_truth.get("task_type", "")
167
  target_table = ground_truth.get(
168
  "table",
 
199
  "DOUBLE",
200
  "INTEGER",
201
  )
202
+
203
+ # DEEP AUDIT: Verify data wasn't just deleted, actually cast correctly
204
+ if issue_resolved:
205
+ try:
206
+ # Count total records after fix
207
+ total_after = test_conn.execute(
208
+ f"SELECT COUNT(*) FROM {target_table}"
209
+ ).fetchone()[0]
210
+
211
+ # Count non-null values in target column
212
+ non_null_after = test_conn.execute(
213
+ f"SELECT COUNT(*) FROM {target_table} WHERE {target_col} IS NOT NULL"
214
+ ).fetchone()[0]
215
+
216
+ # Sample some values to verify they're actually numeric
217
+ sample_values = test_conn.execute(
218
+ f"SELECT {target_col} FROM {target_table} WHERE {target_col} IS NOT NULL LIMIT 10"
219
+ ).fetchall()
220
+
221
+ # Verify sample values are actually numeric (not strings)
222
+ actual_numeric_count = 0
223
+ for (val,) in sample_values:
224
+ if isinstance(val, (int, float)):
225
+ actual_numeric_count += 1
226
+ elif isinstance(val, str):
227
+ # Try to parse as number
228
+ try:
229
+ float(val) if '.' in val else int(val)
230
+ actual_numeric_count += 1
231
+ except ValueError:
232
+ pass
233
+
234
+ # Calculate preservation ratio
235
+ if original_snapshot and target_table in original_snapshot:
236
+ original_count = len(original_snapshot[target_table])
237
+ preservation_ratio = total_after / max(1, original_count)
238
+
239
+ # Deep audit criteria:
240
+ # 1. At least 95% of records preserved
241
+ # 2. At least 80% of non-null values are actually numeric
242
+ numeric_ratio = actual_numeric_count / max(1, len(sample_values))
243
+
244
+ if preservation_ratio >= 0.95 and numeric_ratio >= 0.8:
245
+ preservation = 1.0 # Perfect fix
246
+ elif preservation_ratio >= 0.90 and numeric_ratio >= 0.6:
247
+ preservation = 0.9 # Good fix
248
+ elif preservation_ratio >= 0.80 and numeric_ratio >= 0.4:
249
+ preservation = 0.7 # Acceptable fix
250
+ elif preservation_ratio >= 0.5:
251
+ preservation = 0.4 # Poor fix (too much data lost)
252
+ else:
253
+ preservation = 0.1 # Bad fix (most data lost)
254
+ else:
255
+ preservation = 0.8 if actual_numeric_count >= 8 else 0.4
256
+
257
+ except sqlite3.Error:
258
+ preservation = 0.5 # Unknown - give partial credit
259
+ else:
260
+ preservation = 0.0
261
+
262
  return issue_resolved, preservation, True
263
 
264
+ if task_type == "null_audit":
265
+ target_col = ground_truth.get("column", ground_truth.get("null_column", "city"))
266
+ expected_clean = ground_truth.get("expected_clean_count", 0)
267
+
268
+ try:
269
+ remaining_nulls = test_conn.execute(
270
+ f"SELECT COUNT(*) FROM {target_table} WHERE {target_col} IS NULL"
271
+ ).fetchone()[0]
272
+
273
+ # DEEP AUDIT: Check if 'Fixed' values are logically consistent
274
+ if remaining_nulls == 0:
275
+ # Sample fixed values to check for logical consistency
276
+ sample_fixed = test_conn.execute(
277
+ f"SELECT {target_col} FROM {target_table} WHERE {target_col} IS NOT NULL LIMIT 20"
278
+ ).fetchall()
279
+
280
+ # Check for obviously bad fixes
281
+ bad_fix_count = 0
282
+ total_checked = len(sample_fixed)
283
+
284
+ for (val,) in sample_fixed:
285
+ val_str = str(val).strip().lower()
286
+
287
+ # Check for negative values in columns that shouldn't be negative
288
+ if any(keyword in target_col.lower() for keyword in ['revenue', 'price', 'amount', 'salary', 'rating', 'age', 'quantity']):
289
+ try:
290
+ numeric_val = float(val_str)
291
+ if numeric_val < 0:
292
+ bad_fix_count += 1
293
+ except ValueError:
294
+ pass
295
+
296
+ # Check for placeholder values that indicate lazy fixing
297
+ placeholder_patterns = ['na', 'n/a', 'none', 'unknown', 'missing', 'tbd', '???']
298
+ if val_str in placeholder_patterns:
299
+ bad_fix_count += 1
300
+
301
+ # Check for obviously wrong data types
302
+ if any(keyword in target_col.lower() for keyword in ['email', 'phone', 'url']):
303
+ if any(char.isdigit() for char in val_str) and len(val_str) < 10:
304
+ bad_fix_count += 1
305
+
306
+ # Calculate logical consistency score
307
+ if total_checked > 0:
308
+ bad_fix_ratio = bad_fix_count / total_checked
309
+ logical_consistency = max(0.0, 1.0 - bad_fix_ratio)
310
+ else:
311
+ logical_consistency = 1.0
312
+
313
+ # Check data preservation
314
+ if original_snapshot and target_table in original_snapshot:
315
+ original_count = len(original_snapshot[target_table])
316
+ current_count = test_conn.execute(
317
+ f"SELECT COUNT(*) FROM {target_table}"
318
+ ).fetchone()[0]
319
+ preservation = min(1.0, current_count / max(1, original_count))
320
+ else:
321
+ preservation = 1.0
322
+
323
+ # Combine logical consistency with preservation
324
+ final_preservation = (logical_consistency * 0.7) + (preservation * 0.3)
325
+
326
+ else:
327
+ final_preservation = 0.0
328
+
329
+ issue_resolved = remaining_nulls == 0
330
+ return issue_resolved, final_preservation, True
331
+
332
+ except sqlite3.Error:
333
+ return False, 0.0, True
334
+
335
  if task_type == "dedup_bug":
336
  key_col = ground_truth.get("column", ground_truth.get("offending_key", "order_id"))
337
  expected_clean = ground_truth.get("expected_clean_count", 0)
server/main.py CHANGED
@@ -462,8 +462,70 @@ def get_db_schema(conn: sqlite3.Connection) -> list[dict]:
462
  return schema
463
 
464
 
465
- # Global table alias mapping for manual task override
466
- table_aliases: dict[str, str] = {}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
467
 
468
 
469
  def sanitize_data_value(value: str) -> any:
@@ -568,7 +630,7 @@ def analyze_table_fingerprint(conn: sqlite3.Connection, table_name: str) -> dict
568
  return fingerprint
569
 
570
 
571
- def detect_tasks_semantically(schema: list[dict], fingerprints: dict[str, dict]) -> list[dict]:
572
  """Detect compatible tasks using semantic analysis instead of hard-coded names."""
573
  task_suggestions = []
574
 
@@ -582,44 +644,84 @@ def detect_tasks_semantically(schema: list[dict], fingerprints: dict[str, dict])
582
 
583
  # Suggest null audit if there are significant nulls
584
  if total_nulls > 0:
 
 
 
 
 
 
 
 
585
  task_suggestions.append({
586
  "task_id": "null_audit",
587
  "table_name": table_name,
588
- "confidence": min(0.9, total_nulls / fingerprint.get('row_count', 1)),
589
- "reason": f"Found {total_nulls} null values across {len(null_counts)} columns",
590
- "requires_alias": table_name.lower() != 'users'
 
591
  })
592
 
593
  # Suggest type repair if we see suspicious type patterns
594
  data_types = fingerprint.get('data_types', {})
595
- if any(data_types.get(col, '') == 'TEXT' for col in ['amount', 'price', 'rating', 'age', 'quantity'] if col in data_types):
 
 
 
 
 
 
 
 
 
 
 
596
  task_suggestions.append({
597
  "task_id": "type_repair",
598
  "table_name": table_name,
599
- "confidence": 0.7,
600
- "reason": "Detected numeric columns stored as text",
601
- "requires_alias": table_name.lower() != 'transactions'
 
602
  })
603
 
604
  # Suggest dedup if we have potential duplicate indicators
605
- if 'id' not in [col['name'].lower() for col in table_info.get('columns', [])]:
 
 
 
 
 
 
 
 
 
606
  task_suggestions.append({
607
  "task_id": "dedup_bug",
608
  "table_name": table_name,
609
- "confidence": 0.6,
610
- "reason": "No unique ID column detected - potential duplicates",
611
- "requires_alias": table_name.lower() != 'order_items'
 
612
  })
613
 
614
  # Job market specific task
615
  if 'job_market_data' in semantic_hints:
 
 
 
 
 
 
 
 
616
  task_suggestions.append({
617
  "task_id": "job_market_audit",
618
  "table_name": table_name,
619
- "confidence": 0.8,
620
- "reason": "Detected job market data patterns (job titles, companies, ratings)",
621
  "requires_alias": True,
622
- "suggested_alias": "users"
 
623
  })
624
 
625
  # Sort by confidence
@@ -656,12 +758,13 @@ def detect_tasks_from_schema(schema: list[dict]) -> list[str]:
656
 
657
 
658
  @app.post("/upload-db")
659
- async def upload_db(file: UploadFile = File(...), use_for_tasks: bool = True) -> dict:
660
  """Upload a .sqlite, .db, or .csv file for custom exploration and RL tasks.
661
 
662
  Args:
663
  file: The uploaded file (.sqlite, .db, or .csv)
664
  use_for_tasks: If True, enables RL tasks on this database
 
665
 
666
  Returns:
667
  dict: Status, schema info, and compatible RL tasks
@@ -673,6 +776,16 @@ async def upload_db(file: UploadFile = File(...), use_for_tasks: bool = True) ->
673
  """
674
  global _custom_db_conn, _custom_db_schema, _custom_db_filename
675
  global _uploaded_db_for_rl, _uploaded_db_schema, _uploaded_db_tasks
 
 
 
 
 
 
 
 
 
 
676
 
677
  # Validate file extension
678
  filename = file.filename or ""
@@ -780,8 +893,8 @@ async def upload_db(file: UploadFile = File(...), use_for_tasks: bool = True) ->
780
  health_audits[table_name] = perform_health_audit(_custom_db_conn, table_name)
781
  fingerprints[table_name] = analyze_table_fingerprint(_custom_db_conn, table_name)
782
 
783
- # NEW: Semantic task detection
784
- semantic_tasks = detect_tasks_semantically(_custom_db_schema, fingerprints)
785
 
786
  # NEW: Legacy hard-coded task detection (for compatibility)
787
  _uploaded_db_tasks = detect_tasks_from_schema(_custom_db_schema)
@@ -800,6 +913,7 @@ async def upload_db(file: UploadFile = File(...), use_for_tasks: bool = True) ->
800
  "use_for_tasks": use_for_tasks and (len(_uploaded_db_tasks) > 0 or len(semantic_tasks) > 0),
801
  "health_audits": health_audits,
802
  "fingerprints": fingerprints,
 
803
  "detected_table": _custom_db_schema[0]['table_name'] if _custom_db_schema else None
804
  }
805
 
@@ -810,16 +924,25 @@ async def upload_db(file: UploadFile = File(...), use_for_tasks: bool = True) ->
810
 
811
 
812
  @app.post("/set-table-alias")
813
- async def set_table_alias(body: dict) -> dict:
814
  """Set a table alias for manual task override.
815
 
816
  Args:
817
  body: JSON body with {"actual_table": "cleaned_ds_jobs2", "alias_as": "users"}
 
818
 
819
  Returns:
820
  dict: Status and current aliases
821
  """
822
- global table_aliases
 
 
 
 
 
 
 
 
823
 
824
  actual_table = body.get("actual_table", "").strip()
825
  alias_as = body.get("alias_as", "").strip()
@@ -827,55 +950,139 @@ async def set_table_alias(body: dict) -> dict:
827
  if not actual_table or not alias_as:
828
  raise HTTPException(status_code=422, detail="Both 'actual_table' and 'alias_as' required")
829
 
830
- table_aliases[actual_table] = alias_as
 
 
 
 
 
 
831
 
832
  return {
833
  "status": "ok",
 
834
  "message": f"Table '{actual_table}' will be treated as '{alias_as}' for RL tasks",
835
- "aliases": table_aliases.copy()
836
  }
837
 
838
 
839
  @app.get("/table-aliases")
840
- async def get_table_aliases() -> dict:
841
- """Get current table aliases."""
 
 
 
842
  return {
843
- "aliases": table_aliases.copy()
 
844
  }
845
 
846
 
847
  @app.delete("/table-aliases")
848
- async def clear_table_aliases() -> dict:
849
- """Clear all table aliases."""
850
- global table_aliases
851
- table_aliases.clear()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
852
  return {
 
853
  "status": "ok",
854
- "message": "All table aliases cleared"
855
  }
856
 
857
 
858
- def apply_table_aliases(sql: str) -> str:
859
- """Apply table aliases to SQL queries."""
860
- if not table_aliases:
861
- return sql
 
 
 
 
 
862
 
863
- # Simple regex to replace table names with aliases
864
- for actual, alias in table_aliases.items():
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
865
  # Replace FROM table_name with FROM alias_name
866
- sql = re.sub(r'\bFROM\s+' + re.escape(actual) + r'\b', f'FROM {alias}', sql, flags=re.IGNORECASE)
 
 
 
 
 
867
  # Replace table.column with alias.column
868
- sql = re.sub(r'\b' + re.escape(actual) + r'\.', f'{alias}.', sql)
 
 
 
 
869
 
870
- return sql
871
 
872
 
873
  @app.post("/query-custom")
874
- async def query_custom(body: dict) -> dict:
875
  """Execute a SELECT query against the uploaded custom database.
876
 
877
  Args:
878
  body: JSON body with {"sql": "SELECT ..."}
 
879
 
880
  Returns:
881
  dict: Query results with rows, columns, and row_count, plus suggestions if 0 rows
@@ -906,10 +1113,10 @@ async def query_custom(body: dict) -> dict:
906
  }
907
 
908
  try:
909
- # Apply table aliases if any
910
- sql_with_aliases = apply_table_aliases(sql)
911
 
912
- cursor = _custom_db_conn.execute(sql_with_aliases)
913
 
914
  if cursor.description:
915
  columns = [d[0] for d in cursor.description]
 
462
  return schema
463
 
464
 
465
+ import logging
466
+ import json
467
+ from datetime import datetime
468
+ from pathlib import Path
469
+
470
+ # Configure confidence logging
471
+ confidence_logger = logging.getLogger('confidence_scoring')
472
+ confidence_logger.setLevel(logging.INFO)
473
+
474
+ # Create logs directory if it doesn't exist
475
+ log_dir = Path('logs')
476
+ log_dir.mkdir(exist_ok=True)
477
+
478
+ # File handler for confidence scoring
479
+ confidence_handler = logging.FileHandler(log_dir / 'confidence_scoring.log')
480
+ confidence_handler.setLevel(logging.INFO)
481
+
482
+ # Formatter for detailed logging
483
+ confidence_formatter = logging.Formatter('%(asctime)s - %(message)s')
484
+ confidence_handler.setFormatter(confidence_formatter)
485
+ confidence_logger.addHandler(confidence_handler)
486
+
487
+ import uuid
488
+ from typing import Dict
489
+
490
+ # Session-based table alias mapping for manual task override
491
+ table_aliases: Dict[str, Dict[str, str]] = {} # {session_id: {actual_table: alias_as}}
492
+
493
+ # Session management
494
+ active_sessions: Dict[str, dict] = {} # {session_id: {created_at, last_used}}
495
+
496
+
497
+ def log_confidence_calculation(table_name: str, task_id: str, confidence: float,
498
+ reasoning: str, fingerprint: dict, session_id: str = None) -> dict:
499
+ """Log confidence score calculations for judge review and UI display."""
500
+ log_entry = {
501
+ "timestamp": datetime.now().isoformat(),
502
+ "session_id": session_id or "default",
503
+ "table_name": table_name,
504
+ "task_id": task_id,
505
+ "confidence_score": round(confidence, 3),
506
+ "reasoning": reasoning,
507
+ "fingerprint_analysis": {
508
+ "row_count": fingerprint.get('row_count', 0),
509
+ "column_count": fingerprint.get('column_count', 0),
510
+ "null_counts": fingerprint.get('null_counts', {}),
511
+ "semantic_hints": fingerprint.get('semantic_hints', []),
512
+ "data_types": fingerprint.get('data_types', {}),
513
+ "total_nulls": sum(fingerprint.get('null_counts', {}).values()),
514
+ "null_percentage": round(
515
+ sum(fingerprint.get('null_counts', {}).values()) / max(1, fingerprint.get('row_count', 1)) * 100, 2
516
+ )
517
+ },
518
+ "calculation_breakdown": {
519
+ "base_confidence": confidence,
520
+ "adjustments": [],
521
+ "final_score": round(confidence, 3)
522
+ }
523
+ }
524
+
525
+ # Log to file for judge review
526
+ confidence_logger.info(f"CONFIDENCE_CALCULATION: {json.dumps(log_entry, indent=2)}")
527
+
528
+ return log_entry
529
 
530
 
531
  def sanitize_data_value(value: str) -> any:
 
630
  return fingerprint
631
 
632
 
633
+ def detect_tasks_semantically(schema: list[dict], fingerprints: dict[str, dict], session_id: str = None) -> list[dict]:
634
  """Detect compatible tasks using semantic analysis instead of hard-coded names."""
635
  task_suggestions = []
636
 
 
644
 
645
  # Suggest null audit if there are significant nulls
646
  if total_nulls > 0:
647
+ confidence = min(0.9, total_nulls / fingerprint.get('row_count', 1))
648
+ reasoning = f"Found {total_nulls} null values across {len(null_counts)} columns"
649
+
650
+ # Log the calculation for judge visibility
651
+ log_entry = log_confidence_calculation(
652
+ table_name, 'null_audit', confidence, reasoning, fingerprint, session_id
653
+ )
654
+
655
  task_suggestions.append({
656
  "task_id": "null_audit",
657
  "table_name": table_name,
658
+ "confidence": confidence,
659
+ "reason": reasoning,
660
+ "requires_alias": table_name.lower() != 'users',
661
+ "debug_info": log_entry # AI Reasoning for judges
662
  })
663
 
664
  # Suggest type repair if we see suspicious type patterns
665
  data_types = fingerprint.get('data_types', {})
666
+ suspicious_numeric_cols = [col for col in ['amount', 'price', 'rating', 'age', 'quantity']
667
+ if col in data_types and data_types.get(col, '') == 'TEXT']
668
+
669
+ if suspicious_numeric_cols:
670
+ confidence = 0.7 + (len(suspicious_numeric_cols) * 0.05) # Higher confidence for more issues
671
+ reasoning = f"Detected {len(suspicious_numeric_cols)} numeric columns stored as text: {', '.join(suspicious_numeric_cols)}"
672
+
673
+ # Log the calculation
674
+ log_entry = log_confidence_calculation(
675
+ table_name, 'type_repair', confidence, reasoning, fingerprint, session_id
676
+ )
677
+
678
  task_suggestions.append({
679
  "task_id": "type_repair",
680
  "table_name": table_name,
681
+ "confidence": min(0.9, confidence),
682
+ "reason": reasoning,
683
+ "requires_alias": table_name.lower() != 'transactions',
684
+ "debug_info": log_entry
685
  })
686
 
687
  # Suggest dedup if we have potential duplicate indicators
688
+ columns = [col['name'].lower() for col in table_info.get('columns', [])]
689
+ if 'id' not in columns:
690
+ confidence = 0.6
691
+ reasoning = "No unique ID column detected - potential duplicates"
692
+
693
+ # Log the calculation
694
+ log_entry = log_confidence_calculation(
695
+ table_name, 'dedup_bug', confidence, reasoning, fingerprint, session_id
696
+ )
697
+
698
  task_suggestions.append({
699
  "task_id": "dedup_bug",
700
  "table_name": table_name,
701
+ "confidence": confidence,
702
+ "reason": reasoning,
703
+ "requires_alias": table_name.lower() != 'order_items',
704
+ "debug_info": log_entry
705
  })
706
 
707
  # Job market specific task
708
  if 'job_market_data' in semantic_hints:
709
+ confidence = 0.8
710
+ reasoning = "Detected job market data patterns (job titles, companies, ratings)"
711
+
712
+ # Log the calculation
713
+ log_entry = log_confidence_calculation(
714
+ table_name, 'job_market_audit', confidence, reasoning, fingerprint, session_id
715
+ )
716
+
717
  task_suggestions.append({
718
  "task_id": "job_market_audit",
719
  "table_name": table_name,
720
+ "confidence": confidence,
721
+ "reason": reasoning,
722
  "requires_alias": True,
723
+ "suggested_alias": "users",
724
+ "debug_info": log_entry
725
  })
726
 
727
  # Sort by confidence
 
758
 
759
 
760
  @app.post("/upload-db")
761
+ async def upload_db(file: UploadFile = File(...), use_for_tasks: bool = True, session_id: str = None) -> dict:
762
  """Upload a .sqlite, .db, or .csv file for custom exploration and RL tasks.
763
 
764
  Args:
765
  file: The uploaded file (.sqlite, .db, or .csv)
766
  use_for_tasks: If True, enables RL tasks on this database
767
+ session_id: Optional session identifier for alias isolation
768
 
769
  Returns:
770
  dict: Status, schema info, and compatible RL tasks
 
776
  """
777
  global _custom_db_conn, _custom_db_schema, _custom_db_filename
778
  global _uploaded_db_for_rl, _uploaded_db_schema, _uploaded_db_tasks
779
+ global table_aliases, active_sessions
780
+
781
+ # Auto-clear aliases for this session on new upload to prevent stale mappings
782
+ if session_id and session_id in table_aliases:
783
+ table_aliases[session_id].clear()
784
+ # Update session activity
785
+ active_sessions[session_id] = {
786
+ "created_at": active_sessions.get(session_id, {}).get("created_at", datetime.now().isoformat()),
787
+ "last_used": datetime.now().isoformat()
788
+ }
789
 
790
  # Validate file extension
791
  filename = file.filename or ""
 
893
  health_audits[table_name] = perform_health_audit(_custom_db_conn, table_name)
894
  fingerprints[table_name] = analyze_table_fingerprint(_custom_db_conn, table_name)
895
 
896
+ # NEW: Semantic task detection with session support
897
+ semantic_tasks = detect_tasks_semantically(_custom_db_schema, fingerprints, session_id)
898
 
899
  # NEW: Legacy hard-coded task detection (for compatibility)
900
  _uploaded_db_tasks = detect_tasks_from_schema(_custom_db_schema)
 
913
  "use_for_tasks": use_for_tasks and (len(_uploaded_db_tasks) > 0 or len(semantic_tasks) > 0),
914
  "health_audits": health_audits,
915
  "fingerprints": fingerprints,
916
+ "session_id": session_id,
917
  "detected_table": _custom_db_schema[0]['table_name'] if _custom_db_schema else None
918
  }
919
 
 
924
 
925
 
926
  @app.post("/set-table-alias")
927
+ async def set_table_alias(body: dict, session_id: str = None) -> dict:
928
  """Set a table alias for manual task override.
929
 
930
  Args:
931
  body: JSON body with {"actual_table": "cleaned_ds_jobs2", "alias_as": "users"}
932
+ session_id: Optional session identifier for multi-user support
933
 
934
  Returns:
935
  dict: Status and current aliases
936
  """
937
+ global table_aliases, active_sessions
938
+
939
+ # Generate or use provided session ID
940
+ if not session_id:
941
+ session_id = str(uuid.uuid4())
942
+
943
+ # Initialize session if not exists
944
+ if session_id not in table_aliases:
945
+ table_aliases[session_id] = {}
946
 
947
  actual_table = body.get("actual_table", "").strip()
948
  alias_as = body.get("alias_as", "").strip()
 
950
  if not actual_table or not alias_as:
951
  raise HTTPException(status_code=422, detail="Both 'actual_table' and 'alias_as' required")
952
 
953
+ table_aliases[session_id][actual_table] = alias_as
954
+
955
+ # Update session activity
956
+ active_sessions[session_id] = {
957
+ "created_at": datetime.now().isoformat(),
958
+ "last_used": datetime.now().isoformat()
959
+ }
960
 
961
  return {
962
  "status": "ok",
963
+ "session_id": session_id,
964
  "message": f"Table '{actual_table}' will be treated as '{alias_as}' for RL tasks",
965
+ "aliases": table_aliases[session_id].copy()
966
  }
967
 
968
 
969
  @app.get("/table-aliases")
970
+ async def get_table_aliases(session_id: str = None) -> dict:
971
+ """Get current table aliases for session."""
972
+ if not session_id or session_id not in table_aliases:
973
+ return {"aliases": {}, "session_id": session_id}
974
+
975
  return {
976
+ "session_id": session_id,
977
+ "aliases": table_aliases[session_id].copy()
978
  }
979
 
980
 
981
  @app.delete("/table-aliases")
982
+ async def clear_table_aliases(session_id: str = None) -> dict:
983
+ """Clear all table aliases for session or all sessions."""
984
+ global table_aliases, active_sessions
985
+
986
+ if session_id and session_id in table_aliases:
987
+ table_aliases[session_id].clear()
988
+ if session_id in active_sessions:
989
+ del active_sessions[session_id]
990
+ return {
991
+ "status": "ok",
992
+ "session_id": session_id,
993
+ "message": f"Aliases cleared for session {session_id}"
994
+ }
995
+ else:
996
+ # Clear all sessions
997
+ table_aliases.clear()
998
+ active_sessions.clear()
999
+ return {
1000
+ "status": "ok",
1001
+ "message": "All table aliases cleared for all sessions"
1002
+ }
1003
+
1004
+
1005
+ @app.post("/new-session")
1006
+ async def create_session() -> dict:
1007
+ """Create a new session for isolated alias management."""
1008
+ session_id = str(uuid.uuid4())
1009
+ table_aliases[session_id] = {}
1010
+ active_sessions[session_id] = {
1011
+ "created_at": datetime.now().isoformat(),
1012
+ "last_used": datetime.now().isoformat()
1013
+ }
1014
+
1015
  return {
1016
+ "session_id": session_id,
1017
  "status": "ok",
1018
+ "message": "New session created"
1019
  }
1020
 
1021
 
1022
+ def sanitize_sql_identifiers(sql: str) -> str:
1023
+ """Wrap all table and column identifiers in double quotes for safety."""
1024
+ import re
1025
+
1026
+ # Pattern to match table and column names in SQL
1027
+ # This handles: table_name, table.column, "already_quoted", `backtick`, [bracket]
1028
+
1029
+ # Remove existing quotes and backticks to normalize
1030
+ sql = re.sub(r'[`\[\]]', '"', sql) # Convert backticks and brackets to quotes
1031
 
1032
+ # Pattern to find unquoted identifiers (not in quotes, not keywords)
1033
+ identifier_pattern = r'\b(?<!")(?!SELECT|FROM|WHERE|JOIN|ON|AND|OR|NOT|IN|EXISTS|CASE|WHEN|THEN|ELSE|END|AS|BY|GROUP|HAVING|ORDER|LIMIT|OFFSET|UNION|INTERSECT|EXCEPT|WITH|CTE|NULL|TRUE|FALSE|IS|LIKE|BETWEEN|ASC|DESC|DISTINCT|ALL|ANY|SOME|CAST|EXTRACT|DATE|TIME|TIMESTAMP|INTERVAL|CURRENT_DATE|CURRENT_TIME|CURRENT_TIMESTAMP)([a-zA-Z_][a-zA-Z0-9_]*)\b(?!\")'
1034
+
1035
+ def quote_match(match):
1036
+ identifier = match.group(1)
1037
+ # Don't quote if it's already a function or special case
1038
+ if identifier.upper() in ['COUNT', 'SUM', 'AVG', 'MIN', 'MAX', 'STDDEV', 'VARIANCE', 'COALESCE', 'IFNULL', 'NULLIF']:
1039
+ return identifier
1040
+ return f'"{identifier}"'
1041
+
1042
+ # Apply quoting
1043
+ quoted_sql = re.sub(identifier_pattern, quote_match, sql, flags=re.IGNORECASE)
1044
+
1045
+ return quoted_sql
1046
+
1047
+
1048
+ def apply_table_aliases(sql: str, session_id: str = None) -> str:
1049
+ """Apply table aliases to SQL queries with global quoting."""
1050
+ global table_aliases
1051
+
1052
+ # Get aliases for session or default to empty
1053
+ session_aliases = {}
1054
+ if session_id and session_id in table_aliases:
1055
+ session_aliases = table_aliases[session_id]
1056
+
1057
+ # First sanitize all identifiers
1058
+ sanitized_sql = sanitize_sql_identifiers(sql)
1059
+
1060
+ # Then apply aliases
1061
+ for actual, alias in session_aliases.items():
1062
  # Replace FROM table_name with FROM alias_name
1063
+ sanitized_sql = re.sub(
1064
+ r'\bFROM\s+"' + re.escape(actual) + r'"\b',
1065
+ f'FROM "{alias}"',
1066
+ sanitized_sql,
1067
+ flags=re.IGNORECASE
1068
+ )
1069
  # Replace table.column with alias.column
1070
+ sanitized_sql = re.sub(
1071
+ r'\b"' + re.escape(actual) + r'"\.',
1072
+ f'"{alias}".',
1073
+ sanitized_sql
1074
+ )
1075
 
1076
+ return sanitized_sql
1077
 
1078
 
1079
  @app.post("/query-custom")
1080
+ async def query_custom(body: dict, session_id: str = None) -> dict:
1081
  """Execute a SELECT query against the uploaded custom database.
1082
 
1083
  Args:
1084
  body: JSON body with {"sql": "SELECT ..."}
1085
+ session_id: Optional session identifier for alias isolation
1086
 
1087
  Returns:
1088
  dict: Query results with rows, columns, and row_count, plus suggestions if 0 rows
 
1113
  }
1114
 
1115
  try:
1116
+ # Apply table aliases and global quoting with session support
1117
+ sanitized_sql = apply_table_aliases(sql, session_id)
1118
 
1119
+ cursor = _custom_db_conn.execute(sanitized_sql)
1120
 
1121
  if cursor.description:
1122
  columns = [d[0] for d in cursor.description]
server/models.py CHANGED
@@ -189,4 +189,4 @@ class HealthResponse(BaseModel):
189
  """Response body for health/ping endpoints."""
190
 
191
  status: str = Field(description="Service health status")
192
- llm_available: Optional[bool] = Field(default=None, description="LLM connection status")
 
189
  """Response body for health/ping endpoints."""
190
 
191
  status: str = Field(description="Service health status")
192
+ llm_available: Optional[bool] = Field(default=None, description="LLM connection status")
static/index.html CHANGED
@@ -1334,7 +1334,29 @@ async function applyFix() {
1334
  });
1335
 
1336
  if (r.ok) {
1337
- toast('Fix applied: ' + (r.data.reward?.feedback || ''), 'success');
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1338
  } else {
1339
  toast('Fix failed: ' + (r.data.detail || 'Unknown error'), 'error');
1340
  }
@@ -1536,11 +1558,12 @@ function showSmartTaskSelector(legacyTasks, semanticTasks, fingerprints) {
1536
  html += '<div style="margin-bottom: 12px;">';
1537
  html += '<div style="font-size: 12px; color: #558B2F; margin-bottom: 6px;">Based on your data analysis:</div>';
1538
 
1539
- semanticTasks.forEach(task => {
1540
  const confidence = Math.round(task.confidence * 100);
1541
  const tableName = task.table_name;
1542
  const fingerprint = fingerprints[tableName];
1543
  const nullCount = fingerprint?.null_counts ? Object.values(fingerprint.null_counts).reduce((a, b) => a + b, 0) : 0;
 
1544
 
1545
  html += '<div style="margin-bottom: 8px; padding: 8px; background: white; border-radius: 4px; border: 1px solid #C8E6C9;">';
1546
  html += '<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 4px;">';
@@ -1552,11 +1575,11 @@ function showSmartTaskSelector(legacyTasks, semanticTasks, fingerprints) {
1552
  if (task.requires_alias) {
1553
  const suggestedAlias = task.suggested_alias || 'users';
1554
  html += '<div style="display: flex; gap: 6px; align-items: center;">';
1555
- html += '<button class="btn btn-primary" onclick="setTableAliasAndRunTask(\'' + tableName + '\', \'\' + suggestedAlias + '\', \'\' + task.task_id + '\')" style="font-size: 11px; padding: 4px 8px;">Run with Alias</button>';
1556
  html += '<span style="font-size: 10px; color: #888;">Will treat \"' + tableName + '\" as \"' + suggestedAlias + '\"</span>';
1557
  html += '</div>';
1558
  } else {
1559
- html += '<button class="btn btn-primary" onclick="selectUploadedDbTask(\'' + task.task_id + '\')" style="font-size: 11px; padding: 4px 8px;">Run Task</button>';
1560
  }
1561
 
1562
  html += '</div>';
@@ -1571,8 +1594,9 @@ function showSmartTaskSelector(legacyTasks, semanticTasks, fingerprints) {
1571
  html += '<div style="font-size: 12px; color: #558B2F; margin-bottom: 6px;">Classic tasks (exact name match):</div>';
1572
  html += '<div style="display: flex; gap: 8px; flex-wrap: wrap;">';
1573
 
1574
- legacyTasks.forEach(taskId => {
1575
- html += '<button class="btn btn-secondary" onclick="selectUploadedDbTask(\'' + taskId + '\')" style="font-size: 12px; padding: 6px 12px;">' + taskLabels[taskId] + '</button>';
 
1576
  });
1577
 
1578
  html += '</div></div>';
@@ -1582,11 +1606,47 @@ function showSmartTaskSelector(legacyTasks, semanticTasks, fingerprints) {
1582
  html += '<div style="margin-top: 12px; padding: 8px; background: #FFF3E0; border-radius: 4px;">';
1583
  html += '<div style="font-size: 11px; color: #E65100; margin-bottom: 6px;"><strong>Manual Override:</strong></div>';
1584
  html += '<div style="font-size: 10px; color: #666; margin-bottom: 4px;">Treat any table as a specific task type:</div>';
1585
- html += '<button onclick="showManualAliasDialog()" style="font-size: 10px; padding: 4px 8px; background: #FF9800; color: white; border: none; border-radius: 4px; cursor: pointer;">Set Table Alias</button>';
1586
  html += '</div>';
1587
 
1588
  selector.innerHTML = html;
1589
  selector.style.display = 'block';
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1590
  }
1591
 
1592
  function showUploadedDbTaskSelector(compatibleTasks) {
@@ -2229,6 +2289,47 @@ async function checkLLMConnection() {
2229
  }
2230
 
2231
  // ─── INIT ─────────────────────────────────────────────────────────────
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2232
  checkHealth();
2233
  setInterval(checkHealth, 30000);
2234
  </script>
 
1334
  });
1335
 
1336
  if (r.ok) {
1337
+ const feedback = r.data.reward?.feedback || 'Fix applied';
1338
+ toast('Fix applied: ' + feedback, 'success');
1339
+
1340
+ // NEW: Auto-refresh the data to show cleaned results
1341
+ setTimeout(() => {
1342
+ // Refresh the current query to show the fixed data
1343
+ const currentQuery = document.getElementById('query-sql').value.trim();
1344
+ if (currentQuery) {
1345
+ // Re-run the current query to show updated data
1346
+ runQuery();
1347
+ } else {
1348
+ // If no current query, run a basic SELECT * to show the table
1349
+ const tableName = state.schemaInfo?.[0]?.table_name || 'table';
1350
+ document.getElementById('query-sql').value = `SELECT * FROM "${tableName}" LIMIT 20`;
1351
+ runQuery();
1352
+ }
1353
+
1354
+ // Also refresh schema to show any type changes
1355
+ if (state.schemaInfo) {
1356
+ renderSchema(state.schemaInfo);
1357
+ }
1358
+ }, 500); // Small delay to ensure fix is applied
1359
+
1360
  } else {
1361
  toast('Fix failed: ' + (r.data.detail || 'Unknown error'), 'error');
1362
  }
 
1558
  html += '<div style="margin-bottom: 12px;">';
1559
  html += '<div style="font-size: 12px; color: #558B2F; margin-bottom: 6px;">Based on your data analysis:</div>';
1560
 
1561
+ semanticTasks.forEach((task, index) => {
1562
  const confidence = Math.round(task.confidence * 100);
1563
  const tableName = task.table_name;
1564
  const fingerprint = fingerprints[tableName];
1565
  const nullCount = fingerprint?.null_counts ? Object.values(fingerprint.null_counts).reduce((a, b) => a + b, 0) : 0;
1566
+ const buttonId = `semantic-task-${index}`;
1567
 
1568
  html += '<div style="margin-bottom: 8px; padding: 8px; background: white; border-radius: 4px; border: 1px solid #C8E6C9;">';
1569
  html += '<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 4px;">';
 
1575
  if (task.requires_alias) {
1576
  const suggestedAlias = task.suggested_alias || 'users';
1577
  html += '<div style="display: flex; gap: 6px; align-items: center;">';
1578
+ html += '<button id="' + buttonId + '" class="btn btn-primary" style="font-size: 11px; padding: 4px 8px;">Run with Alias</button>';
1579
  html += '<span style="font-size: 10px; color: #888;">Will treat \"' + tableName + '\" as \"' + suggestedAlias + '\"</span>';
1580
  html += '</div>';
1581
  } else {
1582
+ html += '<button id="' + buttonId + '" class="btn btn-primary" style="font-size: 11px; padding: 4px 8px;">Run Task</button>';
1583
  }
1584
 
1585
  html += '</div>';
 
1594
  html += '<div style="font-size: 12px; color: #558B2F; margin-bottom: 6px;">Classic tasks (exact name match):</div>';
1595
  html += '<div style="display: flex; gap: 8px; flex-wrap: wrap;">';
1596
 
1597
+ legacyTasks.forEach((taskId, index) => {
1598
+ const buttonId = `legacy-task-${index}`;
1599
+ html += '<button id="' + buttonId + '" class="btn btn-secondary" style="font-size: 12px; padding: 6px 12px;">' + taskLabels[taskId] + '</button>';
1600
  });
1601
 
1602
  html += '</div></div>';
 
1606
  html += '<div style="margin-top: 12px; padding: 8px; background: #FFF3E0; border-radius: 4px;">';
1607
  html += '<div style="font-size: 11px; color: #E65100; margin-bottom: 6px;"><strong>Manual Override:</strong></div>';
1608
  html += '<div style="font-size: 10px; color: #666; margin-bottom: 4px;">Treat any table as a specific task type:</div>';
1609
+ html += '<button id="manual-alias-btn" style="font-size: 10px; padding: 4px 8px; background: #FF9800; color: white; border: none; border-radius: 4px; cursor: pointer;">Set Table Alias</button>';
1610
  html += '</div>';
1611
 
1612
  selector.innerHTML = html;
1613
  selector.style.display = 'block';
1614
+
1615
+ // Add event listeners to buttons
1616
+ setTimeout(() => {
1617
+ // Semantic task buttons
1618
+ semanticTasks.forEach((task, index) => {
1619
+ const buttonId = `semantic-task-${index}`;
1620
+ const button = document.getElementById(buttonId);
1621
+ if (button) {
1622
+ button.addEventListener('click', () => {
1623
+ if (task.requires_alias) {
1624
+ const suggestedAlias = task.suggested_alias || 'users';
1625
+ setTableAliasAndRunTask(task.table_name, suggestedAlias, task.task_id);
1626
+ } else {
1627
+ selectUploadedDbTask(task.task_id);
1628
+ }
1629
+ });
1630
+ }
1631
+ });
1632
+
1633
+ // Legacy task buttons
1634
+ legacyTasks.forEach((taskId, index) => {
1635
+ const buttonId = `legacy-task-${index}`;
1636
+ const button = document.getElementById(buttonId);
1637
+ if (button) {
1638
+ button.addEventListener('click', () => {
1639
+ selectUploadedDbTask(taskId);
1640
+ });
1641
+ }
1642
+ });
1643
+
1644
+ // Manual alias button
1645
+ const manualBtn = document.getElementById('manual-alias-btn');
1646
+ if (manualBtn) {
1647
+ manualBtn.addEventListener('click', showManualAliasDialog);
1648
+ }
1649
+ }, 100);
1650
  }
1651
 
1652
  function showUploadedDbTaskSelector(compatibleTasks) {
 
2289
  }
2290
 
2291
  // ─── INIT ─────────────────────────────────────────────────────────────
2292
+ // Initialize event delegation for dynamic elements
2293
+ document.addEventListener('DOMContentLoaded', function() {
2294
+ // Handle clicks on dynamically created elements
2295
+ document.body.addEventListener('click', function(e) {
2296
+ const target = e.target;
2297
+
2298
+ // Handle task selection buttons
2299
+ if (target.closest('[onclick*="selectUploadedDbTask"]')) {
2300
+ const onclick = target.closest('[onclick*="selectUploadedDbTask"]').getAttribute('onclick');
2301
+ const match = onclick.match(/selectUploadedDbTask\('([^']+)\')/);
2302
+ if (match) {
2303
+ selectUploadedDbTask(match[1]);
2304
+ }
2305
+ }
2306
+
2307
+ // Handle alias buttons
2308
+ if (target.closest('[onclick*="setTableAliasAndRunTask"]')) {
2309
+ const onclick = target.closest('[onclick*="setTableAliasAndRunTask"]').getAttribute('onclick');
2310
+ const match = onclick.match(/setTableAliasAndRunTask\('([^']+)', '([^']*)', '([^']+)\')/);
2311
+ if (match) {
2312
+ setTableAliasAndRunTask(match[1], match[2], match[3]);
2313
+ }
2314
+ }
2315
+
2316
+ // Handle manual alias dialog
2317
+ if (target.closest('[onclick*="showManualAliasDialog"]')) {
2318
+ showManualAliasDialog();
2319
+ }
2320
+
2321
+ // Handle modal buttons
2322
+ if (target.id === 'alias-modal') {
2323
+ if (target.closest('[onclick*="closeManualAliasDialog"]')) {
2324
+ closeManualAliasDialog();
2325
+ }
2326
+ if (target.closest('[onclick*="applyManualAlias"]')) {
2327
+ applyManualAlias();
2328
+ }
2329
+ }
2330
+ });
2331
+ });
2332
+
2333
  checkHealth();
2334
  setInterval(checkHealth, 30000);
2335
  </script>
tests/__pycache__/__init__.cpython-313.pyc ADDED
Binary file (206 Bytes). View file
 
tests/__pycache__/test_environment.cpython-313-pytest-8.3.5.pyc ADDED
Binary file (25.7 kB). View file