CoBeDigger commited on
Commit
a0bbcba
Β·
1 Parent(s): 2cf9a43

feat: semantic rewards on script state, raise step cap to 20, full-script rewrite support

Browse files
Files changed (3) hide show
  1. app/environment.py +2 -1
  2. app/reward.py +80 -81
  3. baseline/run_baseline.py +23 -22
app/environment.py CHANGED
@@ -308,7 +308,7 @@ class DataEngEnvironment:
308
  self.last_run_error = f"Action Handler Error: {exc}"
309
  message = self.last_run_error
310
 
311
- if self.step_number >= 60 or self.stage_step_number >= 15:
312
  self.done = True
313
  message = f"{message} Stage or episode step limit reached."
314
 
@@ -325,6 +325,7 @@ class DataEngEnvironment:
325
  },
326
  grader_score=score,
327
  step_number=self.step_number,
 
328
  )
329
 
330
  # For submit, override score but keep partial breakdown
 
308
  self.last_run_error = f"Action Handler Error: {exc}"
309
  message = self.last_run_error
310
 
311
+ if self.step_number >= 60 or self.stage_step_number >= 20:
312
  self.done = True
313
  message = f"{message} Stage or episode step limit reached."
314
 
 
325
  },
326
  grader_score=score,
327
  step_number=self.step_number,
328
+ current_script=self.current_script,
329
  )
330
 
331
  # For submit, override score but keep partial breakdown
app/reward.py CHANGED
@@ -6,12 +6,10 @@ from app.models import Reward
6
 
7
 
8
  class RewardEngine:
9
- """Dense partial reward shaping aligned to the 4-stage cascade.
10
 
11
- Stage 1 — Data Repair: rename age_years→age, dropna
12
- Stage 2 β€” Training Monitor: add StandardScaler before MLPClassifier
13
- Stage 3 β€” Eval Validation: move scaler.fit after train_test_split
14
- Stage 4 β€” Deploy Gate: add class_weight='balanced'
15
  """
16
 
17
  def __init__(self) -> None:
@@ -21,7 +19,7 @@ class RewardEngine:
21
  if task_id not in self._state:
22
  self._state[task_id] = {
23
  "awarded": {},
24
- "has_edited": False,
25
  }
26
  return self._state[task_id]
27
 
@@ -31,20 +29,16 @@ class RewardEngine:
31
  st["awarded"][key] = float(value)
32
  return True
33
 
34
- def _payload_text(self, payload: dict) -> str:
35
- parts = []
36
- for v in (payload or {}).values():
37
- if isinstance(v, str):
38
- parts.append(v)
39
- return "\n".join(parts).lower()
40
-
41
- def _no_error(self, observation: dict) -> bool:
42
- err = (observation or {}).get("last_run_error")
43
- return err is None or str(err).strip() == ""
44
-
45
  def _compute_score(self, st: Dict[str, Any]) -> float:
46
  total = float(sum(st["awarded"].values()))
47
- return max(0.01, min(0.99, total))
 
 
 
 
 
 
 
48
 
49
  def compute_reward(
50
  self,
@@ -54,118 +48,128 @@ class RewardEngine:
54
  observation: dict,
55
  grader_score: float,
56
  step_number: int,
 
57
  ) -> Reward:
58
  st = self._get_task_state(task_id)
59
  msg_parts = []
60
  action_type = (action_type or "").strip()
61
- payload_text = self._payload_text(action_payload)
62
 
63
- # ── query_actor β€” applies to ALL stages ──────────────────────
 
 
 
 
 
 
 
 
 
 
 
64
  if action_type == "query_actor":
65
  if self._add_partial(st, "actor_query", 0.1):
66
  msg_parts.append("+0.1 actor consulted")
67
  return Reward(
68
  score=self._compute_score(st),
69
  partial_rewards=dict(st["awarded"]),
70
- message=", ".join(msg_parts) if msg_parts else "actor already consulted",
71
  is_terminal=False,
72
  )
73
 
74
- # ── STAGE 1 β€” Data Repair ────────────────────────────────────
75
  if task_id == 1:
76
  if action_type in {"inspect_data", "check_schema"}:
77
  if self._add_partial(st, "s1_inspect", 0.1):
78
  msg_parts.append("S1: +0.1 inspect/schema")
79
 
 
80
  if action_type == "edit_script":
81
- st["has_edited"] = True
82
- # Check for column rename fix (age_years β†’ age)
83
- if "age" in payload_text and "age_years" not in payload_text:
84
- if self._add_partial(st, "s1_rename_fix", 0.2):
85
- msg_parts.append("S1: +0.2 column rename fix")
86
- # Check for NaN handling
87
- if "dropna" in payload_text or "fillna" in payload_text:
88
- if self._add_partial(st, "s1_nan_fix", 0.1):
89
- msg_parts.append("S1: +0.1 NaN handling added")
90
-
91
- if action_type == "run_script" and self._no_error(observation):
 
 
 
 
 
92
  if self._add_partial(st, "s1_clean_run", 0.2):
93
  msg_parts.append("S1: +0.2 clean run")
94
 
95
- # ── STAGE 2 β€” Training Monitor (StandardScaler) ─────────────
96
  elif task_id == 2:
97
- if action_type == "inspect_data":
98
  if self._add_partial(st, "s2_inspect", 0.1):
99
- msg_parts.append("S2: +0.1 data inspected")
100
 
101
  if action_type == "run_script":
102
- # Running reveals NaN loss / divergence
103
  if self._add_partial(st, "s2_run_diagnose", 0.1):
104
  msg_parts.append("S2: +0.1 divergence observed")
105
 
 
106
  if action_type == "edit_script":
107
- st["has_edited"] = True
108
- # Check for StandardScaler addition
109
- if "standardscaler" in payload_text or "standard_scaler" in payload_text:
110
  if self._add_partial(st, "s2_scaler_fix", 0.3):
111
- msg_parts.append("S2: +0.3 StandardScaler added")
112
- # Also accept MinMaxScaler or normalize
113
- elif "minmaxscaler" in payload_text or "normalize" in payload_text:
114
- if self._add_partial(st, "s2_scaler_fix", 0.2):
115
- msg_parts.append("S2: +0.2 alternative scaler added")
116
 
117
- if action_type == "run_script" and st.get("has_edited") and self._no_error(observation):
 
 
118
  if self._add_partial(st, "s2_clean_run", 0.2):
119
  msg_parts.append("S2: +0.2 clean run after fix")
120
 
121
- # ── STAGE 3 β€” Eval Validation (Data Leakage) ────────────────
122
  elif task_id == 3:
123
- if action_type == "inspect_data":
124
  if self._add_partial(st, "s3_inspect", 0.1):
125
- msg_parts.append("S3: +0.1 data inspected")
126
 
127
  if action_type == "run_script":
128
- # Running reveals suspicious 98% accuracy
129
  if self._add_partial(st, "s3_run_suspicious", 0.1):
130
  msg_parts.append("S3: +0.1 suspicious accuracy observed")
131
 
 
132
  if action_type == "edit_script":
133
- st["has_edited"] = True
134
- txt = payload_text
135
- # Detect: train_test_split appears before scaler.fit
136
- idx_split = txt.find("train_test_split")
137
- idx_fit = min(
138
- [i for i in [txt.find("scaler.fit("), txt.find("fit_transform(")] if i != -1]
139
- or [-1]
140
- )
141
  if idx_split != -1 and idx_fit != -1 and idx_split < idx_fit:
142
  if self._add_partial(st, "s3_leakage_fix", 0.3):
143
- msg_parts.append("S3: +0.3 fit moved after split")
144
- elif idx_split != -1 or idx_fit != -1:
145
- if self._add_partial(st, "s3_partial_edit", 0.1):
146
- msg_parts.append("S3: +0.1 relevant edit attempt")
147
 
148
- if action_type == "run_script" and st.get("has_edited") and self._no_error(observation):
 
149
  if self._add_partial(st, "s3_clean_run", 0.2):
150
  msg_parts.append("S3: +0.2 clean run after fix")
151
 
152
- if action_type == "submit" and not st.get("has_edited"):
153
- self._add_partial(st, "s3_submit_no_edit", -0.2)
154
- msg_parts.append("S3: -0.2 submitted without editing")
155
 
156
- # ── STAGE 4 β€” Deploy Gate (Fairness) ─────────────────────────
157
  elif task_id == 4:
158
  if action_type == "inspect_data":
159
  if self._add_partial(st, "s4_inspect", 0.1):
160
- msg_parts.append("S4: +0.1 data inspected")
161
 
 
162
  if action_type == "edit_script":
163
- st["has_edited"] = True
164
- payload_str = str(action_payload)
165
- if "class_weight" in payload_str:
166
  if self._add_partial(st, "s4_fairness_fix", 0.3):
167
- msg_parts.append("S4: +0.3 class_weight balanced added")
168
- elif "stratify" in payload_str:
169
  if self._add_partial(st, "s4_stratify", 0.1):
170
  msg_parts.append("S4: +0.1 stratified sampling")
171
  else:
@@ -176,21 +180,16 @@ class RewardEngine:
176
  if self._add_partial(st, "s4_run", 0.1):
177
  msg_parts.append("S4: +0.1 script executed")
178
 
179
- # ── Terminal / Score ──────────────────────────────────────────
180
  is_terminal = action_type == "submit"
181
- if is_terminal:
182
- total_score = float(grader_score)
183
- else:
184
- total_score = self._compute_score(st)
185
-
186
  total_score = max(0.0, min(1.0, total_score))
187
 
188
  message = ", ".join(msg_parts) if msg_parts else "no partial reward"
189
- cumulative_partials = {k: float(v) for k, v in st["awarded"].items()}
190
 
191
  return Reward(
192
  score=float(total_score),
193
- partial_rewards=cumulative_partials,
194
  message=message,
195
  is_terminal=is_terminal,
196
  )
 
6
 
7
 
8
  class RewardEngine:
9
+ """Dense semantic reward shaping for 4-stage cascade.
10
 
11
+ Key principle: reward is based on RESULTING SCRIPT STATE,
12
+ not exact payload text. This lets any valid fix earn reward.
 
 
13
  """
14
 
15
  def __init__(self) -> None:
 
19
  if task_id not in self._state:
20
  self._state[task_id] = {
21
  "awarded": {},
22
+ "action_history": [],
23
  }
24
  return self._state[task_id]
25
 
 
29
  st["awarded"][key] = float(value)
30
  return True
31
 
 
 
 
 
 
 
 
 
 
 
 
32
  def _compute_score(self, st: Dict[str, Any]) -> float:
33
  total = float(sum(st["awarded"].values()))
34
+ return max(0.0, min(0.99, total))
35
+
36
+ def _repeat_penalty(self, st: Dict[str, Any], action_type: str) -> float:
37
+ """Small penalty for repeating the same action consecutively."""
38
+ history = st["action_history"]
39
+ if len(history) >= 2 and history[-1] == action_type and history[-2] == action_type:
40
+ return -0.03
41
+ return 0.0
42
 
43
  def compute_reward(
44
  self,
 
48
  observation: dict,
49
  grader_score: float,
50
  step_number: int,
51
+ current_script: str = "",
52
  ) -> Reward:
53
  st = self._get_task_state(task_id)
54
  msg_parts = []
55
  action_type = (action_type or "").strip()
 
56
 
57
+ # Track action history for repeat penalty
58
+ st["action_history"].append(action_type)
59
+
60
+ # Check for repeat penalty
61
+ penalty = self._repeat_penalty(st, action_type)
62
+ if penalty < 0:
63
+ self._add_partial(st, f"repeat_penalty_{step_number}", penalty)
64
+ msg_parts.append(f"{penalty:.2f} repeated action")
65
+
66
+ script_lower = current_script.lower()
67
+
68
+ # ── query_actor β€” all stages ─────────────────────────────
69
  if action_type == "query_actor":
70
  if self._add_partial(st, "actor_query", 0.1):
71
  msg_parts.append("+0.1 actor consulted")
72
  return Reward(
73
  score=self._compute_score(st),
74
  partial_rewards=dict(st["awarded"]),
75
+ message=", ".join(msg_parts) or "actor already consulted",
76
  is_terminal=False,
77
  )
78
 
79
+ # ── STAGE 1 β€” Data Repair ────────────────────────────────
80
  if task_id == 1:
81
  if action_type in {"inspect_data", "check_schema"}:
82
  if self._add_partial(st, "s1_inspect", 0.1):
83
  msg_parts.append("S1: +0.1 inspect/schema")
84
 
85
+ # Semantic check on RESULTING SCRIPT after edit
86
  if action_type == "edit_script":
87
+ # Column fix: age_years no longer in script
88
+ if "age_years" not in script_lower and "age" in script_lower:
89
+ if self._add_partial(st, "s1_column_fix", 0.2):
90
+ msg_parts.append("S1: +0.2 column bug fixed")
91
+ # NaN handling present in script
92
+ if any(kw in script_lower for kw in ["dropna", "fillna", "simpleimputer", "isna", "isnull"]):
93
+ if self._add_partial(st, "s1_nan_fix", 0.15):
94
+ msg_parts.append("S1: +0.15 NaN handling added")
95
+ # Outlier handling present in script
96
+ if any(kw in script_lower for kw in ["clip", "quantile", "robustscaler", "winsor"]):
97
+ if self._add_partial(st, "s1_outlier_fix", 0.15):
98
+ msg_parts.append("S1: +0.15 outlier handling added")
99
+
100
+ # Clean run
101
+ no_error = observation.get("last_run_error") is None or str(observation.get("last_run_error", "")).strip() == ""
102
+ if action_type == "run_script" and no_error:
103
  if self._add_partial(st, "s1_clean_run", 0.2):
104
  msg_parts.append("S1: +0.2 clean run")
105
 
106
+ # ── STAGE 2 β€” Training Monitor (StandardScaler) ─────────
107
  elif task_id == 2:
108
+ if action_type in {"inspect_data", "check_schema"}:
109
  if self._add_partial(st, "s2_inspect", 0.1):
110
+ msg_parts.append("S2: +0.1 inspected")
111
 
112
  if action_type == "run_script":
 
113
  if self._add_partial(st, "s2_run_diagnose", 0.1):
114
  msg_parts.append("S2: +0.1 divergence observed")
115
 
116
+ # Semantic: does script now contain a scaler?
117
  if action_type == "edit_script":
118
+ if any(kw in script_lower for kw in ["standardscaler", "minmaxscaler", "robustscaler", "normalize"]):
 
 
119
  if self._add_partial(st, "s2_scaler_fix", 0.3):
120
+ msg_parts.append("S2: +0.3 scaler added")
 
 
 
 
121
 
122
+ # Clean run after edit
123
+ no_error = observation.get("last_run_error") is None or str(observation.get("last_run_error", "")).strip() == ""
124
+ if action_type == "run_script" and "s2_scaler_fix" in st["awarded"] and no_error:
125
  if self._add_partial(st, "s2_clean_run", 0.2):
126
  msg_parts.append("S2: +0.2 clean run after fix")
127
 
128
+ # ── STAGE 3 β€” Eval Validation (Data Leakage) ────────────
129
  elif task_id == 3:
130
+ if action_type in {"inspect_data", "check_schema"}:
131
  if self._add_partial(st, "s3_inspect", 0.1):
132
+ msg_parts.append("S3: +0.1 inspected")
133
 
134
  if action_type == "run_script":
 
135
  if self._add_partial(st, "s3_run_suspicious", 0.1):
136
  msg_parts.append("S3: +0.1 suspicious accuracy observed")
137
 
138
+ # Semantic: is fit/fit_transform now AFTER train_test_split?
139
  if action_type == "edit_script":
140
+ idx_split = script_lower.find("train_test_split")
141
+ fit_positions = [script_lower.find("scaler.fit("), script_lower.find("fit_transform(")]
142
+ fit_positions = [p for p in fit_positions if p != -1]
143
+ idx_fit = min(fit_positions) if fit_positions else -1
144
+
 
 
 
145
  if idx_split != -1 and idx_fit != -1 and idx_split < idx_fit:
146
  if self._add_partial(st, "s3_leakage_fix", 0.3):
147
+ msg_parts.append("S3: +0.3 leakage fixed (fit after split)")
148
+ elif idx_split != -1:
149
+ if self._add_partial(st, "s3_partial_edit", 0.05):
150
+ msg_parts.append("S3: +0.05 relevant edit attempt")
151
 
152
+ no_error = observation.get("last_run_error") is None or str(observation.get("last_run_error", "")).strip() == ""
153
+ if action_type == "run_script" and "s3_leakage_fix" in st["awarded"] and no_error:
154
  if self._add_partial(st, "s3_clean_run", 0.2):
155
  msg_parts.append("S3: +0.2 clean run after fix")
156
 
157
+ if action_type == "submit" and "s3_leakage_fix" not in st["awarded"]:
158
+ self._add_partial(st, "s3_no_edit_penalty", -0.2)
159
+ msg_parts.append("S3: -0.2 submitted without fixing leakage")
160
 
161
+ # ── STAGE 4 β€” Deploy Gate (Fairness) ─────────────────────
162
  elif task_id == 4:
163
  if action_type == "inspect_data":
164
  if self._add_partial(st, "s4_inspect", 0.1):
165
+ msg_parts.append("S4: +0.1 inspected")
166
 
167
+ # Semantic: does script now contain class_weight?
168
  if action_type == "edit_script":
169
+ if "class_weight" in script_lower:
 
 
170
  if self._add_partial(st, "s4_fairness_fix", 0.3):
171
+ msg_parts.append("S4: +0.3 fairness fix applied")
172
+ elif "stratify" in script_lower:
173
  if self._add_partial(st, "s4_stratify", 0.1):
174
  msg_parts.append("S4: +0.1 stratified sampling")
175
  else:
 
180
  if self._add_partial(st, "s4_run", 0.1):
181
  msg_parts.append("S4: +0.1 script executed")
182
 
183
+ # ── Score ─────────────────────────────────────────────────
184
  is_terminal = action_type == "submit"
185
+ total_score = float(grader_score) if is_terminal else self._compute_score(st)
 
 
 
 
186
  total_score = max(0.0, min(1.0, total_score))
187
 
188
  message = ", ".join(msg_parts) if msg_parts else "no partial reward"
 
189
 
190
  return Reward(
191
  score=float(total_score),
192
+ partial_rewards=dict(st["awarded"]),
193
  message=message,
194
  is_terminal=is_terminal,
195
  )
baseline/run_baseline.py CHANGED
@@ -10,32 +10,33 @@ import os, time, json, re, requests
10
  BASE_URL = os.environ.get("DATAENGENV_URL", "http://localhost:8000")
11
  GROQ_API_KEY = os.environ.get("GROQ_API_KEY", "")
12
 
13
- SYSTEM_PROMPT = """You are a senior Data Engineer. A broken ML pipeline must be fixed.
14
-
15
- You will receive the script, error logs, and data info. You must fix the bug.
16
 
17
  RESPOND WITH ONLY A JSON OBJECT. No explanation. No markdown.
18
 
19
  Actions:
20
- {"action_type": "inspect_data", "payload": {}}
21
- {"action_type": "run_script", "payload": {}}
22
- {"action_type": "edit_script", "payload": {"old": "exact text in script", "new": "replacement"}}
23
- {"action_type": "query_actor", "payload": {}}
24
- {"action_type": "submit", "payload": {}}
25
-
26
- CRITICAL RULES:
27
- - Use inspect_data ONLY ONCE per stage, then move on
28
- - After inspecting, use edit_script to fix the bug
29
- - The "old" field must contain text that EXACTLY exists in the script
30
- - After editing, use run_script to verify
31
- - After a clean run, use submit
32
- - NEVER repeat the same action twice in a row
33
-
34
- Common fixes:
35
- - KeyError 'age_years': the column was renamed to 'age'. Replace 'age_years' with 'age'. Also add df.dropna(inplace=True)
36
- - Training diverges / loss NaN: features are unscaled. Add StandardScaler before the classifier
37
- - Suspiciously high accuracy (0.98+): data leakage. Move scaler.fit() to AFTER train_test_split()
38
- - Fairness rejection: add class_weight='balanced' to the classifier"""
 
 
 
39
 
40
 
41
  def parse_action(raw_text: str) -> dict:
 
10
  BASE_URL = os.environ.get("DATAENGENV_URL", "http://localhost:8000")
11
  GROQ_API_KEY = os.environ.get("GROQ_API_KEY", "")
12
 
13
+ SYSTEM_PROMPT = """You are debugging a broken ML pipeline. Fix it fast.
 
 
14
 
15
  RESPOND WITH ONLY A JSON OBJECT. No explanation. No markdown.
16
 
17
  Actions:
18
+ 1. {"action_type": "inspect_data", "payload": {}}
19
+ 2. {"action_type": "run_script", "payload": {}}
20
+ 3. {"action_type": "edit_script", "payload": {"old": "exact line from script", "new": "replacement"}}
21
+ 4. {"action_type": "edit_script", "payload": {"script": "FULL corrected script here"}}
22
+ 5. {"action_type": "query_actor", "payload": {}}
23
+ 6. {"action_type": "submit", "payload": {}}
24
+
25
+ STRATEGY (follow this exact order):
26
+ 1. Inspect data OR run script ONCE to see the error
27
+ 2. Use edit_script to fix the bug β€” prefer option 4 (full script) if unsure about exact text match
28
+ 3. Run script ONCE to verify fix
29
+ 4. Submit immediately if no error
30
+
31
+ RULES:
32
+ - Maximum 5 actions per stage: inspect, edit, run, submit
33
+ - NEVER inspect more than once
34
+ - NEVER run more than twice
35
+ - Submit as soon as output looks correct
36
+ - If you see KeyError 'age_years': rename to 'age' and add df.dropna(inplace=True) before feature selection
37
+ - If loss is NaN: add StandardScaler before classifier
38
+ - If accuracy suspiciously high (>0.95): move scaler.fit() to AFTER train_test_split()
39
+ - If fairness issue: add class_weight='balanced' to classifier"""
40
 
41
 
42
  def parse_action(raw_text: str) -> dict: