junaid0600 commited on
Commit
2146d9e
Β·
1 Parent(s): d4b572f

corrected everytihng

Browse files
Files changed (2) hide show
  1. env/graders.py +177 -217
  2. inference.py +43 -26
env/graders.py CHANGED
@@ -1,114 +1,92 @@
1
  import re
2
- import math
3
  from env.models import Action, DifficultyLevel
4
  from env.tasks import task_manager
5
 
6
- # ─────────────────────────────────────────────
7
- # SCORE BOUNDS (strictly between 0 and 1)
8
- # ─────────────────────────────────────────────
9
- SCORE_MIN = 0.001 # 0 < SCORE_MIN < 1
10
- SCORE_MAX = 0.999 # 0 < SCORE_MAX < 1
11
-
12
-
13
- def _clamp(value) -> float:
14
- """
15
- Guarantee the returned float is strictly inside (0, 1).
16
- Handles NaN, Inf, None, strings, and any numeric type safely.
17
- The round() call is applied AFTER the clamp, never before.
18
- """
19
- try:
20
- v = float(value)
21
- except (TypeError, ValueError):
22
- return SCORE_MIN
23
-
24
- # Guard against NaN and Β±Inf before any comparison
25
- if not math.isfinite(v):
26
- return SCORE_MIN
27
-
28
- clamped = max(min(v, SCORE_MAX), SCORE_MIN)
29
- return round(clamped, 4)
30
-
31
-
32
  # ─────────────────────────────────────────────
33
  # HELPERS
34
  # ─────────────────────────────────────────────
35
 
36
  def _normalize(text: str) -> str:
 
37
  if not isinstance(text, str):
38
  return ""
39
  return re.sub(r"\s+", " ", text.strip().lower())
40
 
41
-
42
  def _safe_get(payload: dict, key: str, default=None):
 
43
  if not isinstance(payload, dict):
44
  return default
45
  return payload.get(key, default)
46
 
47
-
48
  def _score_explanation(explanation: str) -> float:
 
49
  if not explanation or not isinstance(explanation, str):
50
- return SCORE_MIN
51
  explanation = explanation.strip()
52
  if len(explanation) < 10:
53
- return SCORE_MIN
54
  if len(explanation) < 30:
55
  return 0.05
56
  if len(explanation) < 80:
57
  return 0.10
58
  return 0.15
59
 
60
-
61
  def _score_confidence(confidence) -> float:
 
62
  try:
63
  c = float(confidence)
64
- if math.isfinite(c) and 0.0 <= c <= 1.0:
65
  return 0.05
66
  except (TypeError, ValueError):
67
  pass
68
- return SCORE_MIN
69
-
70
 
71
  def _query_similarity(submitted: str, expected: str) -> float:
 
 
 
 
72
  s = _normalize(submitted)
73
  e = _normalize(expected)
74
 
75
  if s == e:
76
- # Exact match β€” return SCORE_MAX, NOT 1.0
77
- return SCORE_MAX
78
 
79
  s_tokens = set(s.split())
80
  e_tokens = set(e.split())
81
 
82
  if not e_tokens:
83
- return SCORE_MIN
84
 
85
  overlap = len(s_tokens & e_tokens) / len(e_tokens)
86
 
87
  critical_keywords = _extract_critical_keywords(e)
88
  critical_found = sum(1 for kw in critical_keywords if kw in s)
89
- critical_score = (critical_found / len(critical_keywords)
90
- if critical_keywords else 0.0)
91
-
92
- raw = (overlap * 0.4) + (critical_score * 0.6)
93
- return _clamp(raw)
94
 
 
95
 
96
- def _extract_critical_keywords(query: str) -> list:
 
97
  keywords = [
98
  "left join", "inner join", "right join",
99
  "group by", "order by", "having",
100
  "partition by", "coalesce", "distinct",
101
  "where", "on", "and", "or", "not",
102
  "count", "sum", "avg", "max", "min",
103
- "select", "from", "join",
104
  ]
 
105
  q = query.lower()
106
- return [kw for kw in keywords if kw in q]
107
-
 
 
108
 
109
  def _score_error_type(submitted_type: str, expected_type: str) -> float:
 
110
  if not submitted_type:
111
- return SCORE_MIN
112
  s = submitted_type.strip().lower()
113
  e = expected_type.strip().lower()
114
  if s == e:
@@ -116,54 +94,51 @@ def _score_error_type(submitted_type: str, expected_type: str) -> float:
116
  related = {
117
  "performance": ["optimization", "slow", "index", "scan"],
118
  "logic": ["semantic", "incorrect", "wrong"],
119
- "syntax": ["parse", "grammar", "token"],
120
  }
121
  for canonical, aliases in related.items():
122
  if e == canonical and any(alias in s for alias in aliases):
123
  return 0.05
124
- return SCORE_MIN
125
-
126
 
127
- def _score_error_location(submitted_location: str,
128
- expected_location: str) -> float:
129
  if not submitted_location or not expected_location:
130
- return SCORE_MIN
131
  s = submitted_location.strip().lower()
132
  e = expected_location.strip().lower()
133
  if s == e:
134
  return 0.15
135
  e_words = set(e.split())
136
  s_words = set(s.split())
137
- if not e_words:
138
- return SCORE_MIN
139
- overlap = len(e_words & s_words) / len(e_words)
140
- return _clamp(overlap * 0.10)
141
 
142
 
143
  # ─────────────────────────────────────────────
144
- # GRADERS
145
  # ─────────────────────────────────────────────
146
 
147
- def grade_easy(action: Action, ground_truth: dict) -> tuple:
148
  """
149
- Easy β€” syntax errors.
150
- Scoring budget: fix(0.50) + loc(0.15) + type(0.10) + expl(0.15) + conf(0.05) = 0.95
 
151
  """
152
  if action is None or action.payload is None:
153
- return SCORE_MIN, {"error": "null_action"}, "No action provided."
154
 
155
- payload = action.payload
156
- score = 0.0
157
- breakdown = {}
158
  feedback_parts = []
159
 
160
- # 1. Fix correctness (0.50)
161
- submitted_query = (_safe_get(payload, "fixed_query", "")
162
- or _safe_get(payload, "optimized_query", "") or "")
163
  expected_query = ground_truth.get("fixed_query", "")
164
  similarity = _query_similarity(submitted_query, expected_query)
165
 
166
- if similarity >= SCORE_MAX:
167
  fix_score = 0.50
168
  feedback_parts.append("Correct fix applied.")
169
  elif similarity >= 0.75:
@@ -176,69 +151,67 @@ def grade_easy(action: Action, ground_truth: dict) -> tuple:
176
  fix_score = 0.0
177
  feedback_parts.append("Fix is incorrect or not provided.")
178
 
179
- breakdown["fix_correctness"] = _clamp(fix_score)
180
  score += fix_score
181
-
182
- # 2. Error location (0.15)
183
- loc_score = _score_error_location(
184
- str(_safe_get(payload, "error_location", "") or ""),
185
- ground_truth.get("error_location", ""),
186
- )
187
- breakdown["error_location"] = _clamp(loc_score)
188
- score += loc_score
189
- if loc_score > SCORE_MIN:
190
  feedback_parts.append("Correctly identified error location.")
191
 
192
- # 3. Error type (0.10)
193
- type_score = _score_error_type(
194
- str(_safe_get(payload, "error_type", "") or ""),
195
- ground_truth.get("error_type", "syntax"),
196
- )
197
- breakdown["error_type"] = _clamp(type_score)
198
- score += type_score
199
- if type_score > SCORE_MIN:
200
  feedback_parts.append("Correctly identified error type.")
201
 
202
- # 4. Explanation quality (0.15)
203
- explanation = (_safe_get(payload, "explanation", "")
204
- or _safe_get(payload, "change_made", "") or "")
205
- expl_score = _score_explanation(str(explanation))
206
- breakdown["explanation"] = _clamp(expl_score)
207
- score += expl_score
208
- if expl_score > SCORE_MIN:
209
  feedback_parts.append("Explanation provided.")
210
 
211
- # 5. Confidence (0.05)
212
- conf_score = _score_confidence(_safe_get(payload, "confidence", None))
213
- breakdown["confidence"] = _clamp(conf_score)
214
- score += conf_score
 
215
 
216
- final_score = _clamp(score)
217
- feedback = " ".join(feedback_parts) or "No valid response provided."
 
218
  return final_score, breakdown, feedback
219
 
220
 
221
- def grade_medium(action: Action, ground_truth: dict) -> tuple:
222
  """
223
- Medium β€” logic errors.
224
- Scoring budget: fix(0.40) + logic(0.20) + loc(0.15) + expl(0.15)
225
- + conf(0.05) + impact(0.05) = 1.00 -> clamped to SCORE_MAX
226
  """
227
  if action is None or action.payload is None:
228
- return SCORE_MIN, {"error": "null_action"}, "No action provided."
229
 
230
  payload = action.payload
231
  score = 0.0
232
  breakdown = {}
233
  feedback_parts = []
234
 
235
- # 1. Fix correctness (0.40)
236
- submitted_query = (_safe_get(payload, "fixed_query", "")
237
- or _safe_get(payload, "optimized_query", "") or "")
238
  expected_query = ground_truth.get("fixed_query", "")
239
  similarity = _query_similarity(submitted_query, expected_query)
240
 
241
- if similarity >= SCORE_MAX:
242
  fix_score = 0.40
243
  feedback_parts.append("Correct fix applied.")
244
  elif similarity >= 0.80:
@@ -254,84 +227,86 @@ def grade_medium(action: Action, ground_truth: dict) -> tuple:
254
  fix_score = 0.0
255
  feedback_parts.append("Fix is incorrect or missing.")
256
 
257
- breakdown["fix_correctness"] = _clamp(fix_score)
258
  score += fix_score
 
259
 
260
- # 2. Logic flaw identification (0.20)
261
- explanation = str(_safe_get(payload, "explanation", "")
262
- or _safe_get(payload, "change_made", "") or "")
263
  error_type = ground_truth.get("error_type", "logic")
264
 
265
  logic_keywords = {
266
- "logic": ["join", "left join", "inner join", "having", "where",
267
- "group by", "aggregate", "subquery", "correlation",
268
- "distinct", "count"],
269
- "performance": ["index", "scan", "n+1", "correlated",
270
- "cartesian", "window"],
271
  }
 
272
  keywords_to_check = logic_keywords.get(error_type, logic_keywords["logic"])
273
  expl_lower = explanation.lower()
274
  keyword_hits = sum(1 for kw in keywords_to_check if kw in expl_lower)
275
- logic_score = _clamp(min(keyword_hits * 0.05, 0.20))
276
- breakdown["logic_flaw_identification"] = _clamp(logic_score)
277
- score += logic_score
278
- if logic_score > SCORE_MIN:
279
  feedback_parts.append("Shows understanding of the logic flaw.")
280
 
281
- # 3. Error location (0.15)
282
- loc_score = _score_error_location(
283
- str(_safe_get(payload, "error_location", "") or ""),
284
- ground_truth.get("error_location", ""),
285
- )
286
- breakdown["error_location"] = _clamp(loc_score)
287
- score += loc_score
288
 
289
- # 4. Explanation quality (0.15)
290
  expl_score = _score_explanation(explanation)
291
- breakdown["explanation"] = _clamp(expl_score)
292
- score += expl_score
293
 
294
- # 5. Confidence (0.05)
295
- conf_score = _score_confidence(_safe_get(payload, "confidence", None))
296
- breakdown["confidence"] = _clamp(conf_score)
297
- score += conf_score
 
298
 
299
- # 6. Impact analysis bonus (0.05)
300
  impact = str(_safe_get(payload, "impact", "") or "")
301
- impact_score = 0.05 if len(impact.strip()) > 20 else 0.0
302
- breakdown["impact_analysis"] = _clamp(impact_score)
303
- score += impact_score
304
- if impact_score > 0:
305
  feedback_parts.append("Impact analysis provided.")
 
 
306
 
307
- final_score = _clamp(score)
308
- feedback = " ".join(feedback_parts) or "No valid response provided."
 
309
  return final_score, breakdown, feedback
310
 
311
 
312
- def grade_hard(action: Action, ground_truth: dict) -> tuple:
313
  """
314
- Hard β€” performance issues (N+1, missing index, cartesian, etc).
315
- Scoring budget: query(0.30) + concept(0.30) + expl(0.15)
316
- + root(0.10) + improvement(0.10) + conf(0.05) = 1.00 -> clamped
 
317
  """
318
  if action is None or action.payload is None:
319
- return SCORE_MIN, {"error": "null_action"}, "No action provided."
320
 
321
- # All variables initialised before first use
322
  payload = action.payload
323
  score = 0.0
324
  breakdown = {}
325
  feedback_parts = []
326
- _rubric = ground_truth.get("scoring_rubric", {}) # reserved for future use
327
 
328
- # 1. Query correctness (0.30)
329
- submitted_query = (_safe_get(payload, "optimized_query", "")
330
- or _safe_get(payload, "fixed_query", "") or "")
331
- expected_query = ground_truth.get("fixed_query", "")
332
- similarity = _query_similarity(submitted_query, expected_query)
 
 
 
333
 
334
- if similarity >= SCORE_MAX:
335
  fix_score = 0.30
336
  feedback_parts.append("Perfectly optimized query.")
337
  elif similarity >= 0.85:
@@ -347,75 +322,70 @@ def grade_hard(action: Action, ground_truth: dict) -> tuple:
347
  fix_score = 0.0
348
  feedback_parts.append("Query does not address the performance issue.")
349
 
350
- breakdown["query_correctness"] = _clamp(fix_score)
351
  score += fix_score
 
352
 
353
- # 2. Performance concept identification (0.30)
354
- explanation = str(_safe_get(payload, "explanation", "")
355
- or _safe_get(payload, "change_made", "") or "")
356
  optimization = str(_safe_get(payload, "optimization_type", "") or "")
357
  combined_text = (explanation + " " + optimization).lower()
358
  perf_issue = ground_truth.get("performance_issue", {})
359
- issue_type = (perf_issue.get("type", "").lower()
360
- if isinstance(perf_issue, dict) else "")
361
 
362
  performance_concept_map = {
363
- "n+1": ["n+1", "correlated subquery", "subquery per row",
364
- "multiple queries", "join instead"],
365
- "full table scan": ["full table scan", "index not used",
366
- "function on column", "sargable",
367
- "range scan", "seek"],
368
- "cartesian product": ["cartesian", "cross join",
369
- "missing join condition",
370
- "implicit join", "comma join"],
371
- "select *": ["select *", "over-fetch", "covering index",
372
- "column projection", "unnecessary columns"],
373
- "window function": ["window function", "partition by", "row_number",
374
- "subquery filter", "where clause window"],
375
  }
376
 
377
  concept_score = 0.0
378
  for concept, keywords in performance_concept_map.items():
379
- if any(part in issue_type for part in concept.split()):
380
  hits = sum(1 for kw in keywords if kw in combined_text)
381
  concept_score = min(hits * 0.06, 0.30)
382
  break
383
 
384
- breakdown["performance_concept"] = _clamp(concept_score)
385
  score += concept_score
 
386
  if concept_score > 0:
387
  feedback_parts.append("Demonstrates understanding of the performance issue.")
388
 
389
- # 3. Explanation depth (0.15)
390
  expl_score = _score_explanation(explanation)
391
  if len(explanation.strip()) > 150:
392
  expl_score = min(expl_score + 0.05, 0.15)
393
- breakdown["explanation_depth"] = _clamp(expl_score)
394
  score += expl_score
 
395
 
396
- # 4. Root cause analysis (0.10)
397
  root_cause = str(_safe_get(payload, "root_cause", "") or "")
398
- root_score = 0.10 if len(root_cause.strip()) > 30 else 0.0
399
- breakdown["root_cause_analysis"] = _clamp(root_score)
400
- score += root_score
401
- if root_score > 0:
402
  feedback_parts.append("Root cause analysis provided.")
 
 
403
 
404
- # 5. Expected improvement (0.10)
405
  improvement = str(_safe_get(payload, "expected_improvement", "") or "")
406
- imp_score = 0.10 if len(improvement.strip()) > 20 else 0.0
407
- breakdown["expected_improvement"] = _clamp(imp_score)
408
- score += imp_score
409
- if imp_score > 0:
410
  feedback_parts.append("Performance improvement estimate provided.")
 
 
411
 
412
- # 6. Confidence (0.05)
413
- conf_score = _score_confidence(_safe_get(payload, "confidence", None))
414
- breakdown["confidence"] = _clamp(conf_score)
415
- score += conf_score
 
416
 
417
- final_score = _clamp(score)
418
- feedback = " ".join(feedback_parts) or "Performance issue not identified."
 
419
  return final_score, breakdown, feedback
420
 
421
 
@@ -423,40 +393,30 @@ def grade_hard(action: Action, ground_truth: dict) -> tuple:
423
  # MAIN GRADER DISPATCHER
424
  # ─────────────────���───────────────────────────
425
 
426
- def grade(action: Action, task_id: str) -> tuple:
427
  """
428
- Main entry point. Always returns (float, dict, str) β€” never crashes.
429
- The returned float is always strictly inside (0, 1).
 
 
430
  """
431
  if action is None:
432
- return SCORE_MIN, {"error": "null_action"}, "No action provided."
433
 
434
  ground_truth = task_manager.get_ground_truth(task_id)
435
  if ground_truth is None:
436
- return SCORE_MIN, {"error": "unknown_task"}, f"Task '{task_id}' not found."
437
 
438
  difficulty = ground_truth.get("id", "").split("_")[0]
439
 
440
  try:
441
  if difficulty == "easy":
442
- result = grade_easy(action, ground_truth)
443
  elif difficulty == "medium":
444
- result = grade_medium(action, ground_truth)
445
  elif difficulty == "hard":
446
- result = grade_hard(action, ground_truth)
447
  else:
448
- return (SCORE_MIN,
449
- {"error": "unknown_difficulty"},
450
- f"Unknown difficulty: {difficulty}")
451
-
452
- # Final safety net: re-clamp the returned score and every breakdown value
453
- final_score, breakdown, feedback = result
454
- safe_score = _clamp(final_score)
455
- safe_breakdown = {
456
- k: _clamp(v) if isinstance(v, (int, float)) else v
457
- for k, v in breakdown.items()
458
- }
459
- return safe_score, safe_breakdown, feedback
460
-
461
  except Exception as e:
462
- return SCORE_MIN, {"error": str(e)}, f"Grader error: {str(e)}"
 
1
  import re
 
2
  from env.models import Action, DifficultyLevel
3
  from env.tasks import task_manager
4
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
5
  # ─────────────────────────────────────────────
6
  # HELPERS
7
  # ─────────────────────────────────────────────
8
 
9
  def _normalize(text: str) -> str:
10
+ """Normalize SQL for comparison β€” lowercase, strip whitespace, collapse spaces."""
11
  if not isinstance(text, str):
12
  return ""
13
  return re.sub(r"\s+", " ", text.strip().lower())
14
 
 
15
  def _safe_get(payload: dict, key: str, default=None):
16
+ """Safe dict access β€” never KeyError."""
17
  if not isinstance(payload, dict):
18
  return default
19
  return payload.get(key, default)
20
 
 
21
  def _score_explanation(explanation: str) -> float:
22
+ """Score explanation quality by length and keyword richness."""
23
  if not explanation or not isinstance(explanation, str):
24
+ return 0.0
25
  explanation = explanation.strip()
26
  if len(explanation) < 10:
27
+ return 0.0
28
  if len(explanation) < 30:
29
  return 0.05
30
  if len(explanation) < 80:
31
  return 0.10
32
  return 0.15
33
 
 
34
  def _score_confidence(confidence) -> float:
35
+ """Give partial credit for providing a valid confidence score."""
36
  try:
37
  c = float(confidence)
38
+ if 0.0 <= c <= 1.0:
39
  return 0.05
40
  except (TypeError, ValueError):
41
  pass
42
+ return 0.0
 
43
 
44
  def _query_similarity(submitted: str, expected: str) -> float:
45
+ """
46
+ Multi-level SQL similarity check.
47
+ Returns 0.0 - 1.0 based on how close the submitted query is to expected.
48
+ """
49
  s = _normalize(submitted)
50
  e = _normalize(expected)
51
 
52
  if s == e:
53
+ return 1.0
 
54
 
55
  s_tokens = set(s.split())
56
  e_tokens = set(e.split())
57
 
58
  if not e_tokens:
59
+ return 0.0
60
 
61
  overlap = len(s_tokens & e_tokens) / len(e_tokens)
62
 
63
  critical_keywords = _extract_critical_keywords(e)
64
  critical_found = sum(1 for kw in critical_keywords if kw in s)
65
+ critical_score = critical_found / len(critical_keywords) if critical_keywords else 0.0
 
 
 
 
66
 
67
+ return round((overlap * 0.4) + (critical_score * 0.6), 4)
68
 
69
+ def _extract_critical_keywords(query: str) -> list[str]:
70
+ """Extract SQL keywords that are critical to correctness."""
71
  keywords = [
72
  "left join", "inner join", "right join",
73
  "group by", "order by", "having",
74
  "partition by", "coalesce", "distinct",
75
  "where", "on", "and", "or", "not",
76
  "count", "sum", "avg", "max", "min",
77
+ "select", "from", "join"
78
  ]
79
+ found = []
80
  q = query.lower()
81
+ for kw in keywords:
82
+ if kw in q:
83
+ found.append(kw)
84
+ return found
85
 
86
  def _score_error_type(submitted_type: str, expected_type: str) -> float:
87
+ """Score for correctly identifying the error type."""
88
  if not submitted_type:
89
+ return 0.0
90
  s = submitted_type.strip().lower()
91
  e = expected_type.strip().lower()
92
  if s == e:
 
94
  related = {
95
  "performance": ["optimization", "slow", "index", "scan"],
96
  "logic": ["semantic", "incorrect", "wrong"],
97
+ "syntax": ["parse", "grammar", "token"]
98
  }
99
  for canonical, aliases in related.items():
100
  if e == canonical and any(alias in s for alias in aliases):
101
  return 0.05
102
+ return 0.0
 
103
 
104
+ def _score_error_location(submitted_location: str, expected_location: str) -> float:
105
+ """Score for correctly identifying WHERE in the query the error is."""
106
  if not submitted_location or not expected_location:
107
+ return 0.0
108
  s = submitted_location.strip().lower()
109
  e = expected_location.strip().lower()
110
  if s == e:
111
  return 0.15
112
  e_words = set(e.split())
113
  s_words = set(s.split())
114
+ overlap = len(e_words & s_words) / len(e_words) if e_words else 0.0
115
+ return round(overlap * 0.10, 4)
 
 
116
 
117
 
118
  # ─────────────────────────────────────────────
119
+ # GRADERS PER DIFFICULTY
120
  # ─────────────────────────────────────────────
121
 
122
+ def grade_easy(action: Action, ground_truth: dict) -> tuple[float, dict, str]:
123
  """
124
+ Easy task grader β€” syntax errors.
125
+ Max score: 0.999 (strictly less than 1.0)
126
+ DETERMINISTIC: same input always returns same score.
127
  """
128
  if action is None or action.payload is None:
129
+ return 0.001, {"error": "null_action"}, "No action provided."
130
 
131
+ payload = action.payload
132
+ score = 0.0
133
+ breakdown = {}
134
  feedback_parts = []
135
 
136
+ # ── 1. Query fix correctness (0.50) ──────────────────────────
137
+ submitted_query = _safe_get(payload, "fixed_query", "") or _safe_get(payload, "optimized_query", "")
 
138
  expected_query = ground_truth.get("fixed_query", "")
139
  similarity = _query_similarity(submitted_query, expected_query)
140
 
141
+ if similarity >= 1.0:
142
  fix_score = 0.50
143
  feedback_parts.append("Correct fix applied.")
144
  elif similarity >= 0.75:
 
151
  fix_score = 0.0
152
  feedback_parts.append("Fix is incorrect or not provided.")
153
 
 
154
  score += fix_score
155
+ breakdown["fix_correctness"] = round(fix_score, 4)
156
+
157
+ # ── 2. Error location (0.15) ─────────────────────────────────
158
+ submitted_location = _safe_get(payload, "error_location", "")
159
+ expected_location = ground_truth.get("error_location", "")
160
+ loc_score = _score_error_location(str(submitted_location), expected_location)
161
+ score += loc_score
162
+ breakdown["error_location"] = round(loc_score, 4)
163
+ if loc_score > 0:
164
  feedback_parts.append("Correctly identified error location.")
165
 
166
+ # ── 3. Error type (0.10) ─────────────────────────────────────
167
+ submitted_type = _safe_get(payload, "error_type", "")
168
+ expected_type = ground_truth.get("error_type", "syntax")
169
+ type_score = _score_error_type(str(submitted_type), expected_type)
170
+ score += type_score
171
+ breakdown["error_type"] = round(type_score, 4)
172
+ if type_score > 0:
 
173
  feedback_parts.append("Correctly identified error type.")
174
 
175
+ # ── 4. Explanation quality (0.15) ───────────��────────────────
176
+ explanation = _safe_get(payload, "explanation", "") or _safe_get(payload, "change_made", "")
177
+ expl_score = _score_explanation(str(explanation) if explanation else "")
178
+ score += expl_score
179
+ breakdown["explanation"] = round(expl_score, 4)
180
+ if expl_score > 0:
 
181
  feedback_parts.append("Explanation provided.")
182
 
183
+ # ── 5. Confidence (0.05) ─────────────────────────────────────
184
+ confidence = _safe_get(payload, "confidence", None)
185
+ conf_score = _score_confidence(confidence)
186
+ score += conf_score
187
+ breakdown["confidence"] = round(conf_score, 4)
188
 
189
+ # Clamp strictly between 0 and 1 exclusive
190
+ final_score = round(max(0.001, min(0.999, score)), 4)
191
+ feedback = " ".join(feedback_parts) if feedback_parts else "No valid response provided."
192
  return final_score, breakdown, feedback
193
 
194
 
195
+ def grade_medium(action: Action, ground_truth: dict) -> tuple[float, dict, str]:
196
  """
197
+ Medium task grader β€” logic errors.
198
+ Max score: 0.999 (strictly less than 1.0)
199
+ DETERMINISTIC: same input always returns same score.
200
  """
201
  if action is None or action.payload is None:
202
+ return 0.001, {"error": "null_action"}, "No action provided."
203
 
204
  payload = action.payload
205
  score = 0.0
206
  breakdown = {}
207
  feedback_parts = []
208
 
209
+ # ── 1. Query fix correctness (0.40) ──────────────────────────
210
+ submitted_query = _safe_get(payload, "fixed_query", "") or _safe_get(payload, "optimized_query", "")
 
211
  expected_query = ground_truth.get("fixed_query", "")
212
  similarity = _query_similarity(submitted_query, expected_query)
213
 
214
+ if similarity >= 1.0:
215
  fix_score = 0.40
216
  feedback_parts.append("Correct fix applied.")
217
  elif similarity >= 0.80:
 
227
  fix_score = 0.0
228
  feedback_parts.append("Fix is incorrect or missing.")
229
 
 
230
  score += fix_score
231
+ breakdown["fix_correctness"] = round(fix_score, 4)
232
 
233
+ # ── 2. Logic flaw identification (0.20) ──────────────────────
234
+ explanation = str(_safe_get(payload, "explanation", "") or _safe_get(payload, "change_made", "") or "")
 
235
  error_type = ground_truth.get("error_type", "logic")
236
 
237
  logic_keywords = {
238
+ "logic": ["join", "left join", "inner join", "having", "where", "group by",
239
+ "aggregate", "subquery", "correlation", "distinct", "count"],
240
+ "performance": ["index", "scan", "n+1", "correlated", "cartesian", "window"]
 
 
241
  }
242
+
243
  keywords_to_check = logic_keywords.get(error_type, logic_keywords["logic"])
244
  expl_lower = explanation.lower()
245
  keyword_hits = sum(1 for kw in keywords_to_check if kw in expl_lower)
246
+ logic_score = min(keyword_hits * 0.05, 0.20)
247
+ score += logic_score
248
+ breakdown["logic_flaw_identification"] = round(logic_score, 4)
249
+ if logic_score > 0:
250
  feedback_parts.append("Shows understanding of the logic flaw.")
251
 
252
+ # ── 3. Error location (0.15) ─────────────────────────────────
253
+ submitted_location = _safe_get(payload, "error_location", "")
254
+ expected_location = ground_truth.get("error_location", "")
255
+ loc_score = _score_error_location(str(submitted_location), expected_location)
256
+ score += loc_score
257
+ breakdown["error_location"] = round(loc_score, 4)
 
258
 
259
+ # ── 4. Explanation quality (0.15) ────────────────────────────
260
  expl_score = _score_explanation(explanation)
261
+ score += expl_score
262
+ breakdown["explanation"] = round(expl_score, 4)
263
 
264
+ # ── 5. Confidence (0.05) ─────────────────────────────────────
265
+ confidence = _safe_get(payload, "confidence", None)
266
+ conf_score = _score_confidence(confidence)
267
+ score += conf_score
268
+ breakdown["confidence"] = round(conf_score, 4)
269
 
270
+ # ── 6. Impact analysis bonus (0.05) ──────────────────────────
271
  impact = str(_safe_get(payload, "impact", "") or "")
272
+ if len(impact.strip()) > 20:
273
+ score += 0.05
274
+ breakdown["impact_analysis"] = 0.05
 
275
  feedback_parts.append("Impact analysis provided.")
276
+ else:
277
+ breakdown["impact_analysis"] = 0.0
278
 
279
+ # Clamp strictly between 0 and 1 exclusive
280
+ final_score = round(max(0.001, min(0.999, score)), 4)
281
+ feedback = " ".join(feedback_parts) if feedback_parts else "No valid response provided."
282
  return final_score, breakdown, feedback
283
 
284
 
285
+ def grade_hard(action: Action, ground_truth: dict) -> tuple[float, dict, str]:
286
  """
287
+ Hard task grader β€” performance issues.
288
+ Max score: 0.999 (strictly less than 1.0)
289
+ Frontier models expected ~0.10-0.20.
290
+ DETERMINISTIC: same input always returns same score.
291
  """
292
  if action is None or action.payload is None:
293
+ return 0.001, {"error": "null_action"}, "No action provided."
294
 
 
295
  payload = action.payload
296
  score = 0.0
297
  breakdown = {}
298
  feedback_parts = []
 
299
 
300
+ # ── 1. Query correctness (0.30) ──────────────────────────────
301
+ submitted_query = (
302
+ _safe_get(payload, "optimized_query", "")
303
+ or _safe_get(payload, "fixed_query", "")
304
+ or ""
305
+ )
306
+ expected_query = ground_truth.get("fixed_query", "")
307
+ similarity = _query_similarity(submitted_query, expected_query)
308
 
309
+ if similarity >= 1.0:
310
  fix_score = 0.30
311
  feedback_parts.append("Perfectly optimized query.")
312
  elif similarity >= 0.85:
 
322
  fix_score = 0.0
323
  feedback_parts.append("Query does not address the performance issue.")
324
 
 
325
  score += fix_score
326
+ breakdown["query_correctness"] = round(fix_score, 4)
327
 
328
+ # ── 2. Performance concept identification (0.30) ──────────────
329
+ explanation = str(_safe_get(payload, "explanation", "") or _safe_get(payload, "change_made", "") or "")
 
330
  optimization = str(_safe_get(payload, "optimization_type", "") or "")
331
  combined_text = (explanation + " " + optimization).lower()
332
  perf_issue = ground_truth.get("performance_issue", {})
333
+ issue_type = perf_issue.get("type", "").lower()
 
334
 
335
  performance_concept_map = {
336
+ "n+1": ["n+1", "correlated subquery", "subquery per row", "multiple queries", "join instead"],
337
+ "full table scan": ["full table scan", "index not used", "function on column", "sargable", "range scan", "seek"],
338
+ "cartesian product": ["cartesian", "cross join", "missing join condition", "implicit join", "comma join"],
339
+ "select *": ["select *", "over-fetch", "covering index", "column projection", "unnecessary columns"],
340
+ "window function": ["window function", "partition by", "row_number", "subquery filter", "where clause window"]
 
 
 
 
 
 
 
341
  }
342
 
343
  concept_score = 0.0
344
  for concept, keywords in performance_concept_map.items():
345
+ if any(concept_part in issue_type for concept_part in concept.split()):
346
  hits = sum(1 for kw in keywords if kw in combined_text)
347
  concept_score = min(hits * 0.06, 0.30)
348
  break
349
 
 
350
  score += concept_score
351
+ breakdown["performance_concept"] = round(concept_score, 4)
352
  if concept_score > 0:
353
  feedback_parts.append("Demonstrates understanding of the performance issue.")
354
 
355
+ # ── 3. Explanation depth (0.15) ───────────────────────────────
356
  expl_score = _score_explanation(explanation)
357
  if len(explanation.strip()) > 150:
358
  expl_score = min(expl_score + 0.05, 0.15)
 
359
  score += expl_score
360
+ breakdown["explanation_depth"] = round(expl_score, 4)
361
 
362
+ # ── 4. Root cause analysis (0.10) ─────────────────────────────
363
  root_cause = str(_safe_get(payload, "root_cause", "") or "")
364
+ if len(root_cause.strip()) > 30:
365
+ score += 0.10
366
+ breakdown["root_cause_analysis"] = 0.10
 
367
  feedback_parts.append("Root cause analysis provided.")
368
+ else:
369
+ breakdown["root_cause_analysis"] = 0.0
370
 
371
+ # ── 5. Expected improvement (0.10) ────────────────────────────
372
  improvement = str(_safe_get(payload, "expected_improvement", "") or "")
373
+ if len(improvement.strip()) > 20:
374
+ score += 0.10
375
+ breakdown["expected_improvement"] = 0.10
 
376
  feedback_parts.append("Performance improvement estimate provided.")
377
+ else:
378
+ breakdown["expected_improvement"] = 0.0
379
 
380
+ # ── 6. Confidence (0.05) ──────────────────────────────────────
381
+ confidence = _safe_get(payload, "confidence", None)
382
+ conf_score = _score_confidence(confidence)
383
+ score += conf_score
384
+ breakdown["confidence"] = round(conf_score, 4)
385
 
386
+ # Clamp strictly between 0 and 1 exclusive
387
+ final_score = round(max(0.001, min(0.999, score)), 4)
388
+ feedback = " ".join(feedback_parts) if feedback_parts else "Performance issue not identified."
389
  return final_score, breakdown, feedback
390
 
391
 
 
393
  # MAIN GRADER DISPATCHER
394
  # ─────────────────���───────────────────────────
395
 
396
+ def grade(action: Action, task_id: str) -> tuple[float, dict, str]:
397
  """
398
+ Main grader entry point.
399
+ Looks up ground truth, dispatches to correct grader by difficulty.
400
+ ALWAYS returns (float, dict, str) β€” never crashes.
401
+ Score is always strictly between 0.001 and 0.999.
402
  """
403
  if action is None:
404
+ return 0.001, {"error": "null_action"}, "No action provided."
405
 
406
  ground_truth = task_manager.get_ground_truth(task_id)
407
  if ground_truth is None:
408
+ return 0.001, {"error": "unknown_task"}, f"Task '{task_id}' not found."
409
 
410
  difficulty = ground_truth.get("id", "").split("_")[0]
411
 
412
  try:
413
  if difficulty == "easy":
414
+ return grade_easy(action, ground_truth)
415
  elif difficulty == "medium":
416
+ return grade_medium(action, ground_truth)
417
  elif difficulty == "hard":
418
+ return grade_hard(action, ground_truth)
419
  else:
420
+ return 0.001, {"error": "unknown_difficulty"}, f"Unknown difficulty: {difficulty}"
 
 
 
 
 
 
 
 
 
 
 
 
421
  except Exception as e:
422
+ return 0.001, {"error": str(e)}, f"Grader error: {str(e)}"
inference.py CHANGED
@@ -11,10 +11,11 @@ from typing import List, Optional
11
 
12
  from openai import OpenAI
13
  from dotenv import load_dotenv
14
- load_dotenv()
15
 
16
  from env.environment import SQLDebuggerEnvironment
17
  from env.models import Action, ActionType, DifficultyLevel
 
18
  # ─────────────────────────────────────────────
19
  # ENVIRONMENT VARIABLES
20
  # ─────────────────────────────────────────────
@@ -25,20 +26,34 @@ HF_TOKEN = os.getenv("HF_TOKEN")
25
  if HF_TOKEN is None:
26
  raise ValueError("HF_TOKEN environment variable is required")
27
 
28
- API_KEY = HF_TOKEN
29
- BENCHMARK = "sql-query-debugger"
30
- MAX_STEPS = 10
31
  SUCCESS_SCORE_THRESHOLD = 0.5
 
 
 
32
  # ─────────────────────────────────────────────
33
 
34
  def log_start(task: str, env: str, model: str) -> None:
35
  print(f"[START] task={task} env={env} model={model}", flush=True)
36
 
37
 
38
- def log_end(success: bool, steps: int, score: float, rewards: List[float]) -> None:
 
 
 
 
 
 
39
  rewards_str = ",".join(f"{r:.2f}" for r in rewards)
40
  print(f"[END] success={str(success).lower()} steps={steps} rewards={rewards_str}", flush=True)
41
 
 
 
 
 
 
42
  SYSTEM_PROMPT = textwrap.dedent("""
43
  You are an expert SQL debugger. You will be given a buggy SQL query and must fix it.
44
 
@@ -113,7 +128,6 @@ def get_llm_action(client: OpenAI, obs, step: int) -> Action:
113
  )
114
  text = (completion.choices[0].message.content or "").strip()
115
 
116
- # Parse JSON response
117
  # Remove markdown code blocks if present
118
  if "```" in text:
119
  text = text.split("```")[1]
@@ -150,7 +164,6 @@ def get_llm_action(client: OpenAI, obs, step: int) -> Action:
150
 
151
  except Exception as exc:
152
  print(f"[DEBUG] LLM call failed: {exc}", flush=True)
153
- # Fallback to identify_error action
154
  return Action(
155
  action_type=ActionType.IDENTIFY_ERROR,
156
  payload={
@@ -162,17 +175,17 @@ def get_llm_action(client: OpenAI, obs, step: int) -> Action:
162
 
163
 
164
  # ─────────────────────────────────────────────
165
- # MAIN INFERENCE LOOP
166
  # ─────────────────────────────────────────────
167
 
168
  def run_episode(client: OpenAI, difficulty: str, task_id: str) -> dict:
169
  """Run one full episode and return results."""
170
- env = SQLDebuggerEnvironment()
171
- obs = env.reset(difficulty=difficulty, task_id=task_id)
172
- rewards = []
173
- steps = 0
174
- success = False
175
- score = 0.001 # Initialize to minimum valid score
176
 
177
  log_start(task=task_id, env=BENCHMARK, model=MODEL_NAME)
178
 
@@ -181,10 +194,9 @@ def run_episode(client: OpenAI, difficulty: str, task_id: str) -> dict:
181
  if env.state().done:
182
  break
183
 
184
- # Get action from LLM
185
- action = get_llm_action(client, obs, step)
186
- action_str = f"{action.action_type.value}"
187
- error_str = None
188
 
189
  try:
190
  resp = env.step(action)
@@ -192,8 +204,8 @@ def run_episode(client: OpenAI, difficulty: str, task_id: str) -> dict:
192
  done = resp.done
193
  obs = resp.observation
194
  except Exception as e:
195
- reward = -0.1
196
- done = False
197
  error_str = str(e)[:100]
198
 
199
  rewards.append(reward)
@@ -210,20 +222,22 @@ def run_episode(client: OpenAI, difficulty: str, task_id: str) -> dict:
210
  if done:
211
  break
212
 
213
- # Calculate score
214
  total_reward = sum(rewards)
215
- score = min(max(total_reward / MAX_STEPS, 0.001), 0.999)
216
- success = score >= SUCCESS_SCORE_THRESHOLD
 
 
217
 
218
  except Exception as e:
219
  print(f"[DEBUG] Episode error: {e}", flush=True)
220
- error_str = str(e)[:100]
 
221
 
222
  finally:
223
  log_end(
224
  success = success,
225
  steps = steps,
226
- score = score,
227
  rewards = rewards
228
  )
229
 
@@ -236,6 +250,10 @@ def run_episode(client: OpenAI, difficulty: str, task_id: str) -> dict:
236
  }
237
 
238
 
 
 
 
 
239
  def main():
240
  """Main entry point β€” runs inference on all 3 difficulty levels."""
241
  print(f"[DEBUG] API_BASE_URL={API_BASE_URL}", flush=True)
@@ -254,7 +272,6 @@ def main():
254
  result = run_episode(client, difficulty, task_id)
255
  results.append(result)
256
 
257
- # Final summary
258
  avg_score = sum(r["score"] for r in results) / len(results)
259
  print(f"\n[DEBUG] Average Score: {avg_score:.3f}", flush=True)
260
  for r in results:
 
11
 
12
  from openai import OpenAI
13
  from dotenv import load_dotenv
14
+ load_dotenv()
15
 
16
  from env.environment import SQLDebuggerEnvironment
17
  from env.models import Action, ActionType, DifficultyLevel
18
+
19
  # ─────────────────────────────────────────────
20
  # ENVIRONMENT VARIABLES
21
  # ─────────────────────────────────────────────
 
26
  if HF_TOKEN is None:
27
  raise ValueError("HF_TOKEN environment variable is required")
28
 
29
+ API_KEY = HF_TOKEN
30
+ BENCHMARK = "sql-query-debugger"
31
+ MAX_STEPS = 10
32
  SUCCESS_SCORE_THRESHOLD = 0.5
33
+
34
+ # ─────────────────────────────────────────────
35
+ # LOGGING FUNCTIONS
36
  # ─────────────────────────────────────────────
37
 
38
  def log_start(task: str, env: str, model: str) -> None:
39
  print(f"[START] task={task} env={env} model={model}", flush=True)
40
 
41
 
42
+ def log_step(step: int, action: str, reward: float, done: bool, error: Optional[str]) -> None:
43
+ error_val = error if error else "null"
44
+ done_val = str(done).lower()
45
+ print(f"[STEP] step={step} action={action} reward={reward:.2f} done={done_val} error={error_val}", flush=True)
46
+
47
+
48
+ def log_end(success: bool, steps: int, rewards: List[float]) -> None:
49
  rewards_str = ",".join(f"{r:.2f}" for r in rewards)
50
  print(f"[END] success={str(success).lower()} steps={steps} rewards={rewards_str}", flush=True)
51
 
52
+
53
+ # ─────────────────────────────────────────────
54
+ # SYSTEM PROMPT
55
+ # ─────────────────────────────────────────────
56
+
57
  SYSTEM_PROMPT = textwrap.dedent("""
58
  You are an expert SQL debugger. You will be given a buggy SQL query and must fix it.
59
 
 
128
  )
129
  text = (completion.choices[0].message.content or "").strip()
130
 
 
131
  # Remove markdown code blocks if present
132
  if "```" in text:
133
  text = text.split("```")[1]
 
164
 
165
  except Exception as exc:
166
  print(f"[DEBUG] LLM call failed: {exc}", flush=True)
 
167
  return Action(
168
  action_type=ActionType.IDENTIFY_ERROR,
169
  payload={
 
175
 
176
 
177
  # ─────────────────────────────────────────────
178
+ # EPISODE RUNNER
179
  # ─────────────────────────────────────────────
180
 
181
  def run_episode(client: OpenAI, difficulty: str, task_id: str) -> dict:
182
  """Run one full episode and return results."""
183
+ env = SQLDebuggerEnvironment()
184
+ obs = env.reset(difficulty=difficulty, task_id=task_id)
185
+ rewards = []
186
+ steps = 0
187
+ success = False
188
+ score = 0.0
189
 
190
  log_start(task=task_id, env=BENCHMARK, model=MODEL_NAME)
191
 
 
194
  if env.state().done:
195
  break
196
 
197
+ action = get_llm_action(client, obs, step)
198
+ action_str = action.action_type.value
199
+ error_str = None
 
200
 
201
  try:
202
  resp = env.step(action)
 
204
  done = resp.done
205
  obs = resp.observation
206
  except Exception as e:
207
+ reward = -0.1
208
+ done = False
209
  error_str = str(e)[:100]
210
 
211
  rewards.append(reward)
 
222
  if done:
223
  break
224
 
225
+ # Score must be strictly between 0 and 1 (not 0.0, not 1.0)
226
  total_reward = sum(rewards)
227
+ raw_score = total_reward / MAX_STEPS if MAX_STEPS > 0 else 0.0
228
+ # Clamp strictly between 0 and 1 exclusive
229
+ score = max(0.001, min(0.999, raw_score))
230
+ success = score >= SUCCESS_SCORE_THRESHOLD
231
 
232
  except Exception as e:
233
  print(f"[DEBUG] Episode error: {e}", flush=True)
234
+ score = 0.001
235
+ success = False
236
 
237
  finally:
238
  log_end(
239
  success = success,
240
  steps = steps,
 
241
  rewards = rewards
242
  )
243
 
 
250
  }
251
 
252
 
253
+ # ─────────────────────────────────────────────
254
+ # MAIN
255
+ # ─────────────────────────────────────────────
256
+
257
  def main():
258
  """Main entry point β€” runs inference on all 3 difficulty levels."""
259
  print(f"[DEBUG] API_BASE_URL={API_BASE_URL}", flush=True)
 
272
  result = run_episode(client, difficulty, task_id)
273
  results.append(result)
274
 
 
275
  avg_score = sum(r["score"] for r in results) / len(results)
276
  print(f"\n[DEBUG] Average Score: {avg_score:.3f}", flush=True)
277
  for r in results: