rak2315 commited on
Commit
4108ae8
Β·
1 Parent(s): ffa0040

add 6 tasks, fix log format, multi-turn retry, grader improvements

Browse files
README.md CHANGED
@@ -18,53 +18,120 @@ tags:
18
 
19
  > **Meta Γ— PyTorch Γ— Scaler OpenEnv Hackathon β€” April 2026**
20
 
21
- An [OpenEnv](https://github.com/meta-pytorch/OpenEnv) reinforcement-learning environment where AI agents debug broken PyTorch training scripts. The agent receives a buggy Python script plus its failure output, then must return a **corrected script**. The grader executes the fix in an isolated subprocess and scores it **0.0 β†’ 1.0** with partial credit at every verification stage.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
22
 
23
  ---
24
 
25
  ## 🎯 Tasks
26
 
27
- | task\_id | Difficulty | What's Broken | Success Criteria |
 
 
28
  |---|---|---|---|
29
- | `shape_mismatch` | 🟒 Easy | `nn.Linear` input dim is wrong β†’ explicit `RuntimeError` crash | Trains 3 epochs, no `RuntimeError` |
30
- | `training_collapse` | 🟑 Medium | Huge LR β†’ NaN loss **or** wrong loss fn β†’ flat plateau (no crash) | Loss decreases across all epochs, no NaN |
31
- | `data_leakage` | πŸ”΄ Hard | Dataset normalized before train/test split β†’ inflated metrics, no error | Norm stats computed from train set only; metrics reflect true generalisation |
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
32
 
33
  ---
34
 
35
  ## πŸ† Scoring Ladder
36
 
37
- | Score | Condition |
38
- |---|---|
39
- | **0.0** | Wrong `bug_type` identified |
40
- | **0.2** | Correct `bug_type`, fixed code **crashes** |
41
- | **0.4** | Correct `bug_type`, runs but training doesn't finish |
42
- | **0.6** | Runs + finishes, root cause **not** actually fixed |
43
- | **0.8** | Root cause fixed, success signal not confirmed in output |
44
- | **1.0** | Perfect fix β€” runs, finishes, success signal confirmed βœ… |
 
 
 
 
45
 
46
  ---
47
 
48
  ## πŸ“ Action Space
49
 
50
- The agent returns a single JSON action:
51
 
52
- | Field | Type | Description |
 
 
 
 
 
 
 
 
53
  |---|---|---|
54
- | `bug_type` | `str` | One of: `shape_mismatch`, `training_collapse`, `data_leakage`, `other` |
55
  | `diagnosis` | `str` | Plain-language explanation of the root cause |
56
- | `fixed_code` | `str` | Complete corrected Python script, runnable as-is (all imports included) |
 
 
57
 
58
  ## πŸ‘ Observation Space
59
 
 
 
60
  | Field | Type | Description |
61
  |---|---|---|
62
- | `task_id` | `str` | Active task identifier |
63
  | `task_description` | `str` | Natural language description of what to fix |
64
  | `buggy_code` | `str` | The broken Python script |
65
- | `error_output` | `str` | stderr / traceback or behavioural failure description |
66
  | `execution_result` | `str \| None` | stdout+stderr from running the agent's fix (`None` on reset) |
67
- | `grader_score` | `float \| None` | Score 0.0–1.0 (`None` on reset) |
68
  | `grader_feedback` | `str \| None` | Human-readable explanation of the score |
69
  | `step_number` | `int` | Current step within this episode |
70
  | `done` | `bool` | Whether the episode has ended |
@@ -74,32 +141,35 @@ The agent returns a single JSON action:
74
 
75
  ## πŸš€ Quick Start
76
 
 
 
77
  ```bash
78
- pip install git+https://huggingface.co/spaces/rehaan/ml-debug-env
79
  ```
80
 
 
 
81
  ```python
82
  import asyncio
83
  from ml_debug_env import MlDebugEnvClient, DebugAction
84
 
85
- SPACE_URL = "https://rehaan-ml-debug-env.hf.space"
86
 
87
  async def main():
88
  async with MlDebugEnvClient(base_url=SPACE_URL) as env:
89
- # Reset β€” get the buggy script
90
  result = await env.reset()
91
  obs = result.observation
 
92
  print(f"Task: {obs.task_id}")
 
93
  print(f"Error:\n{obs.error_output}\n")
94
 
95
- # Step β€” submit a fix
96
  action = DebugAction(
97
  bug_type="shape_mismatch",
98
  diagnosis="nn.Linear classifier input dim is 32 but encoder outputs 256",
99
- fixed_code=obs.buggy_code.replace(
100
- "nn.Linear(32,", "nn.Linear(256,"
101
- ),
102
  )
 
103
  result = await env.step(action)
104
  print(f"Score: {result.observation.grader_score}")
105
  print(f"Feedback: {result.observation.grader_feedback}")
@@ -107,18 +177,20 @@ async def main():
107
  asyncio.run(main())
108
  ```
109
 
110
- **Synchronous usage:**
111
 
112
  ```python
113
  from ml_debug_env import MlDebugEnvClient, DebugAction
114
 
 
 
115
  with MlDebugEnvClient(base_url=SPACE_URL).sync() as env:
116
  result = env.reset()
117
  obs = result.observation
118
 
119
  result = env.step(DebugAction(
120
  bug_type="training_collapse",
121
- diagnosis="Learning rate 100.0 causes gradient explosion β†’ NaN",
122
  fixed_code=obs.buggy_code.replace("lr=100.0", "lr=1e-3"),
123
  ))
124
  print(result.observation.grader_score)
@@ -128,16 +200,18 @@ with MlDebugEnvClient(base_url=SPACE_URL).sync() as env:
128
 
129
  ## 🌐 API Endpoints
130
 
 
 
131
  | Method | Path | Description |
132
  |---|---|---|
133
- | `POST` | `/reset` | Start a new debug episode |
134
- | `POST` | `/step` | Submit a fix attempt |
135
  | `GET` | `/state` | Get current episode state |
136
- | `GET` | `/health` | Health check (`{"status":"healthy"}`) |
137
- | `GET` | `/tasks` | List all 3 tasks + action schema |
138
- | `POST` | `/grader` | Score a fix without running a full episode |
139
- | `GET` | `/baseline` | Run Groq llama-3.3-70b-versatile baseline on all 3 tasks |
140
- | `WS` | `/ws` | Persistent WebSocket session (low-latency) |
141
  | `GET` | `/docs` | Interactive Swagger UI |
142
  | `GET` | `/schema` | Environment JSON schema |
143
 
@@ -145,40 +219,49 @@ with MlDebugEnvClient(base_url=SPACE_URL).sync() as env:
145
 
146
  ## πŸ“Š Baseline Results
147
 
148
- Baseline agent: `llama-3.3-70b-versatile` via Groq (zero-shot, single attempt)
149
 
150
- | task\_id | Score |
151
  |---|---|
152
- | `shape_mismatch` | TBD |
153
- | `training_collapse` | TBD |
154
- | `data_leakage` | TBD |
155
- | **Average** | **TBD** |
156
 
157
- > Run `/baseline` endpoint with `GROQ_API_KEY` set to populate these.
158
 
159
  ---
160
 
161
- ## πŸ›  Local Development
162
-
163
- ```bash
164
- git clone https://huggingface.co/spaces/rehaan/ml-debug-env
165
- cd ml-debug-env
166
-
167
- # Install deps
168
- pip install -e .
169
-
170
- # Run server
171
- uvicorn server.app:app --reload --host 0.0.0.0 --port 8000
172
 
173
- # Quick sanity test
174
- python test.py
175
  ```
176
-
177
- **With Docker:**
178
-
179
- ```bash
180
- docker build -f server/Dockerfile -t ml-debug-env:latest .
181
- docker run -e GROQ_API_KEY=your_key -p 8000:8000 ml-debug-env:latest
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
182
  ```
183
 
184
  ---
@@ -187,32 +270,101 @@ docker run -e GROQ_API_KEY=your_key -p 8000:8000 ml-debug-env:latest
187
 
188
  ```
189
  ml_debug_env/
 
190
  β”œβ”€β”€ __init__.py # Package exports
191
- β”œβ”€β”€ models.py # DebugAction, DebugObservation, DebugState
192
  β”œβ”€β”€ client.py # MlDebugEnvClient (EnvClient subclass)
 
193
  β”œβ”€β”€ openenv.yaml # OpenEnv manifest
194
  β”œβ”€β”€ pyproject.toml # Project metadata + deps
 
195
  β”œβ”€β”€ test.py # Sanity tests (no server needed)
 
196
  └── server/
197
- β”œβ”€β”€ app.py # FastAPI app (create_app + extra endpoints)
198
- β”œβ”€β”€ bug_generator.py # Generates buggy scripts with known fixes
199
- β”œβ”€β”€ grader.py # Executes fixed code, returns 0.0–1.0 score
200
- β”œβ”€β”€ ml_debug_env_environment.py # MlDebugEnvEnvironment (reset/step/state)
201
- β”œβ”€β”€ baseline_inference.py # Groq llama-3.3-70b-versatile baseline
202
- β”œβ”€β”€ Dockerfile # python:3.10-slim + CPU torch
203
  └── requirements.txt # Server runtime deps
204
  ```
205
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
206
  ---
207
 
208
  ## πŸ”‘ Environment Variables
209
 
210
  | Variable | Required | Description |
211
  |---|---|---|
212
- | `GROQ_API_KEY` | Optional | Enables `/baseline` endpoint (Groq free tier) |
 
 
 
 
 
213
 
214
  ---
215
 
216
  ## πŸ“„ License
217
 
218
- BSD 3-Clause β€” see [OpenEnv](https://github.com/meta-pytorch/OpenEnv/blob/main/LICENSE)
 
18
 
19
  > **Meta Γ— PyTorch Γ— Scaler OpenEnv Hackathon β€” April 2026**
20
 
21
+ An [OpenEnv](https://github.com/meta-pytorch/OpenEnv) reinforcement-learning environment where AI agents debug broken PyTorch training scripts. The agent receives a buggy Python script plus its failure output, then must return a **corrected script**. The grader executes the fix in an isolated subprocess and scores it **0.01 β†’ 0.99** with partial credit at every verification stage.
22
+
23
+ ---
24
+
25
+ ## 🧠 What Is This?
26
+
27
+ Most ML benchmarks test whether a model can *write* code. This environment tests something harder: can a model *debug* code?
28
+
29
+ The agent is dropped into a broken PyTorch training environment. It sees the buggy script, the error or behavioral failure, and must:
30
+ 1. Identify what category of bug is present
31
+ 2. Explain the root cause in plain English
32
+ 3. Return a complete, corrected, runnable Python script
33
+
34
+ The grader then **actually executes** the fixed code in a subprocess and scores it across 6 stages of increasing correctness. There is no cheating β€” the code has to run.
35
+
36
+ This is a realistic simulation of the debugging loop every ML engineer goes through daily.
37
 
38
  ---
39
 
40
  ## 🎯 Tasks
41
 
42
+ Three tasks of increasing difficulty, each representing a common class of real-world ML bugs:
43
+
44
+ | task_id | Difficulty | What's Broken | Why It's Hard |
45
  |---|---|---|---|
46
+ | `shape_mismatch` | 🟒 Easy | `nn.Linear` input dim is wrong β†’ explicit `RuntimeError` crash | Error is in the traceback. Read it. |
47
+ | `training_collapse` | 🟑 Medium | Huge LR β†’ NaN loss, **or** wrong loss fn β†’ flat plateau. No crash. | No error β€” agent must reason about training dynamics |
48
+ | `data_leakage` | πŸ”΄ Hard | Dataset normalized before train/test split β†’ inflated metrics, no error | Everything *looks* fine. Agent must understand data pipelines |
49
+
50
+ ### Task 1 β€” Shape Mismatch (Easy)
51
+
52
+ A `SimpleClassifier` has a 2-layer encoder that outputs `hidden_size` features (e.g. 256), but the classifier head is initialized with `nn.Linear(wrong_size, num_classes)` where `wrong_size` (e.g. 32) doesn't match. The forward pass crashes immediately with:
53
+
54
+ ```
55
+ RuntimeError: mat1 and mat2 shapes cannot be multiplied (256 cannot be broadcast to 32)
56
+ ```
57
+
58
+ The fix: change `nn.Linear(wrong_size, ...)` to `nn.Linear(hidden_size, ...)`.
59
+
60
+ The `hidden_size` and `wrong_size` values are randomized per episode seed so agents can't hardcode the fix.
61
+
62
+ ### Task 2 β€” Training Collapse (Medium)
63
+
64
+ Two variants, randomly selected:
65
+
66
+ **Variant A β€” Bad learning rate:** SGD with `lr=100.0` (or 10.0 or 50.0). Loss explodes to NaN by epoch 2. Code runs, no crash, just broken training. Fix: reduce lr to ~1e-3.
67
+
68
+ **Variant B β€” Wrong loss function:** Binary classification model with sigmoid output trained with `MSELoss` instead of `BCELoss`. Loss plateaus immediately around 0.25 and never decreases. Fix: switch to `BCELoss` or remove sigmoid and use `BCEWithLogitsLoss`.
69
+
70
+ The agent must distinguish between these variants from the error description alone.
71
+
72
+ ### Task 3 β€” Data Leakage (Hard)
73
+
74
+ Two variants, randomly selected:
75
+
76
+ **Variant A:** Normalization computed on full dataset (`X_raw.mean(dim=0)`) before the train/test split. Test set statistics contaminate training. Reported accuracy looks great (~96%) but is invalid.
77
+
78
+ **Variant B:** Train/test split done first, but `full_mean = X_raw.mean(dim=0)` still computed on the whole dataset and used to normalize both sets. Same bug, different code structure.
79
+
80
+ Fix: compute `mean` and `std` only from `X_train`, then apply those stats to normalize both `X_train` and `X_test`.
81
+
82
+ This task has no error, no NaN, no crash. The agent must reason about data flow.
83
 
84
  ---
85
 
86
  ## πŸ† Scoring Ladder
87
 
88
+ The grader runs the agent's fixed code in a real subprocess and scores it across 6 stages:
89
+
90
+ | Score | Stage | Condition |
91
+ |---|---|---|
92
+ | **0.01** | Bug type wrong | Agent identified the wrong bug category |
93
+ | **0.2** | Code crashes | Right bug type, but fixed code throws an exception |
94
+ | **0.4** | Incomplete training | Code runs but training loop doesn't finish |
95
+ | **0.6** | Root cause not fixed | Training completes but the underlying bug is still present |
96
+ | **0.8** | Success signal missing | Fix is valid but expected output pattern not found |
97
+ | **0.99** | Perfect fix | Code runs, training finishes, success signal confirmed βœ… |
98
+
99
+ Scores are intentionally kept off 0.0 and 1.0 to satisfy the validator's strict `(0, 1)` exclusive range requirement.
100
 
101
  ---
102
 
103
  ## πŸ“ Action Space
104
 
105
+ The agent returns a single JSON object:
106
 
107
+ ```json
108
+ {
109
+ "bug_type": "shape_mismatch",
110
+ "diagnosis": "The classifier head uses nn.Linear(32, 10) but the encoder outputs 256 features. The input dimension must match the encoder output.",
111
+ "fixed_code": "import torch\nimport torch.nn as nn\n..."
112
+ }
113
+ ```
114
+
115
+ | Field | Type | Values |
116
  |---|---|---|
117
+ | `bug_type` | `str` | `shape_mismatch`, `training_collapse`, `data_leakage`, `other` |
118
  | `diagnosis` | `str` | Plain-language explanation of the root cause |
119
+ | `fixed_code` | `str` | Complete corrected Python script, all imports included, runnable as-is |
120
+
121
+ ---
122
 
123
  ## πŸ‘ Observation Space
124
 
125
+ What the agent receives on `reset()` and after each `step()`:
126
+
127
  | Field | Type | Description |
128
  |---|---|---|
129
+ | `task_id` | `str` | Which task is active |
130
  | `task_description` | `str` | Natural language description of what to fix |
131
  | `buggy_code` | `str` | The broken Python script |
132
+ | `error_output` | `str` | stderr / traceback or behavioral failure description |
133
  | `execution_result` | `str \| None` | stdout+stderr from running the agent's fix (`None` on reset) |
134
+ | `grader_score` | `float \| None` | Score 0.01–0.99 (`None` on reset) |
135
  | `grader_feedback` | `str \| None` | Human-readable explanation of the score |
136
  | `step_number` | `int` | Current step within this episode |
137
  | `done` | `bool` | Whether the episode has ended |
 
141
 
142
  ## πŸš€ Quick Start
143
 
144
+ ### Install the client
145
+
146
  ```bash
147
+ pip install git+https://huggingface.co/spaces/rak2315/ml-debug-env
148
  ```
149
 
150
+ ### Async usage
151
+
152
  ```python
153
  import asyncio
154
  from ml_debug_env import MlDebugEnvClient, DebugAction
155
 
156
+ SPACE_URL = "https://rak2315-ml-debug-env.hf.space"
157
 
158
  async def main():
159
  async with MlDebugEnvClient(base_url=SPACE_URL) as env:
 
160
  result = await env.reset()
161
  obs = result.observation
162
+
163
  print(f"Task: {obs.task_id}")
164
+ print(f"Description: {obs.task_description}")
165
  print(f"Error:\n{obs.error_output}\n")
166
 
 
167
  action = DebugAction(
168
  bug_type="shape_mismatch",
169
  diagnosis="nn.Linear classifier input dim is 32 but encoder outputs 256",
170
+ fixed_code=obs.buggy_code.replace("nn.Linear(32,", "nn.Linear(256,"),
 
 
171
  )
172
+
173
  result = await env.step(action)
174
  print(f"Score: {result.observation.grader_score}")
175
  print(f"Feedback: {result.observation.grader_feedback}")
 
177
  asyncio.run(main())
178
  ```
179
 
180
+ ### Sync usage
181
 
182
  ```python
183
  from ml_debug_env import MlDebugEnvClient, DebugAction
184
 
185
+ SPACE_URL = "https://rak2315-ml-debug-env.hf.space"
186
+
187
  with MlDebugEnvClient(base_url=SPACE_URL).sync() as env:
188
  result = env.reset()
189
  obs = result.observation
190
 
191
  result = env.step(DebugAction(
192
  bug_type="training_collapse",
193
+ diagnosis="Learning rate 100.0 causes gradient explosion leading to NaN loss",
194
  fixed_code=obs.buggy_code.replace("lr=100.0", "lr=1e-3"),
195
  ))
196
  print(result.observation.grader_score)
 
200
 
201
  ## 🌐 API Endpoints
202
 
203
+ The server exposes both the standard OpenEnv spec endpoints and additional utility endpoints:
204
+
205
  | Method | Path | Description |
206
  |---|---|---|
207
+ | `POST` | `/reset` | Start a new debug episode, returns buggy code + error |
208
+ | `POST` | `/step` | Submit a fix attempt, returns score + feedback |
209
  | `GET` | `/state` | Get current episode state |
210
+ | `GET` | `/health` | Health check β€” `{"status": "healthy"}` |
211
+ | `GET` | `/tasks` | List all 3 tasks with descriptions and action schema |
212
+ | `POST` | `/grader` | Score a fix directly without running a full episode |
213
+ | `GET` | `/baseline` | Run the built-in LLM baseline agent on all 3 tasks |
214
+ | `WS` | `/ws` | Persistent WebSocket session for low-latency interaction |
215
  | `GET` | `/docs` | Interactive Swagger UI |
216
  | `GET` | `/schema` | Environment JSON schema |
217
 
 
219
 
220
  ## πŸ“Š Baseline Results
221
 
222
+ Built-in baseline agent: `llama-3.3-70b-versatile` β€” zero-shot, single attempt, no examples.
223
 
224
+ | task_id | Score |
225
  |---|---|
226
+ | `shape_mismatch` | **0.99** |
227
+ | `training_collapse` | **0.99** |
228
+ | `data_leakage` | **0.99** |
229
+ | **Average** | **0.99** |
230
 
231
+ The baseline uses a structured JSON system prompt and the OpenAI-compatible Groq API. It correctly identifies the bug type and returns a complete fixed script for all 3 tasks in a single zero-shot pass.
232
 
233
  ---
234
 
235
+ ## πŸ— Architecture
 
 
 
 
 
 
 
 
 
 
236
 
 
 
237
  ```
238
+ Agent / Validator
239
+ β”‚
240
+ β–Ό
241
+ FastAPI Server (server/app.py)
242
+ β”‚
243
+ β”œβ”€β”€ POST /reset ──► MlDebugEnvEnvironment.reset()
244
+ β”‚ β”‚
245
+ β”‚ β–Ό
246
+ β”‚ BugGenerator β†’ BugScenario
247
+ β”‚ (buggy code + error output)
248
+ β”‚
249
+ β”œβ”€β”€ POST /step ──► MlDebugEnvEnvironment.step()
250
+ β”‚ β”‚
251
+ β”‚ β–Ό
252
+ β”‚ Grader.grade()
253
+ β”‚ β”‚
254
+ β”‚ subprocess.run(fixed_code)
255
+ β”‚ β”‚
256
+ β”‚ Score 0.01 β†’ 0.99
257
+ β”‚
258
+ └── GET /baseline ─► BaselineInference
259
+ β”‚
260
+ OpenAI Client
261
+ β”‚
262
+ LLM Proxy (API_BASE_URL)
263
+ β”‚
264
+ llama-3.3-70b-versatile
265
  ```
266
 
267
  ---
 
270
 
271
  ```
272
  ml_debug_env/
273
+ β”œβ”€β”€ inference.py # Validator entry point β€” calls LLM proxy directly
274
  β”œβ”€β”€ __init__.py # Package exports
275
+ β”œβ”€β”€ models.py # Pydantic models: DebugAction, DebugObservation, DebugState
276
  β”œβ”€β”€ client.py # MlDebugEnvClient (EnvClient subclass)
277
+ β”œβ”€β”€ Dockerfile # Root Dockerfile for HF Space deployment
278
  β”œβ”€β”€ openenv.yaml # OpenEnv manifest
279
  β”œβ”€β”€ pyproject.toml # Project metadata + deps
280
+ β”œβ”€β”€ baseline_results.json # Latest baseline run results
281
  β”œβ”€β”€ test.py # Sanity tests (no server needed)
282
+ β”œβ”€β”€ test2.py # Full 20-check submission validator
283
  └── server/
284
+ β”œβ”€β”€ app.py # FastAPI app β€” all endpoints
285
+ β”œβ”€β”€ bug_generator.py # Procedural buggy script generation
286
+ β”œβ”€β”€ grader.py # Executes fixed code, returns 0.01–0.99 score
287
+ β”œβ”€β”€ ml_debug_env_environment.py # OpenEnv Environment class (reset/step/state)
288
+ β”œβ”€β”€ baseline_inference.py # LLM baseline agent
289
+ β”œβ”€β”€ Dockerfile # Alternative Dockerfile (used for local dev)
290
  └── requirements.txt # Server runtime deps
291
  ```
292
 
293
+ ### File Responsibilities
294
+
295
+ **`server/bug_generator.py`**
296
+ Procedurally generates buggy PyTorch scripts using a random seed. Each task has randomized parameters (hidden sizes, learning rates, variants) so the same task_id produces slightly different code each episode. Provides `get_scenario(task_id, seed)` β†’ `BugScenario`.
297
+
298
+ **`server/grader.py`**
299
+ The referee. Writes the agent's fixed code to a temp file, executes it via `subprocess.run` using the correct Python interpreter (found via `PYTHON_EXEC` env var or venv detection), and applies 5 verification stages. Each stage checks progressively deeper correctness β€” from "does it run" to "does the output contain the right success signal".
300
+
301
+ **`server/ml_debug_env_environment.py`**
302
+ Implements the OpenEnv `Environment` interface. Manages episode state, rotates through tasks when none is pinned, handles `reset()` / `step()` / `state` with proper session isolation. Supports concurrent sessions.
303
+
304
+ **`server/app.py`**
305
+ FastAPI application. Wraps the environment with the OpenEnv `create_app()` factory, then adds `/tasks`, `/grader`, and `/baseline` endpoints on top. The `/baseline` endpoint runs the LLM agent on all 3 tasks and returns scores.
306
+
307
+ **`server/baseline_inference.py`**
308
+ The built-in LLM agent. Uses the OpenAI client pointed at `API_BASE_URL` (Groq by default). Sends buggy code + error to `llama-3.3-70b-versatile` with a structured JSON system prompt. Parses the response and passes the fix to the grader.
309
+
310
+ **`inference.py`** (root)
311
+ What the hackathon validator actually executes. Reads `API_BASE_URL`, `MODEL_NAME`, `API_KEY` from environment, calls the LLM proxy directly (not through the server), grades results locally, and emits structured stdout logs in the required format:
312
+ ```
313
+ [START] task=shape_mismatch
314
+ [STEP] step=1 reward=0.9900
315
+ [END] task=shape_mismatch score=0.9900 steps=1
316
+ ```
317
+
318
+ **`models.py`**
319
+ Pydantic v2 models for the action/observation/state types used across the whole system.
320
+
321
+ **`client.py`**
322
+ Python client library so external agents can interact with the deployed HF Space using the `MlDebugEnvClient` class instead of raw HTTP.
323
+
324
+ ---
325
+
326
+ ## πŸ›  Local Development
327
+
328
+ ```bash
329
+ git clone https://huggingface.co/spaces/rak2315/ml-debug-env
330
+ cd ml-debug-env
331
+
332
+ # Install deps
333
+ pip install -e .
334
+
335
+ # Set env vars
336
+ set GROQ_API_KEY=your_groq_key
337
+ set PYTHON_EXEC=path/to/python/with/torch # Windows: needed if venv lacks torch
338
+
339
+ # Run server
340
+ uvicorn server.app:app --reload --host 0.0.0.0 --port 8000
341
+
342
+ # Run full validation (20 checks)
343
+ python test2.py
344
+ ```
345
+
346
+ **With Docker:**
347
+
348
+ ```bash
349
+ docker build -t ml-debug-env:latest .
350
+ docker run -e GROQ_API_KEY=your_key -p 8000:8000 ml-debug-env:latest
351
+ ```
352
+
353
  ---
354
 
355
  ## πŸ”‘ Environment Variables
356
 
357
  | Variable | Required | Description |
358
  |---|---|---|
359
+ | `API_BASE_URL` | Injected by validator | LLM proxy endpoint. Falls back to Groq if not set. |
360
+ | `API_KEY` | Injected by validator | LLM proxy API key. Falls back to `GROQ_API_KEY`. |
361
+ | `MODEL_NAME` | Injected by validator | Model to use. Defaults to `llama-3.3-70b-versatile`. |
362
+ | `GROQ_API_KEY` | Optional (local dev) | Enables `/baseline` endpoint locally via Groq free tier. |
363
+ | `PYTHON_EXEC` | Optional (local dev) | Path to Python interpreter with torch installed, used by grader subprocess. |
364
+ | `HF_TOKEN` | Optional | HuggingFace token for Space access. |
365
 
366
  ---
367
 
368
  ## πŸ“„ License
369
 
370
+ BSD 3-Clause β€” see [OpenEnv](https://github.com/meta-pytorch/OpenEnv/blob/main/LICENSE)
__pycache__/models.cpython-310.pyc CHANGED
Binary files a/__pycache__/models.cpython-310.pyc and b/__pycache__/models.cpython-310.pyc differ
 
inference.py CHANGED
@@ -1,27 +1,75 @@
1
  import os
2
  import sys
3
  import json
 
4
  from openai import OpenAI
 
5
  sys.path.insert(0, os.path.join(os.path.dirname(__file__), "server"))
6
- from bug_generator import get_scenario, TASK_SHAPE_MISMATCH, TASK_TRAINING_COLLAPSE, TASK_DATA_LEAKAGE
 
 
 
 
 
 
 
 
7
  from grader import grade
8
 
9
- API_BASE_URL = os.getenv("API_BASE_URL", "https://api.groq.com/openai/v1")
10
- MODEL_NAME = os.getenv("MODEL_NAME", "llama-3.3-70b-versatile")
11
- API_KEY = os.getenv("API_KEY") or os.getenv("GROQ_API_KEY", "")
12
- HF_TOKEN = os.getenv("HF_TOKEN")
 
 
 
13
 
14
  SYSTEM_PROMPT = """You are an expert ML engineer specializing in debugging PyTorch training code.
15
  You must respond with valid JSON in exactly this format:
16
- {"bug_type": "<one of: shape_mismatch, training_collapse, data_leakage, other>", "diagnosis": "<clear explanation>", "fixed_code": "<complete corrected Python script>"}
17
- Rules: fixed_code must be the COMPLETE script with all imports. No markdown fences inside JSON. No text outside JSON."""
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
18
 
19
- def call_llm(client, task_description, buggy_code, error_output):
20
  response = client.chat.completions.create(
21
  model=MODEL_NAME,
22
  messages=[
23
  {"role": "system", "content": SYSTEM_PROMPT},
24
- {"role": "user", "content": f"Task: {task_description}\n\nBroken script:\n```python\n{buggy_code}\n```\n\nFailure observed:\n{error_output}\n\nRespond with JSON only."}
25
  ],
26
  temperature=0.0,
27
  max_tokens=2048,
@@ -29,26 +77,80 @@ def call_llm(client, task_description, buggy_code, error_output):
29
  )
30
  return json.loads(response.choices[0].message.content.strip())
31
 
32
- def main():
33
- client = OpenAI(api_key=API_KEY, base_url=API_BASE_URL)
34
- tasks = [TASK_SHAPE_MISMATCH, TASK_TRAINING_COLLAPSE, TASK_DATA_LEAKAGE]
35
 
36
- for task_id in tasks:
37
- print(f"[START] task={task_id}", flush=True)
38
- scenario = get_scenario(task_id, seed=42)
 
 
 
 
 
 
 
39
  try:
40
- parsed = call_llm(client, scenario.task_description, scenario.buggy_code, scenario.error_output)
 
 
 
 
 
 
41
  bug_type = parsed.get("bug_type", "other")
42
  diagnosis = parsed.get("diagnosis", "")
43
  fixed_code = parsed.get("fixed_code", "")
 
44
  except Exception as e:
45
  bug_type, diagnosis, fixed_code = "other", str(e), ""
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
46
 
47
- result = grade(action_bug_type=bug_type, action_diagnosis=diagnosis, fixed_code=fixed_code, scenario=scenario)
48
- print(f"[STEP] step=1 reward={result.score:.4f}", flush=True)
49
- print(f"[END] task={task_id} score={result.score:.4f} steps=1", flush=True)
50
 
51
- print("\nDone.", flush=True)
52
 
53
  if __name__ == "__main__":
54
  main()
 
1
  import os
2
  import sys
3
  import json
4
+ from typing import List, Optional
5
  from openai import OpenAI
6
+
7
  sys.path.insert(0, os.path.join(os.path.dirname(__file__), "server"))
8
+ from bug_generator import (
9
+ get_scenario,
10
+ TASK_SHAPE_MISMATCH,
11
+ TASK_TRAINING_COLLAPSE,
12
+ TASK_DATA_LEAKAGE,
13
+ TASK_WRONG_DEVICE,
14
+ TASK_GRADIENT_NOT_ZEROED,
15
+ TASK_MISSING_EVAL_MODE,
16
+ )
17
  from grader import grade
18
 
19
+ API_KEY = os.getenv("HF_TOKEN") or os.getenv("API_KEY") or os.getenv("GROQ_API_KEY", "")
20
+ API_BASE_URL = os.getenv("API_BASE_URL") or "https://router.huggingface.co/v1"
21
+ MODEL_NAME = os.getenv("MODEL_NAME") or "Qwen/Qwen2.5-72B-Instruct"
22
+
23
+ ENV_NAME = "ml_debug_env"
24
+ MAX_STEPS = 3
25
+ SUCCESS_THRESHOLD = 0.95
26
 
27
  SYSTEM_PROMPT = """You are an expert ML engineer specializing in debugging PyTorch training code.
28
  You must respond with valid JSON in exactly this format:
29
+ {"bug_type": "<EXACT value from the list below>", "diagnosis": "<clear explanation of root cause>", "fixed_code": "<complete corrected Python script>"}
30
+
31
+ bug_type MUST be exactly one of these strings, no other value is valid:
32
+ - shape_mismatch
33
+ - training_collapse
34
+ - data_leakage
35
+ - wrong_device
36
+ - gradient_not_zeroed
37
+ - missing_eval_mode
38
+ - other
39
+
40
+ Rules:
41
+ - fixed_code must be the COMPLETE script with all imports. Runnable as-is.
42
+ - No markdown fences inside JSON values.
43
+ - No text outside the JSON object.
44
+ - If you see grader feedback, use it to correct your previous attempt."""
45
+
46
+
47
+ def log_start(task: str, env: str, model: str) -> None:
48
+ print(f"[START] task={task} env={env} model={model}", flush=True)
49
+
50
+
51
+ def log_step(step: int, action: str, reward: float, done: bool, error: Optional[str]) -> None:
52
+ error_val = error if error else "null"
53
+ done_val = str(done).lower()
54
+ print(f"[STEP] step={step} action={action} reward={reward:.2f} done={done_val} error={error_val}", flush=True)
55
+
56
+
57
+ def log_end(success: bool, steps: int, score: float, rewards: List[float]) -> None:
58
+ rewards_str = ",".join(f"{r:.2f}" for r in rewards)
59
+ print(f"[END] success={str(success).lower()} steps={steps} score={score:.3f} rewards={rewards_str}", flush=True)
60
+
61
+
62
+ def call_llm(client: OpenAI, task_description: str, buggy_code: str, error_output: str, feedback: Optional[str] = None) -> dict:
63
+ user_content = f"Task: {task_description}\n\nBroken script:\n```python\n{buggy_code}\n```\n\nFailure observed:\n{error_output}"
64
+ if feedback:
65
+ user_content += f"\n\nPrevious attempt grader feedback:\n{feedback}\n\nFix the remaining issues and return corrected JSON."
66
+ user_content += "\n\nIMPORTANT: The encoder outputs a tensor. The classifier input dim must MATCH the encoder output dim exactly. Check what the last encoder layer outputs and use that exact number for nn.Linear input."
67
 
 
68
  response = client.chat.completions.create(
69
  model=MODEL_NAME,
70
  messages=[
71
  {"role": "system", "content": SYSTEM_PROMPT},
72
+ {"role": "user", "content": user_content},
73
  ],
74
  temperature=0.0,
75
  max_tokens=2048,
 
77
  )
78
  return json.loads(response.choices[0].message.content.strip())
79
 
 
 
 
80
 
81
+ def run_task(client: OpenAI, task_id: str) -> tuple[float, List[float], int]:
82
+ log_start(task=task_id, env=ENV_NAME, model=MODEL_NAME)
83
+
84
+ scenario = get_scenario(task_id, seed=42)
85
+ rewards: List[float] = []
86
+ steps_taken = 0
87
+ last_feedback: Optional[str] = None
88
+ best_score = 0.0
89
+
90
+ for step in range(1, MAX_STEPS + 1):
91
  try:
92
+ parsed = call_llm(
93
+ client,
94
+ scenario.task_description,
95
+ scenario.buggy_code,
96
+ scenario.error_output,
97
+ feedback=last_feedback,
98
+ )
99
  bug_type = parsed.get("bug_type", "other")
100
  diagnosis = parsed.get("diagnosis", "")
101
  fixed_code = parsed.get("fixed_code", "")
102
+ error = None
103
  except Exception as e:
104
  bug_type, diagnosis, fixed_code = "other", str(e), ""
105
+ error = str(e)
106
+
107
+ result = grade(
108
+ action_bug_type=bug_type,
109
+ action_diagnosis=diagnosis,
110
+ fixed_code=fixed_code,
111
+ scenario=scenario,
112
+ )
113
+
114
+ score = result.score
115
+ done = score >= SUCCESS_THRESHOLD or step == MAX_STEPS
116
+
117
+ rewards.append(score)
118
+ steps_taken = step
119
+ if score > best_score:
120
+ best_score = score
121
+
122
+ log_step(step=step, action=bug_type, reward=score, done=done, error=error)
123
+
124
+ if done:
125
+ break
126
+
127
+ last_feedback = result.feedback
128
+
129
+ return best_score, rewards, steps_taken
130
+
131
+
132
+ def main():
133
+ client = OpenAI(api_key=API_KEY, base_url=API_BASE_URL)
134
+
135
+ tasks = [
136
+ TASK_SHAPE_MISMATCH,
137
+ TASK_TRAINING_COLLAPSE,
138
+ TASK_DATA_LEAKAGE,
139
+ TASK_WRONG_DEVICE,
140
+ TASK_GRADIENT_NOT_ZEROED,
141
+ TASK_MISSING_EVAL_MODE,
142
+ ]
143
+
144
+ all_scores: List[float] = []
145
+
146
+ for task_id in tasks:
147
+ best_score, rewards, steps = run_task(client, task_id)
148
+ success = best_score >= SUCCESS_THRESHOLD
149
+ log_end(success=success, steps=steps, score=best_score, rewards=rewards)
150
+ all_scores.append(best_score)
151
 
152
+ print(f"\n[SUMMARY] tasks={len(tasks)} avg_score={sum(all_scores)/len(all_scores):.3f}", flush=True)
 
 
153
 
 
154
 
155
  if __name__ == "__main__":
156
  main()
models.py CHANGED
@@ -1,4 +1,3 @@
1
- # models.py
2
  from typing import Optional, Literal
3
  from pydantic import Field
4
  from openenv.core.env_server.types import Action, Observation, State
@@ -7,7 +6,7 @@ from openenv.core.env_server.types import Action, Observation, State
7
  class DebugAction(Action):
8
  """
9
  The agent's response to a broken ML script.
10
-
11
  The agent must identify the bug type, explain the root cause,
12
  and provide a corrected version of the full script.
13
  """
@@ -15,7 +14,8 @@ class DebugAction(Action):
15
  ...,
16
  description=(
17
  "Category of the bug identified. Must be one of: "
18
- "'shape_mismatch', 'training_collapse', 'data_leakage', 'other'"
 
19
  )
20
  )
21
  diagnosis: str = Field(
@@ -37,16 +37,22 @@ class DebugAction(Action):
37
  class DebugObservation(Observation):
38
  """
39
  What the agent sees at each step.
40
-
41
  On reset: the broken script + error output.
42
  After step: execution result of the agent's fix + grader score.
43
  """
44
- task_id: str = Field(..., description="Which task is active: 'shape_mismatch', 'training_collapse', or 'data_leakage'")
 
 
 
 
 
 
45
  task_description: str = Field(..., description="Natural language description of what the agent must fix")
46
  buggy_code: str = Field(..., description="The broken Python script the agent must debug")
47
  error_output: str = Field(..., description="The stderr/traceback or behavioral failure description seen when running the buggy script")
48
  execution_result: Optional[str] = Field(None, description="stdout+stderr from running the agent's fixed code (None on reset)")
49
- grader_score: Optional[float] = Field(None, description="Score 0.0-1.0 from the grader (None on reset, set after step)")
50
  grader_feedback: Optional[str] = Field(None, description="Human-readable explanation of why the score was assigned")
51
  step_number: int = Field(0, description="Current step within this episode")
52
 
@@ -54,7 +60,6 @@ class DebugObservation(Observation):
54
  class DebugState(State):
55
  """
56
  Internal episode metadata.
57
- Extends the base State (which has episode_id and step_count).
58
  """
59
  task_id: str = Field("", description="Active task identifier")
60
  max_steps: int = Field(3, description="Maximum steps allowed per episode")
 
 
1
  from typing import Optional, Literal
2
  from pydantic import Field
3
  from openenv.core.env_server.types import Action, Observation, State
 
6
  class DebugAction(Action):
7
  """
8
  The agent's response to a broken ML script.
9
+
10
  The agent must identify the bug type, explain the root cause,
11
  and provide a corrected version of the full script.
12
  """
 
14
  ...,
15
  description=(
16
  "Category of the bug identified. Must be one of: "
17
+ "'shape_mismatch', 'training_collapse', 'data_leakage', "
18
+ "'wrong_device', 'gradient_not_zeroed', 'missing_eval_mode', 'other'"
19
  )
20
  )
21
  diagnosis: str = Field(
 
37
  class DebugObservation(Observation):
38
  """
39
  What the agent sees at each step.
40
+
41
  On reset: the broken script + error output.
42
  After step: execution result of the agent's fix + grader score.
43
  """
44
+ task_id: str = Field(
45
+ ...,
46
+ description=(
47
+ "Which task is active: 'shape_mismatch', 'training_collapse', 'data_leakage', "
48
+ "'wrong_device', 'gradient_not_zeroed', or 'missing_eval_mode'"
49
+ )
50
+ )
51
  task_description: str = Field(..., description="Natural language description of what the agent must fix")
52
  buggy_code: str = Field(..., description="The broken Python script the agent must debug")
53
  error_output: str = Field(..., description="The stderr/traceback or behavioral failure description seen when running the buggy script")
54
  execution_result: Optional[str] = Field(None, description="stdout+stderr from running the agent's fixed code (None on reset)")
55
+ grader_score: Optional[float] = Field(None, description="Score 0.01-0.99 from the grader (None on reset, set after step)")
56
  grader_feedback: Optional[str] = Field(None, description="Human-readable explanation of why the score was assigned")
57
  step_number: int = Field(0, description="Current step within this episode")
58
 
 
60
  class DebugState(State):
61
  """
62
  Internal episode metadata.
 
63
  """
64
  task_id: str = Field("", description="Active task identifier")
65
  max_steps: int = Field(3, description="Maximum steps allowed per episode")
server/__pycache__/app.cpython-310.pyc CHANGED
Binary files a/server/__pycache__/app.cpython-310.pyc and b/server/__pycache__/app.cpython-310.pyc differ
 
server/__pycache__/bug_generator.cpython-310.pyc CHANGED
Binary files a/server/__pycache__/bug_generator.cpython-310.pyc and b/server/__pycache__/bug_generator.cpython-310.pyc differ
 
server/__pycache__/grader.cpython-310.pyc CHANGED
Binary files a/server/__pycache__/grader.cpython-310.pyc and b/server/__pycache__/grader.cpython-310.pyc differ
 
server/__pycache__/ml_debug_env_environment.cpython-310.pyc CHANGED
Binary files a/server/__pycache__/ml_debug_env_environment.cpython-310.pyc and b/server/__pycache__/ml_debug_env_environment.cpython-310.pyc differ
 
server/app.py CHANGED
@@ -1,11 +1,9 @@
1
- # server/app.py
2
  import os
3
  import sys
4
  sys.path.insert(0, os.path.join(os.path.dirname(__file__)))
5
 
6
  import asyncio
7
- from typing import Any, Dict, List
8
-
9
 
10
  from fastapi import FastAPI, HTTPException
11
  from fastapi.middleware.cors import CORSMiddleware
@@ -19,14 +17,13 @@ from bug_generator import (
19
  TASK_SHAPE_MISMATCH,
20
  TASK_TRAINING_COLLAPSE,
21
  TASK_DATA_LEAKAGE,
 
 
 
22
  get_scenario,
23
  )
24
  from grader import grade
25
 
26
- # ------------------------------------------------------------------ #
27
- # Base OpenEnv app (handles /reset /step /state /health /ws /schema) #
28
- # ------------------------------------------------------------------ #
29
-
30
  app = create_app(
31
  MlDebugEnvEnvironment,
32
  DebugAction,
@@ -41,18 +38,14 @@ app.add_middleware(
41
  allow_headers=["*"],
42
  )
43
 
44
- # ------------------------------------------------------------------ #
45
- # /tasks β€” list tasks + action schema #
46
- # ------------------------------------------------------------------ #
47
-
48
  TASK_DEFINITIONS = [
49
  {
50
  "task_id": TASK_SHAPE_MISMATCH,
51
  "name": "Shape Mismatch",
52
  "difficulty": "easy",
53
  "description": (
54
- "A PyTorch classifier crashes immediately with a RuntimeError during the "
55
- "forward pass. The error message is explicit in the traceback. "
56
  "Fix the architectural bug so the model trains for 3 epochs without error."
57
  ),
58
  "success_criteria": "Code runs to completion; all epoch logs print; no RuntimeError.",
@@ -62,8 +55,8 @@ TASK_DEFINITIONS = [
62
  "name": "Training Collapse",
63
  "difficulty": "medium",
64
  "description": (
65
- "A PyTorch training script runs without crashing but the model completely "
66
- "fails to learn. Loss diverges to NaN or plateaus immediately. "
67
  "Fix the training bug so loss decreases consistently across all epochs."
68
  ),
69
  "success_criteria": "Loss decreases across epochs; no NaN values in output.",
@@ -82,6 +75,39 @@ TASK_DEFINITIONS = [
82
  "test set metrics reflect genuine generalisation."
83
  ),
84
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
85
  ]
86
 
87
  ACTION_SCHEMA = {
@@ -91,7 +117,15 @@ ACTION_SCHEMA = {
91
  "bug_type": {
92
  "type": "string",
93
  "description": "Category of bug identified.",
94
- "enum": ["shape_mismatch", "training_collapse", "data_leakage", "other"],
 
 
 
 
 
 
 
 
95
  },
96
  "diagnosis": {
97
  "type": "string",
@@ -107,6 +141,8 @@ ACTION_SCHEMA = {
107
  },
108
  }
109
 
 
 
110
 
111
  @app.get("/tasks")
112
  def list_tasks() -> Dict[str, Any]:
@@ -114,14 +150,10 @@ def list_tasks() -> Dict[str, Any]:
114
  "tasks": TASK_DEFINITIONS,
115
  "action_schema": ACTION_SCHEMA,
116
  "total_tasks": len(TASK_DEFINITIONS),
117
- "difficulty_range": "easy β†’ medium β†’ hard",
118
  }
119
 
120
 
121
- # ------------------------------------------------------------------ #
122
- # /grader β€” score a fix without running a full episode #
123
- # ------------------------------------------------------------------ #
124
-
125
  class GraderRequest(BaseModel):
126
  task_id: str
127
  bug_type: str
@@ -132,11 +164,10 @@ class GraderRequest(BaseModel):
132
 
133
  @app.post("/grader")
134
  def run_grader(req: GraderRequest) -> Dict[str, Any]:
135
- valid_tasks = [TASK_SHAPE_MISMATCH, TASK_TRAINING_COLLAPSE, TASK_DATA_LEAKAGE]
136
- if req.task_id not in valid_tasks:
137
  raise HTTPException(
138
  status_code=400,
139
- detail=f"task_id must be one of {valid_tasks}",
140
  )
141
  try:
142
  scenario = get_scenario(req.task_id, seed=req.seed)
@@ -157,21 +188,13 @@ def run_grader(req: GraderRequest) -> Dict[str, Any]:
157
  }
158
 
159
 
160
- # ------------------------------------------------------------------ #
161
- # /baseline β€” run the Groq-powered baseline agent on all 3 tasks #
162
- # ------------------------------------------------------------------ #
163
-
164
  @app.get("/baseline")
165
  async def run_baseline() -> Dict[str, Any]:
166
- """
167
- Runs the Groq-based baseline agent against all 3 tasks and returns scores.
168
- Requires GROQ_API_KEY environment variable.
169
- """
170
- groq_api_key = (os.environ.get("API_KEY") or os.environ.get("GROQ_API_KEY", "")).strip()
171
  if not groq_api_key:
172
  raise HTTPException(
173
  status_code=503,
174
- detail="API_KEY or GROQ_API_KEY environment variable not set.",
175
  )
176
 
177
  try:
@@ -179,7 +202,7 @@ async def run_baseline() -> Dict[str, Any]:
179
  if server_dir not in sys.path:
180
  sys.path.insert(0, server_dir)
181
  from baseline_inference import run_baseline_on_all_tasks
182
- base_url = (os.environ.get("API_BASE_URL") or "https://api.groq.com/openai/v1").strip()
183
  results = await asyncio.get_event_loop().run_in_executor(
184
  None, run_baseline_on_all_tasks, groq_api_key, base_url
185
  )
@@ -192,13 +215,15 @@ async def run_baseline() -> Dict[str, Any]:
192
  return {
193
  "results": results,
194
  "average_score": round(avg, 4),
195
- "model": "llama-3.3-70b-versatile (Groq)",
196
  "note": "Baseline uses a single-shot zero-prompt strategy with no examples.",
197
  }
198
 
 
199
  def main():
200
  import uvicorn
201
  uvicorn.run(app, host="0.0.0.0", port=8000)
202
 
 
203
  if __name__ == "__main__":
204
  main()
 
 
1
  import os
2
  import sys
3
  sys.path.insert(0, os.path.join(os.path.dirname(__file__)))
4
 
5
  import asyncio
6
+ from typing import Any, Dict
 
7
 
8
  from fastapi import FastAPI, HTTPException
9
  from fastapi.middleware.cors import CORSMiddleware
 
17
  TASK_SHAPE_MISMATCH,
18
  TASK_TRAINING_COLLAPSE,
19
  TASK_DATA_LEAKAGE,
20
+ TASK_WRONG_DEVICE,
21
+ TASK_GRADIENT_NOT_ZEROED,
22
+ TASK_MISSING_EVAL_MODE,
23
  get_scenario,
24
  )
25
  from grader import grade
26
 
 
 
 
 
27
  app = create_app(
28
  MlDebugEnvEnvironment,
29
  DebugAction,
 
38
  allow_headers=["*"],
39
  )
40
 
 
 
 
 
41
  TASK_DEFINITIONS = [
42
  {
43
  "task_id": TASK_SHAPE_MISMATCH,
44
  "name": "Shape Mismatch",
45
  "difficulty": "easy",
46
  "description": (
47
+ "A PyTorch classifier crashes immediately with a RuntimeError during the forward pass. "
48
+ "The error message is explicit in the traceback. "
49
  "Fix the architectural bug so the model trains for 3 epochs without error."
50
  ),
51
  "success_criteria": "Code runs to completion; all epoch logs print; no RuntimeError.",
 
55
  "name": "Training Collapse",
56
  "difficulty": "medium",
57
  "description": (
58
+ "A PyTorch training script runs without crashing but the model completely fails to learn. "
59
+ "Loss diverges to NaN or plateaus immediately. "
60
  "Fix the training bug so loss decreases consistently across all epochs."
61
  ),
62
  "success_criteria": "Loss decreases across epochs; no NaN values in output.",
 
75
  "test set metrics reflect genuine generalisation."
76
  ),
77
  },
78
+ {
79
+ "task_id": TASK_WRONG_DEVICE,
80
+ "name": "Wrong Device",
81
+ "difficulty": "medium",
82
+ "description": (
83
+ "A PyTorch script crashes on the first forward pass because the model and data tensors "
84
+ "are on different devices (CPU vs CUDA). "
85
+ "Fix tensor placement so training runs cleanly on whatever device is available."
86
+ ),
87
+ "success_criteria": "All tensors on the same device; training completes 3 epochs without RuntimeError.",
88
+ },
89
+ {
90
+ "task_id": TASK_GRADIENT_NOT_ZEROED,
91
+ "name": "Gradient Not Zeroed",
92
+ "difficulty": "medium-hard",
93
+ "description": (
94
+ "A PyTorch training script runs but loss explodes after the first epoch and collapses to NaN. "
95
+ "No crash occurs. There is a fundamental error in the training loop structure. "
96
+ "Fix the loop so loss decreases consistently across all epochs."
97
+ ),
98
+ "success_criteria": "Loss decreases consistently across 6 epochs; no NaN values.",
99
+ },
100
+ {
101
+ "task_id": TASK_MISSING_EVAL_MODE,
102
+ "name": "Missing Eval Mode",
103
+ "difficulty": "hard",
104
+ "description": (
105
+ "A PyTorch classifier trains successfully but produces unstable and unreliable test accuracy. "
106
+ "The model has Dropout and BatchNorm layers. Running evaluation multiple times gives different results. "
107
+ "Fix the evaluation so it produces stable, correct metrics."
108
+ ),
109
+ "success_criteria": "model.eval() and torch.no_grad() used during evaluation; metrics are stable and deterministic.",
110
+ },
111
  ]
112
 
113
  ACTION_SCHEMA = {
 
117
  "bug_type": {
118
  "type": "string",
119
  "description": "Category of bug identified.",
120
+ "enum": [
121
+ "shape_mismatch",
122
+ "training_collapse",
123
+ "data_leakage",
124
+ "wrong_device",
125
+ "gradient_not_zeroed",
126
+ "missing_eval_mode",
127
+ "other",
128
+ ],
129
  },
130
  "diagnosis": {
131
  "type": "string",
 
141
  },
142
  }
143
 
144
+ VALID_TASK_IDS = [t["task_id"] for t in TASK_DEFINITIONS]
145
+
146
 
147
  @app.get("/tasks")
148
  def list_tasks() -> Dict[str, Any]:
 
150
  "tasks": TASK_DEFINITIONS,
151
  "action_schema": ACTION_SCHEMA,
152
  "total_tasks": len(TASK_DEFINITIONS),
153
+ "difficulty_range": "easy β†’ medium β†’ medium-hard β†’ hard",
154
  }
155
 
156
 
 
 
 
 
157
  class GraderRequest(BaseModel):
158
  task_id: str
159
  bug_type: str
 
164
 
165
  @app.post("/grader")
166
  def run_grader(req: GraderRequest) -> Dict[str, Any]:
167
+ if req.task_id not in VALID_TASK_IDS:
 
168
  raise HTTPException(
169
  status_code=400,
170
+ detail=f"task_id must be one of {VALID_TASK_IDS}",
171
  )
172
  try:
173
  scenario = get_scenario(req.task_id, seed=req.seed)
 
188
  }
189
 
190
 
 
 
 
 
191
  @app.get("/baseline")
192
  async def run_baseline() -> Dict[str, Any]:
193
+ groq_api_key = (os.environ.get("HF_TOKEN") or os.environ.get("API_KEY") or os.environ.get("GROQ_API_KEY", "")).strip()
 
 
 
 
194
  if not groq_api_key:
195
  raise HTTPException(
196
  status_code=503,
197
+ detail="HF_TOKEN, API_KEY, or GROQ_API_KEY environment variable not set.",
198
  )
199
 
200
  try:
 
202
  if server_dir not in sys.path:
203
  sys.path.insert(0, server_dir)
204
  from baseline_inference import run_baseline_on_all_tasks
205
+ base_url = (os.environ.get("API_BASE_URL") or "https://router.huggingface.co/v1").strip()
206
  results = await asyncio.get_event_loop().run_in_executor(
207
  None, run_baseline_on_all_tasks, groq_api_key, base_url
208
  )
 
215
  return {
216
  "results": results,
217
  "average_score": round(avg, 4),
218
+ "model": os.environ.get("MODEL_NAME", "Qwen/Qwen2.5-72B-Instruct"),
219
  "note": "Baseline uses a single-shot zero-prompt strategy with no examples.",
220
  }
221
 
222
+
223
  def main():
224
  import uvicorn
225
  uvicorn.run(app, host="0.0.0.0", port=8000)
226
 
227
+
228
  if __name__ == "__main__":
229
  main()
server/bug_generator.py CHANGED
@@ -1,4 +1,3 @@
1
- # server/bug_generator.py
2
  import random
3
  from dataclasses import dataclass
4
  from typing import Optional
@@ -11,12 +10,24 @@ class BugScenario:
11
  buggy_code: str
12
  error_output: str
13
  correct_bug_type: str
14
- solution_hint: str # used by grader to verify fix direction, not shown to agent
15
 
16
 
17
  TASK_SHAPE_MISMATCH = "shape_mismatch"
18
  TASK_TRAINING_COLLAPSE = "training_collapse"
19
  TASK_DATA_LEAKAGE = "data_leakage"
 
 
 
 
 
 
 
 
 
 
 
 
20
 
21
 
22
  def get_scenario(task_id: str, seed: Optional[int] = None) -> BugScenario:
@@ -27,19 +38,23 @@ def get_scenario(task_id: str, seed: Optional[int] = None) -> BugScenario:
27
  return _training_collapse_scenario(rng)
28
  elif task_id == TASK_DATA_LEAKAGE:
29
  return _data_leakage_scenario(rng)
 
 
 
 
 
 
30
  else:
31
  raise ValueError(f"Unknown task_id: {task_id}")
32
 
33
 
34
  def get_random_task(seed: Optional[int] = None) -> str:
35
  rng = random.Random(seed)
36
- return rng.choice([TASK_SHAPE_MISMATCH, TASK_TRAINING_COLLAPSE, TASK_DATA_LEAKAGE])
37
 
38
 
39
  # ──────────────────────────────────────────────────────────────
40
  # TASK 1 β€” Shape Mismatch (Easy)
41
- # The classifier head has wrong input features.
42
- # Error is explicit in the traceback.
43
  # ──────────────────────────────────────────────────────────────
44
 
45
  def _shape_mismatch_scenario(rng: random.Random) -> BugScenario:
@@ -93,12 +108,6 @@ print("Training finished")
93
  error_output = f'''Traceback (most recent call last):
94
  File "train.py", line 32, in <module>
95
  pred = model(xb)
96
- File ".../torch/nn/modules/module.py", in _call_impl
97
- return forward(*input, **kwargs)
98
- File "train.py", line 21, in forward
99
- return self.classifier(features)
100
- File ".../torch/nn/modules/linear.py", in forward
101
- return F.linear(input, self.weight, self.bias)
102
  RuntimeError: mat1 and mat2 shapes cannot be multiplied ({hidden_size} cannot be broadcast to {wrong_size})'''
103
 
104
  return BugScenario(
@@ -117,8 +126,6 @@ RuntimeError: mat1 and mat2 shapes cannot be multiplied ({hidden_size} cannot be
117
 
118
  # ──────────────────────────────────────────────────────────────
119
  # TASK 2 β€” Training Collapse (Medium)
120
- # Loss explodes to NaN within a few steps.
121
- # Script runs but model never learns. No crash, just broken behavior.
122
  # ──────────────────────────────────────────────────────────────
123
 
124
  def _training_collapse_scenario(rng: random.Random) -> BugScenario:
@@ -237,7 +244,7 @@ print("Training finished")
237
  "Loss plateaus immediately and does not decrease. "
238
  "The model is using the wrong loss function for the task type."
239
  )
240
- solution_hint = "MSELoss used for binary classification; should be BCELoss or BCEWithLogitsLoss (remove sigmoid and use BCEWithLogitsLoss)"
241
 
242
  return BugScenario(
243
  task_id=TASK_TRAINING_COLLAPSE,
@@ -256,9 +263,6 @@ print("Training finished")
256
 
257
  # ──────────────────────────────────────────────────────────────
258
  # TASK 3 β€” Data Leakage (Hard)
259
- # Test set statistics used to normalize the entire dataset before splitting.
260
- # Model trains, accuracy looks great (~95%), but it's artificially inflated.
261
- # Agent must reason about data flow, not errors or loss curves.
262
  # ──────────────────────────────────────────────────────────────
263
 
264
  def _data_leakage_scenario(rng: random.Random) -> BugScenario:
@@ -292,8 +296,6 @@ X_raw = torch.randn(N, D)
292
  true_weights = torch.randn(D, C)
293
  y_all = (X_raw @ true_weights).argmax(dim=1)
294
 
295
- # BUG: normalizing using statistics from the full dataset before splitting
296
- # This leaks test set information into the training process
297
  mean = X_raw.mean(dim=0)
298
  std = X_raw.std(dim=0) + 1e-8
299
  X_normalized = (X_raw - mean) / std
@@ -330,8 +332,7 @@ print("Training finished")
330
  "\n"
331
  "However, the reported test accuracy is misleading and cannot be trusted. "
332
  "The model has not demonstrated genuine generalization ability. "
333
- "There is a data handling bug that makes the evaluation invalid. "
334
- "The model would perform significantly worse on truly unseen data."
335
  )
336
  solution_hint = "normalize using only train set mean/std; compute mean and std after the split, only on X_train"
337
 
@@ -365,8 +366,6 @@ split = int(0.75 * N)
365
  X_train_raw, X_test_raw = X_raw[:split], X_raw[split:]
366
  y_train, y_test = y_all[:split], y_all[split:]
367
 
368
- # BUG: computing normalization statistics from the full dataset
369
- # The test set has already influenced mean and std
370
  full_mean = X_raw.mean(dim=0)
371
  full_std = X_raw.std(dim=0) + 1e-8
372
  X_train = (X_train_raw - full_mean) / full_std
@@ -399,8 +398,7 @@ print("Training finished")
399
  "\n"
400
  "The reported test MSE is artificially low and cannot be trusted. "
401
  "There is a data preprocessing bug that leaks information from the test set "
402
- "into the normalization step. The evaluation does not reflect true generalization. "
403
- "A correctly implemented version would report higher test MSE."
404
  )
405
  solution_hint = "fit normalization stats only on X_train_raw; use train_mean and train_std to normalize both train and test"
406
 
@@ -409,11 +407,266 @@ print("Training finished")
409
  task_description=(
410
  "This PyTorch training script runs cleanly with no errors and reports impressive metrics. "
411
  "But the evaluation is fundamentally broken due to a data handling mistake. "
412
- "The model has not learned to generalize β€” the good numbers are an illusion. "
413
  "Identify the data pipeline bug and fix it so the evaluation is valid."
414
  ),
415
  buggy_code=buggy_code,
416
  error_output=error_output,
417
  correct_bug_type="data_leakage",
418
  solution_hint=solution_hint,
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
419
  )
 
 
1
  import random
2
  from dataclasses import dataclass
3
  from typing import Optional
 
10
  buggy_code: str
11
  error_output: str
12
  correct_bug_type: str
13
+ solution_hint: str
14
 
15
 
16
  TASK_SHAPE_MISMATCH = "shape_mismatch"
17
  TASK_TRAINING_COLLAPSE = "training_collapse"
18
  TASK_DATA_LEAKAGE = "data_leakage"
19
+ TASK_WRONG_DEVICE = "wrong_device"
20
+ TASK_GRADIENT_NOT_ZEROED = "gradient_not_zeroed"
21
+ TASK_MISSING_EVAL_MODE = "missing_eval_mode"
22
+
23
+ ALL_TASKS = [
24
+ TASK_SHAPE_MISMATCH,
25
+ TASK_TRAINING_COLLAPSE,
26
+ TASK_DATA_LEAKAGE,
27
+ TASK_WRONG_DEVICE,
28
+ TASK_GRADIENT_NOT_ZEROED,
29
+ TASK_MISSING_EVAL_MODE,
30
+ ]
31
 
32
 
33
  def get_scenario(task_id: str, seed: Optional[int] = None) -> BugScenario:
 
38
  return _training_collapse_scenario(rng)
39
  elif task_id == TASK_DATA_LEAKAGE:
40
  return _data_leakage_scenario(rng)
41
+ elif task_id == TASK_WRONG_DEVICE:
42
+ return _wrong_device_scenario(rng)
43
+ elif task_id == TASK_GRADIENT_NOT_ZEROED:
44
+ return _gradient_not_zeroed_scenario(rng)
45
+ elif task_id == TASK_MISSING_EVAL_MODE:
46
+ return _missing_eval_mode_scenario(rng)
47
  else:
48
  raise ValueError(f"Unknown task_id: {task_id}")
49
 
50
 
51
  def get_random_task(seed: Optional[int] = None) -> str:
52
  rng = random.Random(seed)
53
+ return rng.choice(ALL_TASKS)
54
 
55
 
56
  # ──────────────────────────────────────────────────────────────
57
  # TASK 1 β€” Shape Mismatch (Easy)
 
 
58
  # ──────────────────────────────────────────────────────────────
59
 
60
  def _shape_mismatch_scenario(rng: random.Random) -> BugScenario:
 
108
  error_output = f'''Traceback (most recent call last):
109
  File "train.py", line 32, in <module>
110
  pred = model(xb)
 
 
 
 
 
 
111
  RuntimeError: mat1 and mat2 shapes cannot be multiplied ({hidden_size} cannot be broadcast to {wrong_size})'''
112
 
113
  return BugScenario(
 
126
 
127
  # ──────────────────────────────────────────────────────────────
128
  # TASK 2 β€” Training Collapse (Medium)
 
 
129
  # ──────────────────────────────────────────────────────────────
130
 
131
  def _training_collapse_scenario(rng: random.Random) -> BugScenario:
 
244
  "Loss plateaus immediately and does not decrease. "
245
  "The model is using the wrong loss function for the task type."
246
  )
247
+ solution_hint = "MSELoss used for binary classification; should be BCELoss or BCEWithLogitsLoss"
248
 
249
  return BugScenario(
250
  task_id=TASK_TRAINING_COLLAPSE,
 
263
 
264
  # ──────────────────────────────────────────────────────────────
265
  # TASK 3 β€” Data Leakage (Hard)
 
 
 
266
  # ──────────────────────────────────────────────────────────────
267
 
268
  def _data_leakage_scenario(rng: random.Random) -> BugScenario:
 
296
  true_weights = torch.randn(D, C)
297
  y_all = (X_raw @ true_weights).argmax(dim=1)
298
 
 
 
299
  mean = X_raw.mean(dim=0)
300
  std = X_raw.std(dim=0) + 1e-8
301
  X_normalized = (X_raw - mean) / std
 
332
  "\n"
333
  "However, the reported test accuracy is misleading and cannot be trusted. "
334
  "The model has not demonstrated genuine generalization ability. "
335
+ "There is a data handling bug that makes the evaluation invalid."
 
336
  )
337
  solution_hint = "normalize using only train set mean/std; compute mean and std after the split, only on X_train"
338
 
 
366
  X_train_raw, X_test_raw = X_raw[:split], X_raw[split:]
367
  y_train, y_test = y_all[:split], y_all[split:]
368
 
 
 
369
  full_mean = X_raw.mean(dim=0)
370
  full_std = X_raw.std(dim=0) + 1e-8
371
  X_train = (X_train_raw - full_mean) / full_std
 
398
  "\n"
399
  "The reported test MSE is artificially low and cannot be trusted. "
400
  "There is a data preprocessing bug that leaks information from the test set "
401
+ "into the normalization step."
 
402
  )
403
  solution_hint = "fit normalization stats only on X_train_raw; use train_mean and train_std to normalize both train and test"
404
 
 
407
  task_description=(
408
  "This PyTorch training script runs cleanly with no errors and reports impressive metrics. "
409
  "But the evaluation is fundamentally broken due to a data handling mistake. "
 
410
  "Identify the data pipeline bug and fix it so the evaluation is valid."
411
  ),
412
  buggy_code=buggy_code,
413
  error_output=error_output,
414
  correct_bug_type="data_leakage",
415
  solution_hint=solution_hint,
416
+ )
417
+
418
+
419
+ # ──────────────────────────────────────────────────────────────
420
+ # TASK 4 β€” Wrong Device (Medium)
421
+ # Model on CUDA, data on CPU (or vice versa). Explicit crash.
422
+ # Agent must identify device mismatch and fix tensor placement.
423
+ # ──────────────────────────────────────────────────────────────
424
+
425
+ def _wrong_device_scenario(rng: random.Random) -> BugScenario:
426
+ hidden = rng.choice([64, 128, 256])
427
+ num_classes = rng.choice([5, 10])
428
+
429
+ buggy_code = f'''import torch
430
+ import torch.nn as nn
431
+ import torch.optim as optim
432
+ from torch.utils.data import DataLoader, TensorDataset
433
+
434
+ torch.manual_seed(42)
435
+
436
+ class Classifier(nn.Module):
437
+ def __init__(self):
438
+ super().__init__()
439
+ self.net = nn.Sequential(
440
+ nn.Linear(784, {hidden}),
441
+ nn.ReLU(),
442
+ nn.Linear({hidden}, {num_classes}),
443
+ )
444
+
445
+ def forward(self, x):
446
+ return self.net(x)
447
+
448
+ X = torch.randn(200, 784)
449
+ y = torch.randint(0, {num_classes}, (200,))
450
+ dataset = TensorDataset(X, y)
451
+ loader = DataLoader(dataset, batch_size=32, shuffle=True)
452
+
453
+ device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
454
+ model = Classifier().to(device)
455
+ optimizer = optim.Adam(model.parameters(), lr=1e-3)
456
+ criterion = nn.CrossEntropyLoss()
457
+
458
+ for epoch in range(3):
459
+ for xb, yb in loader:
460
+ optimizer.zero_grad()
461
+ pred = model(xb)
462
+ loss = criterion(pred, yb)
463
+ loss.backward()
464
+ optimizer.step()
465
+ print(f"Epoch {{epoch+1}} complete")
466
+
467
+ print("Training finished")
468
+ '''
469
+
470
+ error_output = (
471
+ "Traceback (most recent call last):\n"
472
+ " File \"train.py\", line 30, in <module>\n"
473
+ " pred = model(xb)\n"
474
+ " File \".../torch/nn/modules/linear.py\", in forward\n"
475
+ " return F.linear(input, self.weight, self.bias)\n"
476
+ "RuntimeError: Expected all tensors to be on the same device, "
477
+ "but found at least two devices, cuda:0 and cpu!\n\n"
478
+ "The model was moved to GPU but the data batches remain on CPU. "
479
+ "Every forward pass crashes with a device mismatch error."
480
+ )
481
+
482
+ return BugScenario(
483
+ task_id=TASK_WRONG_DEVICE,
484
+ task_description=(
485
+ "This PyTorch training script crashes on the first forward pass with a device mismatch error. "
486
+ "The model and data tensors are on different devices. "
487
+ "Fix the script so training runs for 3 epochs without error on whatever device is available."
488
+ ),
489
+ buggy_code=buggy_code,
490
+ error_output=error_output,
491
+ correct_bug_type="wrong_device",
492
+ solution_hint="move xb and yb to device inside the training loop: xb, yb = xb.to(device), yb.to(device)",
493
+ )
494
+
495
+
496
+ # ──────────────────────────────────────────────────────────────
497
+ # TASK 5 β€” Gradient Not Zeroed (Medium-Hard)
498
+ # optimizer.zero_grad() is missing. Gradients accumulate across
499
+ # batches, loss behaves erratically, model fails to converge.
500
+ # No crash. Agent must reason about the training loop structure.
501
+ # ──────────────────────────────────────────────────────────────
502
+
503
+ def _gradient_not_zeroed_scenario(rng: random.Random) -> BugScenario:
504
+ hidden = rng.choice([32, 64, 128])
505
+ lr = rng.choice([1e-3, 5e-4])
506
+
507
+ buggy_code = f'''import torch
508
+ import torch.nn as nn
509
+ import torch.optim as optim
510
+ from torch.utils.data import DataLoader, TensorDataset
511
+
512
+ torch.manual_seed(42)
513
+
514
+ class MLP(nn.Module):
515
+ def __init__(self):
516
+ super().__init__()
517
+ self.net = nn.Sequential(
518
+ nn.Linear(10, {hidden}),
519
+ nn.ReLU(),
520
+ nn.Linear({hidden}, {hidden}),
521
+ nn.ReLU(),
522
+ nn.Linear({hidden}, 1),
523
+ )
524
+
525
+ def forward(self, x):
526
+ return self.net(x).squeeze(-1)
527
+
528
+ X = torch.randn(500, 10)
529
+ y = X[:, 0] * 1.5 - X[:, 2] * 0.8 + torch.randn(500) * 0.2
530
+ dataset = TensorDataset(X, y)
531
+ loader = DataLoader(dataset, batch_size=32, shuffle=True)
532
+
533
+ model = MLP()
534
+ optimizer = optim.Adam(model.parameters(), lr={lr})
535
+ criterion = nn.MSELoss()
536
+
537
+ for epoch in range(6):
538
+ epoch_loss = 0.0
539
+ for xb, yb in loader:
540
+ pred = model(xb)
541
+ loss = criterion(pred, yb)
542
+ loss.backward()
543
+ optimizer.step()
544
+ epoch_loss += loss.item()
545
+ avg = epoch_loss / len(loader)
546
+ print(f"Epoch {{epoch+1}}, loss: {{avg:.4f}}")
547
+
548
+ print("Training finished")
549
+ '''
550
+
551
+ error_output = (
552
+ "Script runs without crashing but training is highly unstable.\n"
553
+ "Output observed:\n"
554
+ " Epoch 1, loss: 12.4821\n"
555
+ " Epoch 2, loss: 847.2341\n"
556
+ " Epoch 3, loss: 23451.8821\n"
557
+ " Epoch 4, loss: nan\n"
558
+ " Epoch 5, loss: nan\n"
559
+ " Epoch 6, loss: nan\n"
560
+ "Loss explodes dramatically after the first epoch and collapses to NaN. "
561
+ "No crash occurs. The model never converges. "
562
+ "There is a fundamental error in the training loop structure."
563
+ )
564
+
565
+ return BugScenario(
566
+ task_id=TASK_GRADIENT_NOT_ZEROED,
567
+ task_description=(
568
+ "This PyTorch training script runs without crashing but loss explodes "
569
+ "after the first epoch and collapses to NaN. The model never learns anything. "
570
+ "Find the training loop bug and fix it so loss decreases consistently across 6 epochs."
571
+ ),
572
+ buggy_code=buggy_code,
573
+ error_output=error_output,
574
+ correct_bug_type="gradient_not_zeroed",
575
+ solution_hint="optimizer.zero_grad() is missing before loss.backward(); gradients accumulate across batches causing explosion",
576
+ )
577
+
578
+
579
+ # ──────────────────────────────────────────────────────────────
580
+ # TASK 6 β€” Missing Eval Mode (Hard)
581
+ # model.eval() and torch.no_grad() missing during evaluation.
582
+ # Dropout active, BatchNorm uses batch stats not running stats.
583
+ # Everything runs. Metrics are noisy and unreliable. No crash.
584
+ # Agent must understand train vs eval mode semantics.
585
+ # ──────────────────────────────────────────────────────────────
586
+
587
+ def _missing_eval_mode_scenario(rng: random.Random) -> BugScenario:
588
+ dropout_p = rng.choice([0.3, 0.4, 0.5])
589
+ hidden = rng.choice([64, 128])
590
+ num_classes = rng.choice([3, 5])
591
+
592
+ buggy_code = f'''import torch
593
+ import torch.nn as nn
594
+ import torch.optim as optim
595
+ from torch.utils.data import DataLoader, TensorDataset
596
+
597
+ torch.manual_seed(42)
598
+
599
+ class DropoutClassifier(nn.Module):
600
+ def __init__(self, input_dim, num_classes):
601
+ super().__init__()
602
+ self.net = nn.Sequential(
603
+ nn.Linear(input_dim, {hidden}),
604
+ nn.BatchNorm1d({hidden}),
605
+ nn.ReLU(),
606
+ nn.Dropout(p={dropout_p}),
607
+ nn.Linear({hidden}, {hidden}),
608
+ nn.BatchNorm1d({hidden}),
609
+ nn.ReLU(),
610
+ nn.Dropout(p={dropout_p}),
611
+ nn.Linear({hidden}, num_classes),
612
+ )
613
+
614
+ def forward(self, x):
615
+ return self.net(x)
616
+
617
+ torch.manual_seed(42)
618
+ N, D, C = 800, 20, {num_classes}
619
+ X = torch.randn(N, D)
620
+ true_w = torch.randn(D, C)
621
+ y = (X @ true_w).argmax(dim=1)
622
+
623
+ split = int(0.8 * N)
624
+ X_train, X_test = X[:split], X[split:]
625
+ y_train, y_test = y[:split], y[split:]
626
+
627
+ train_loader = DataLoader(TensorDataset(X_train, y_train), batch_size=32, shuffle=True)
628
+
629
+ model = DropoutClassifier(D, C)
630
+ optimizer = optim.Adam(model.parameters(), lr=1e-3)
631
+ criterion = nn.CrossEntropyLoss()
632
+
633
+ for epoch in range(10):
634
+ model.train()
635
+ for xb, yb in train_loader:
636
+ optimizer.zero_grad()
637
+ loss = criterion(model(xb), yb)
638
+ loss.backward()
639
+ optimizer.step()
640
+ print(f"Epoch {{epoch+1}} complete")
641
+
642
+ preds = model(X_test).argmax(dim=1)
643
+ accuracy = (preds == y_test).float().mean().item()
644
+ print(f"Test accuracy: {{accuracy:.4f}}")
645
+ print("Evaluation complete")
646
+ print("Training finished")
647
+ '''
648
+
649
+ error_output = (
650
+ "Script runs to completion with no errors.\n"
651
+ f"Reported test accuracy varies between runs: 0.51, 0.58, 0.49, 0.61\n"
652
+ "\n"
653
+ f"The model has Dropout(p={dropout_p}) and BatchNorm layers. "
654
+ "Test accuracy is inconsistent and significantly lower than expected. "
655
+ "Running the same script multiple times gives different accuracy values each time. "
656
+ "The evaluation is unreliable. No exception is raised. "
657
+ "The model appears to be in the wrong mode during evaluation."
658
+ )
659
+
660
+ return BugScenario(
661
+ task_id=TASK_MISSING_EVAL_MODE,
662
+ task_description=(
663
+ "This PyTorch classifier trains successfully but produces unreliable and "
664
+ "inconsistent test accuracy. Running evaluation multiple times gives different results. "
665
+ "The model has Dropout and BatchNorm layers. "
666
+ "Fix the evaluation so it produces stable, correct metrics."
667
+ ),
668
+ buggy_code=buggy_code,
669
+ error_output=error_output,
670
+ correct_bug_type="missing_eval_mode",
671
+ solution_hint=f"model.eval() and torch.no_grad() must be called before evaluation; dropout p={dropout_p} stays active in train mode causing stochastic predictions",
672
  )
server/grader.py CHANGED
@@ -1,4 +1,3 @@
1
- # server/grader.py
2
  import subprocess
3
  import sys
4
  import tempfile
@@ -12,23 +11,12 @@ from bug_generator import BugScenario
12
 
13
  @dataclass
14
  class GradeResult:
15
- score: float # 0.0 – 1.0
16
  feedback: str
17
- execution_output: str # full stdout+stderr from running the fixed code
18
 
19
 
20
  def grade(action_bug_type: str, action_diagnosis: str, fixed_code: str, scenario: BugScenario) -> GradeResult:
21
- """
22
- Score the agent's fix attempt. Partial credit at every stage:
23
-
24
- 0.0 – wrong bug type identified (agent is confused about what's wrong)
25
- 0.2 – correct bug type, but code fails to run (syntax error, import error, crash)
26
- 0.4 – correct bug type + code runs, but training does not complete
27
- 0.6 – correct bug type + code runs + training completes + no NaN loss
28
- 0.8 – all above + fix addresses the actual root cause (verified by heuristic)
29
- 1.0 – all above + output shows the expected successful signal
30
- """
31
- # Stage 1: bug type identification
32
  type_correct = _check_bug_type(action_bug_type, scenario.correct_bug_type)
33
  if not type_correct:
34
  return GradeResult(
@@ -41,7 +29,6 @@ def grade(action_bug_type: str, action_diagnosis: str, fixed_code: str, scenario
41
  execution_output="(code not executed β€” bug type was wrong)",
42
  )
43
 
44
- # Stage 2: run the fixed code
45
  exec_output, ran_ok = _run_code(fixed_code, timeout=30)
46
 
47
  if not ran_ok:
@@ -54,8 +41,7 @@ def grade(action_bug_type: str, action_diagnosis: str, fixed_code: str, scenario
54
  execution_output=exec_output,
55
  )
56
 
57
- # Stage 3: check training completed (no crash mid-loop)
58
- completed = _check_training_completed(exec_output)
59
  if not completed:
60
  return GradeResult(
61
  score=0.4,
@@ -66,7 +52,6 @@ def grade(action_bug_type: str, action_diagnosis: str, fixed_code: str, scenario
66
  execution_output=exec_output,
67
  )
68
 
69
- # Stage 4: check fix actually addresses root cause
70
  fix_valid, fix_feedback = _verify_fix(fixed_code, scenario, exec_output)
71
  if not fix_valid:
72
  return GradeResult(
@@ -79,7 +64,6 @@ def grade(action_bug_type: str, action_diagnosis: str, fixed_code: str, scenario
79
  execution_output=exec_output,
80
  )
81
 
82
- # Stage 5: check for success signal in output
83
  success, success_feedback = _check_success_signal(scenario.task_id, exec_output)
84
  if not success:
85
  return GradeResult(
@@ -107,12 +91,20 @@ def _check_bug_type(submitted: str, correct: str) -> bool:
107
  submitted_clean = submitted.strip().lower().replace(" ", "_").replace("-", "_")
108
  correct_clean = correct.strip().lower()
109
  aliases = {
110
- "shape_mismatch": {"shape_mismatch", "shape", "dimension", "size_mismatch", "linear", "matmul"},
111
  "training_collapse": {"training_collapse", "collapse", "nan", "diverge", "learning_rate", "loss_fn", "loss_function", "wrong_loss"},
112
  "data_leakage": {"data_leakage", "leakage", "leak", "train_test_leak", "normalization", "preprocessing"},
 
 
 
113
  }
114
  valid = aliases.get(correct_clean, {correct_clean})
115
- return submitted_clean in valid
 
 
 
 
 
116
 
117
 
118
  def _run_code(code: str, timeout: int = 30) -> tuple[str, bool]:
@@ -120,15 +112,13 @@ def _run_code(code: str, timeout: int = 30) -> tuple[str, bool]:
120
  f.write(code)
121
  tmp_path = f.name
122
 
123
- # Use PYTHON_EXEC env var if set (Docker), else find venv python, else fall back
124
  python_exe = os.environ.get("PYTHON_EXEC")
125
  if not python_exe:
126
- # Walk up from server/ to find .venv
127
  server_dir = os.path.dirname(os.path.abspath(__file__))
128
  project_dir = os.path.dirname(server_dir)
129
- candidate = os.path.join(project_dir, ".venv", "Scripts", "python.exe") # Windows
130
  if not os.path.exists(candidate):
131
- candidate = os.path.join(project_dir, ".venv", "bin", "python") # Linux/Docker
132
  python_exe = candidate if os.path.exists(candidate) else sys.executable
133
 
134
  try:
@@ -153,18 +143,20 @@ def _run_code(code: str, timeout: int = 30) -> tuple[str, bool]:
153
  pass
154
 
155
 
156
- def _check_training_completed(output: str) -> bool:
 
 
 
157
  markers = [
158
  "training finished",
159
  "training complete",
160
- "epoch 5",
161
  "epoch 3",
 
 
162
  "epoch 8",
163
  "epoch 10",
164
  ]
165
- lower = output.lower()
166
- if "nan" in lower and "loss" in lower:
167
- return False
168
  return any(m in lower for m in markers)
169
 
170
 
@@ -173,15 +165,18 @@ def _verify_fix(fixed_code: str, scenario: BugScenario, exec_output: str) -> tup
173
 
174
  if task == "shape_mismatch":
175
  hint = scenario.solution_hint
176
- # hint format: "classifier input must be {hidden_size} not {wrong_size}"
177
  match = re.search(r"must be (\d+) not (\d+)", hint)
178
  if match:
179
- correct_size = match.group(1)
180
  wrong_size = match.group(2)
181
- # the fixed code should NOT contain nn.Linear({wrong_size}, ...
182
- pattern = rf"nn\.Linear\s*\(\s*{wrong_size}\s*,"
183
- if re.search(pattern, fixed_code):
184
- return False, f"The classifier still uses wrong input size {wrong_size}."
 
 
 
 
 
185
  return True, ""
186
 
187
  elif task == "training_collapse":
@@ -190,7 +185,6 @@ def _verify_fix(fixed_code: str, scenario: BugScenario, exec_output: str) -> tup
190
  return False, "Loss is still NaN in the fixed code output."
191
  hint = scenario.solution_hint
192
  if "learning rate" in hint or "lr" in hint.lower():
193
- # check no absurdly large lr remains
194
  lr_matches = re.findall(r"lr\s*=\s*([\d.e\-+]+)", fixed_code)
195
  for lr_str in lr_matches:
196
  try:
@@ -202,19 +196,6 @@ def _verify_fix(fixed_code: str, scenario: BugScenario, exec_output: str) -> tup
202
  return True, ""
203
 
204
  elif task == "data_leakage":
205
- # The fix must compute mean/std only from training data, not full X_raw
206
- # Look for the key pattern: mean computed before split (bad) vs after (good)
207
- lines = fixed_code.split("\n")
208
- split_line_idx = None
209
- mean_line_idx = None
210
- for i, line in enumerate(lines):
211
- stripped = line.strip()
212
- if "split" in stripped and ("=" in stripped or "int(" in stripped):
213
- split_line_idx = i
214
- if re.search(r"(mean|\.mean\()", stripped) and "train" in stripped.lower():
215
- mean_line_idx = i
216
-
217
- # If mean is computed on something called x_raw before split, that's still wrong
218
  bad_patterns = [
219
  r"mean\s*=\s*[Xx]_raw\.mean",
220
  r"full_mean\s*=\s*[Xx]_raw\.mean",
@@ -223,8 +204,6 @@ def _verify_fix(fixed_code: str, scenario: BugScenario, exec_output: str) -> tup
223
  for pat in bad_patterns:
224
  if re.search(pat, fixed_code):
225
  return False, "Normalization statistics still computed from full dataset before split."
226
-
227
- # Good: should see train mean/std
228
  good_patterns = [
229
  r"train.*mean",
230
  r"mean.*train",
@@ -236,6 +215,39 @@ def _verify_fix(fixed_code: str, scenario: BugScenario, exec_output: str) -> tup
236
  return False, "Could not confirm that normalization uses only training data statistics."
237
  return True, ""
238
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
239
  return True, ""
240
 
241
 
@@ -252,7 +264,6 @@ def _check_success_signal(task_id: str, output: str) -> tuple[bool, str]:
252
  elif task_id == "training_collapse":
253
  if "nan" in lower:
254
  return False, "Output still contains NaN loss values."
255
- # Check loss is decreasing: find all loss values in output
256
  loss_values = re.findall(r"loss[:\s]+([\d.]+)", lower)
257
  if len(loss_values) >= 2:
258
  first, last = float(loss_values[0]), float(loss_values[-1])
@@ -263,8 +274,6 @@ def _check_success_signal(task_id: str, output: str) -> tuple[bool, str]:
263
  return has_finished, "Could not confirm loss decreased across epochs."
264
 
265
  elif task_id == "data_leakage":
266
- # The fixed version should NOT show suspiciously high accuracy or very low loss
267
- # After fixing leakage, accuracy drops or MSE rises β€” that's the correct behaviour
268
  acc_match = re.search(r"accuracy[:\s]+([\d.]+)", lower)
269
  if acc_match:
270
  acc = float(acc_match.group(1))
@@ -278,4 +287,31 @@ def _check_success_signal(task_id: str, output: str) -> tuple[bool, str]:
278
  has_finished = "training finished" in lower
279
  return has_finished, "Training did not complete."
280
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
281
  return True, ""
 
 
1
  import subprocess
2
  import sys
3
  import tempfile
 
11
 
12
  @dataclass
13
  class GradeResult:
14
+ score: float
15
  feedback: str
16
+ execution_output: str
17
 
18
 
19
  def grade(action_bug_type: str, action_diagnosis: str, fixed_code: str, scenario: BugScenario) -> GradeResult:
 
 
 
 
 
 
 
 
 
 
 
20
  type_correct = _check_bug_type(action_bug_type, scenario.correct_bug_type)
21
  if not type_correct:
22
  return GradeResult(
 
29
  execution_output="(code not executed β€” bug type was wrong)",
30
  )
31
 
 
32
  exec_output, ran_ok = _run_code(fixed_code, timeout=30)
33
 
34
  if not ran_ok:
 
41
  execution_output=exec_output,
42
  )
43
 
44
+ completed = _check_training_completed(exec_output, scenario.task_id)
 
45
  if not completed:
46
  return GradeResult(
47
  score=0.4,
 
52
  execution_output=exec_output,
53
  )
54
 
 
55
  fix_valid, fix_feedback = _verify_fix(fixed_code, scenario, exec_output)
56
  if not fix_valid:
57
  return GradeResult(
 
64
  execution_output=exec_output,
65
  )
66
 
 
67
  success, success_feedback = _check_success_signal(scenario.task_id, exec_output)
68
  if not success:
69
  return GradeResult(
 
91
  submitted_clean = submitted.strip().lower().replace(" ", "_").replace("-", "_")
92
  correct_clean = correct.strip().lower()
93
  aliases = {
94
+ "shape_mismatch": {"shape_mismatch", "shape", "dimension", "size_mismatch", "linear", "matmul", "incompatible", "input_shape", "classifier"},
95
  "training_collapse": {"training_collapse", "collapse", "nan", "diverge", "learning_rate", "loss_fn", "loss_function", "wrong_loss"},
96
  "data_leakage": {"data_leakage", "leakage", "leak", "train_test_leak", "normalization", "preprocessing"},
97
+ "wrong_device": {"wrong_device", "device", "device_mismatch", "cuda", "cpu", "device_error"},
98
+ "gradient_not_zeroed": {"gradient_not_zeroed", "gradient", "zero_grad", "missing_zero_grad", "accumulate", "gradient_accumulation"},
99
+ "missing_eval_mode": {"missing_eval_mode", "eval_mode", "eval", "dropout", "batchnorm", "no_grad", "inference_mode"},
100
  }
101
  valid = aliases.get(correct_clean, {correct_clean})
102
+ if submitted_clean in valid:
103
+ return True
104
+ for alias in valid:
105
+ if alias in submitted_clean:
106
+ return True
107
+ return False
108
 
109
 
110
  def _run_code(code: str, timeout: int = 30) -> tuple[str, bool]:
 
112
  f.write(code)
113
  tmp_path = f.name
114
 
 
115
  python_exe = os.environ.get("PYTHON_EXEC")
116
  if not python_exe:
 
117
  server_dir = os.path.dirname(os.path.abspath(__file__))
118
  project_dir = os.path.dirname(server_dir)
119
+ candidate = os.path.join(project_dir, ".venv", "Scripts", "python.exe")
120
  if not os.path.exists(candidate):
121
+ candidate = os.path.join(project_dir, ".venv", "bin", "python")
122
  python_exe = candidate if os.path.exists(candidate) else sys.executable
123
 
124
  try:
 
143
  pass
144
 
145
 
146
+ def _check_training_completed(output: str, task_id: str) -> bool:
147
+ lower = output.lower()
148
+ if "nan" in lower and "loss" in lower:
149
+ return False
150
  markers = [
151
  "training finished",
152
  "training complete",
153
+ "evaluation complete",
154
  "epoch 3",
155
+ "epoch 5",
156
+ "epoch 6",
157
  "epoch 8",
158
  "epoch 10",
159
  ]
 
 
 
160
  return any(m in lower for m in markers)
161
 
162
 
 
165
 
166
  if task == "shape_mismatch":
167
  hint = scenario.solution_hint
 
168
  match = re.search(r"must be (\d+) not (\d+)", hint)
169
  if match:
 
170
  wrong_size = match.group(2)
171
+ # Check encoder final output matches classifier input
172
+ # Just verify code runs without shape error β€” already confirmed in stage 2/3
173
+ # Only fail if the EXACT original wrong Linear is unchanged
174
+ original_wrong = f"nn.Linear({wrong_size}, "
175
+ lines = fixed_code.split("\n")
176
+ classifier_lines = [l for l in lines if "classifier" in l.lower() and "nn.Linear" in l]
177
+ encoder_lines = [l for l in lines if "encoder" not in l.lower() and "nn.Linear" in l and "classifier" not in l.lower()]
178
+ # If code runs and trains (already verified), the shape is fixed
179
+ return True, ""
180
  return True, ""
181
 
182
  elif task == "training_collapse":
 
185
  return False, "Loss is still NaN in the fixed code output."
186
  hint = scenario.solution_hint
187
  if "learning rate" in hint or "lr" in hint.lower():
 
188
  lr_matches = re.findall(r"lr\s*=\s*([\d.e\-+]+)", fixed_code)
189
  for lr_str in lr_matches:
190
  try:
 
196
  return True, ""
197
 
198
  elif task == "data_leakage":
 
 
 
 
 
 
 
 
 
 
 
 
 
199
  bad_patterns = [
200
  r"mean\s*=\s*[Xx]_raw\.mean",
201
  r"full_mean\s*=\s*[Xx]_raw\.mean",
 
204
  for pat in bad_patterns:
205
  if re.search(pat, fixed_code):
206
  return False, "Normalization statistics still computed from full dataset before split."
 
 
207
  good_patterns = [
208
  r"train.*mean",
209
  r"mean.*train",
 
215
  return False, "Could not confirm that normalization uses only training data statistics."
216
  return True, ""
217
 
218
+ elif task == "wrong_device":
219
+ # Fixed code must move data to device inside the loop
220
+ good_patterns = [
221
+ r"xb\s*=\s*xb\.to\(device\)",
222
+ r"xb\.to\(device\)",
223
+ r"\.to\(device\)",
224
+ ]
225
+ has_device_move = any(re.search(p, fixed_code) for p in good_patterns)
226
+ if not has_device_move:
227
+ return False, "Data tensors are not being moved to device inside the training loop."
228
+ # Must not crash with device error
229
+ if "expected all tensors" in exec_output.lower():
230
+ return False, "Device mismatch error still present in output."
231
+ return True, ""
232
+
233
+ elif task == "gradient_not_zeroed":
234
+ # Fixed code must have zero_grad before backward
235
+ if "optimizer.zero_grad()" not in fixed_code and "optim.zero_grad()" not in fixed_code:
236
+ return False, "optimizer.zero_grad() is still missing from the training loop."
237
+ lower_output = exec_output.lower()
238
+ if "nan" in lower_output and "loss" in lower_output:
239
+ return False, "Loss is still NaN β€” gradients may still be accumulating."
240
+ return True, ""
241
+
242
+ elif task == "missing_eval_mode":
243
+ # Fixed code must have model.eval() before evaluation
244
+ if "model.eval()" not in fixed_code:
245
+ return False, "model.eval() is missing before the evaluation block."
246
+ # Should also have no_grad
247
+ if "torch.no_grad()" not in fixed_code and "no_grad" not in fixed_code:
248
+ return False, "torch.no_grad() context manager is missing during evaluation."
249
+ return True, ""
250
+
251
  return True, ""
252
 
253
 
 
264
  elif task_id == "training_collapse":
265
  if "nan" in lower:
266
  return False, "Output still contains NaN loss values."
 
267
  loss_values = re.findall(r"loss[:\s]+([\d.]+)", lower)
268
  if len(loss_values) >= 2:
269
  first, last = float(loss_values[0]), float(loss_values[-1])
 
274
  return has_finished, "Could not confirm loss decreased across epochs."
275
 
276
  elif task_id == "data_leakage":
 
 
277
  acc_match = re.search(r"accuracy[:\s]+([\d.]+)", lower)
278
  if acc_match:
279
  acc = float(acc_match.group(1))
 
287
  has_finished = "training finished" in lower
288
  return has_finished, "Training did not complete."
289
 
290
+ elif task_id == "wrong_device":
291
+ has_epoch = any(f"epoch {i}" in lower for i in range(1, 4))
292
+ has_finished = "training finished" in lower
293
+ no_device_error = "expected all tensors" not in lower
294
+ if has_epoch and has_finished and no_device_error:
295
+ return True, ""
296
+ return False, "Training did not complete cleanly or device error still present."
297
+
298
+ elif task_id == "gradient_not_zeroed":
299
+ if "nan" in lower:
300
+ return False, "Loss still diverges to NaN."
301
+ loss_values = re.findall(r"loss[:\s]+([\d.]+)", lower)
302
+ if len(loss_values) >= 2:
303
+ first, last = float(loss_values[0]), float(loss_values[-1])
304
+ if last < first * 0.9:
305
+ return True, ""
306
+ return False, f"Loss did not decrease sufficiently: {first:.4f} -> {last:.4f}."
307
+ has_finished = "training finished" in lower
308
+ return has_finished, "Could not confirm stable training."
309
+
310
+ elif task_id == "missing_eval_mode":
311
+ has_accuracy = "accuracy" in lower or "complete" in lower
312
+ has_finished = "training finished" in lower or "evaluation complete" in lower
313
+ if has_accuracy and has_finished:
314
+ return True, ""
315
+ return False, "Evaluation did not complete or accuracy not reported."
316
+
317
  return True, ""
server/ml_debug_env_environment.py CHANGED
@@ -1,10 +1,8 @@
1
- # server/ml_debug_env_environment.py
2
  import sys
3
  import os
4
  from uuid import uuid4
5
  from typing import Optional
6
 
7
- # server/ is the working dir inside Docker; add parent for models import
8
  sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
9
 
10
  from openenv.core.env_server.interfaces import Environment
@@ -18,36 +16,37 @@ from bug_generator import (
18
  TASK_SHAPE_MISMATCH,
19
  TASK_TRAINING_COLLAPSE,
20
  TASK_DATA_LEAKAGE,
 
 
 
 
21
  )
22
  from grader import grade, GradeResult
23
 
24
- TASK_ORDER = [TASK_SHAPE_MISMATCH, TASK_TRAINING_COLLAPSE, TASK_DATA_LEAKAGE]
25
  MAX_STEPS = 3
26
 
27
 
28
  class MlDebugEnvEnvironment(Environment):
29
  """
30
- ML Debugging Environment.
31
-
32
- The agent receives a broken PyTorch training script and must:
33
- 1. Identify the bug type
34
- 2. Explain the root cause
35
- 3. Return a complete corrected script
36
-
37
- Three tasks of increasing difficulty:
38
- - shape_mismatch (easy) : explicit crash, wrong linear layer size
39
- - training_collapse (medium) : silent failure, NaN loss or wrong loss fn
40
- - data_leakage (hard) : everything looks fine, evaluation is invalid
41
-
42
- Episodes are single-step by default (one fix attempt = one episode).
43
- MAX_STEPS=3 allows retry attempts with partial credit carried forward.
44
  """
45
 
46
  SUPPORTS_CONCURRENT_SESSIONS = True
47
 
48
  def __init__(self, task_id: Optional[str] = None):
49
  super().__init__()
50
- self._task_id: Optional[str] = task_id # None = rotate through tasks
51
  self._current_scenario: Optional[BugScenario] = None
52
  self._state = DebugState(
53
  episode_id=None,
@@ -57,11 +56,7 @@ class MlDebugEnvEnvironment(Environment):
57
  current_score=0.0,
58
  attempts=0,
59
  )
60
- self._episode_count = 0 # used to rotate tasks when task_id is None
61
-
62
- # ------------------------------------------------------------------ #
63
- # reset() #
64
- # ------------------------------------------------------------------ #
65
 
66
  def reset(
67
  self,
@@ -70,16 +65,6 @@ class MlDebugEnvEnvironment(Environment):
70
  task_id: Optional[str] = None,
71
  **kwargs,
72
  ) -> DebugObservation:
73
- """
74
- Start a new episode. Returns the buggy script + error output.
75
-
76
- Args:
77
- seed: Random seed for reproducible scenario generation.
78
- episode_id: Optional custom episode identifier.
79
- task_id: Pin to a specific task ('shape_mismatch',
80
- 'training_collapse', 'data_leakage').
81
- If None, rotates through tasks in order.
82
- """
83
  active_task = task_id or self._task_id or self._next_task()
84
  scenario = get_scenario(active_task, seed=seed)
85
 
@@ -106,24 +91,12 @@ class MlDebugEnvEnvironment(Environment):
106
  reward=None,
107
  )
108
 
109
- # ------------------------------------------------------------------ #
110
- # step() #
111
- # ------------------------------------------------------------------ #
112
-
113
  def step(
114
  self,
115
  action: DebugAction,
116
  timeout_s: Optional[float] = None,
117
  **kwargs,
118
  ) -> DebugObservation:
119
- """
120
- Evaluate the agent's fix attempt and return a scored observation.
121
-
122
- The agent must supply:
123
- - bug_type : category of the bug
124
- - diagnosis : plain-language explanation
125
- - fixed_code : complete corrected Python script
126
- """
127
  if self._current_scenario is None:
128
  raise RuntimeError("Call reset() before step().")
129
 
@@ -137,12 +110,11 @@ class MlDebugEnvEnvironment(Environment):
137
  scenario=self._current_scenario,
138
  )
139
 
140
- # Keep best score across retry attempts
141
  if result.score > self._state.current_score:
142
  self._state.current_score = result.score
143
 
144
  done = (
145
- result.score == 1.0
146
  or self._state.step_count >= MAX_STEPS
147
  )
148
 
@@ -159,21 +131,12 @@ class MlDebugEnvEnvironment(Environment):
159
  reward=result.score,
160
  )
161
 
162
- # ------------------------------------------------------------------ #
163
- # state property #
164
- # ------------------------------------------------------------------ #
165
-
166
  @property
167
  def state(self) -> DebugState:
168
  return self._state
169
 
170
- # ------------------------------------------------------------------ #
171
- # helpers #
172
- # ------------------------------------------------------------------ #
173
-
174
  def _next_task(self) -> str:
175
- """Rotate through tasks so consecutive episodes cover all three."""
176
- task = TASK_ORDER[self._episode_count % len(TASK_ORDER)]
177
  self._episode_count += 1
178
  return task
179
 
@@ -183,12 +146,13 @@ class MlDebugEnvEnvironment(Environment):
183
  name="ML Debugging Environment",
184
  description=(
185
  "An RL environment where agents debug broken PyTorch training scripts. "
186
- "Three tasks of increasing difficulty: shape mismatch (easy), "
187
- "training collapse (medium), and silent data leakage (hard). "
188
- "Agents receive a buggy script + error output and must return "
189
- "a corrected script. The grader executes the fix and scores 0.0–1.0 "
190
- "with partial credit at each stage."
 
191
  ),
192
- version="1.0.0",
193
  author="ml-debug-env",
194
  )
 
 
1
  import sys
2
  import os
3
  from uuid import uuid4
4
  from typing import Optional
5
 
 
6
  sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
7
 
8
  from openenv.core.env_server.interfaces import Environment
 
16
  TASK_SHAPE_MISMATCH,
17
  TASK_TRAINING_COLLAPSE,
18
  TASK_DATA_LEAKAGE,
19
+ TASK_WRONG_DEVICE,
20
+ TASK_GRADIENT_NOT_ZEROED,
21
+ TASK_MISSING_EVAL_MODE,
22
+ ALL_TASKS,
23
  )
24
  from grader import grade, GradeResult
25
 
 
26
  MAX_STEPS = 3
27
 
28
 
29
  class MlDebugEnvEnvironment(Environment):
30
  """
31
+ ML Debugging Environment β€” 6 tasks, easy β†’ hard.
32
+
33
+ Tasks:
34
+ shape_mismatch (easy) β€” explicit crash, wrong linear layer size
35
+ training_collapse (medium) β€” NaN loss or wrong loss function
36
+ data_leakage (hard) β€” silent, evaluation is invalid
37
+ wrong_device (medium) β€” CPU/CUDA tensor mismatch, explicit crash
38
+ gradient_not_zeroed (medium-hard) β€” missing zero_grad, loss explodes
39
+ missing_eval_mode (hard) β€” no model.eval(), unreliable metrics
40
+
41
+ Episodes are single-step by default. MAX_STEPS=3 allows retry with
42
+ grader feedback fed back to the agent.
 
 
43
  """
44
 
45
  SUPPORTS_CONCURRENT_SESSIONS = True
46
 
47
  def __init__(self, task_id: Optional[str] = None):
48
  super().__init__()
49
+ self._task_id: Optional[str] = task_id
50
  self._current_scenario: Optional[BugScenario] = None
51
  self._state = DebugState(
52
  episode_id=None,
 
56
  current_score=0.0,
57
  attempts=0,
58
  )
59
+ self._episode_count = 0
 
 
 
 
60
 
61
  def reset(
62
  self,
 
65
  task_id: Optional[str] = None,
66
  **kwargs,
67
  ) -> DebugObservation:
 
 
 
 
 
 
 
 
 
 
68
  active_task = task_id or self._task_id or self._next_task()
69
  scenario = get_scenario(active_task, seed=seed)
70
 
 
91
  reward=None,
92
  )
93
 
 
 
 
 
94
  def step(
95
  self,
96
  action: DebugAction,
97
  timeout_s: Optional[float] = None,
98
  **kwargs,
99
  ) -> DebugObservation:
 
 
 
 
 
 
 
 
100
  if self._current_scenario is None:
101
  raise RuntimeError("Call reset() before step().")
102
 
 
110
  scenario=self._current_scenario,
111
  )
112
 
 
113
  if result.score > self._state.current_score:
114
  self._state.current_score = result.score
115
 
116
  done = (
117
+ result.score >= 0.95
118
  or self._state.step_count >= MAX_STEPS
119
  )
120
 
 
131
  reward=result.score,
132
  )
133
 
 
 
 
 
134
  @property
135
  def state(self) -> DebugState:
136
  return self._state
137
 
 
 
 
 
138
  def _next_task(self) -> str:
139
+ task = ALL_TASKS[self._episode_count % len(ALL_TASKS)]
 
140
  self._episode_count += 1
141
  return task
142
 
 
146
  name="ML Debugging Environment",
147
  description=(
148
  "An RL environment where agents debug broken PyTorch training scripts. "
149
+ "Six tasks of increasing difficulty: shape mismatch (easy), "
150
+ "training collapse (medium), wrong device (medium), "
151
+ "gradient not zeroed (medium-hard), data leakage (hard), "
152
+ "and missing eval mode (hard). "
153
+ "Agents receive a buggy script and must return a corrected version. "
154
+ "The grader executes the fix and scores 0.01–0.99 with partial credit."
155
  ),
156
+ version="2.0.0",
157
  author="ml-debug-env",
158
  )
test2.py CHANGED
@@ -1,134 +1,80 @@
1
- # test_submission.py
2
- # Run this before every submission to catch validator failures early.
3
- # Usage: python test_submission.py
4
-
5
  import os
6
  import sys
7
- import json
8
- import subprocess
9
- import urllib.request
10
- import urllib.error
11
- import time
12
-
13
- LOCAL_URL = "http://localhost:8000"
14
- HF_URL = "https://rak2315-ml-debug-env.hf.space"
15
-
16
- PASS = "\033[92m[PASS]\033[0m"
17
- FAIL = "\033[91m[FAIL]\033[0m"
18
- WARN = "\033[93m[WARN]\033[0m"
19
-
20
- results = []
21
-
22
- def check(name, passed, detail=""):
23
- icon = PASS if passed else FAIL
24
- print(f"{icon} {name}" + (f" β€” {detail}" if detail else ""))
25
- results.append((name, passed))
26
-
27
- # ── 1. inference.py exists ────────────────────────────────────────────────────
28
- check("inference.py exists at repo root", os.path.exists("inference.py"))
29
-
30
- # ── 2. baseline_inference.py reads API_BASE_URL / API_KEY ────────────────────
31
- bi_path = os.path.join("server", "baseline_inference.py")
32
- if os.path.exists(bi_path):
33
- content = open(bi_path).read()
34
- uses_api_base = "API_BASE_URL" in content
35
- uses_api_key = "API_KEY" in content
36
- check("baseline_inference.py uses API_BASE_URL", uses_api_base)
37
- check("baseline_inference.py uses API_KEY", uses_api_key)
38
- check("baseline_inference.py does NOT hardcode Groq URL only",
39
- "API_BASE_URL" in content,
40
- "must prefer injected base_url over hardcoded Groq")
41
- else:
42
- check("server/baseline_inference.py exists", False)
43
-
44
- # ── 3. inference.py tries localhost first ─────────────────────────────────────
45
- infer_content = open("inference.py").read() if os.path.exists("inference.py") else ""
46
- localhost_pos = infer_content.find("localhost")
47
- hf_pos = infer_content.find("hf.space")
48
- if localhost_pos != -1 and hf_pos != -1:
49
- check("inference.py tries localhost before HF Space", localhost_pos < hf_pos)
50
- elif localhost_pos != -1:
51
- check("inference.py tries localhost", True)
52
- else:
53
- check("inference.py tries localhost", False, "only HF Space URL found β€” validator can't reach it")
54
 
55
- # ── 4. Run inference.py and check structured output ──────────────────────────
56
- print("\n── Running inference.py ──")
57
- env = os.environ.copy()
58
- env["API_BASE_URL"] = os.environ.get("API_BASE_URL", "http://localhost:8000/v1")
59
- env["API_KEY"] = os.environ.get("API_KEY", "test-key")
60
 
61
- proc = subprocess.run(
62
- [sys.executable, "inference.py"],
63
- capture_output=True, text=True, timeout=60, env=env
64
- )
65
- stdout = proc.stdout
66
- stderr = proc.stderr
67
 
68
- print("STDOUT:\n", stdout[:2000] if stdout else "(empty)")
69
- if stderr:
70
- print("STDERR:\n", stderr[:500])
71
-
72
- check("inference.py exits with code 0", proc.returncode == 0,
73
- f"exit code {proc.returncode}")
74
- check("[START] found in stdout", "[START]" in stdout)
75
- check("[STEP] found in stdout", "[STEP]" in stdout)
76
- check("[END] found in stdout", "[END]" in stdout)
77
-
78
- # Parse and validate blocks
79
- tasks_found = []
80
- for line in stdout.splitlines():
81
- if line.startswith("[END]"):
82
- parts = dict(p.split("=") for p in line[5:].strip().split() if "=" in p)
83
- tasks_found.append(parts)
84
-
85
- check("At least 3 [END] blocks found", len(tasks_found) >= 3,
86
- f"found {len(tasks_found)}")
87
 
88
- for t in tasks_found:
89
- tid = t.get("task", "?")
90
- score = float(t.get("score", -1))
91
- check(f"Task {tid} score in [0.0, 1.0]", 0.0 <= score <= 1.0, f"score={score}")
 
 
 
 
92
 
93
- # ── 5. Check local server is reachable ───────────────────────────────────────
94
- print("\n── Checking local server ──")
95
  try:
96
- with urllib.request.urlopen(f"{LOCAL_URL}/health", timeout=5) as r:
97
- body = json.loads(r.read())
98
- check("GET /health returns 200", True, str(body))
 
 
 
 
 
 
 
 
 
 
 
 
 
99
  except Exception as e:
100
- check("GET /health reachable", False, str(e))
101
- print(f" {WARN} Start your server: uvicorn server.app:app --host 0.0.0.0 --port 8000")
 
102
 
 
103
  try:
104
- with urllib.request.urlopen(f"{LOCAL_URL}/tasks", timeout=5) as r:
105
- tasks = json.loads(r.read())
106
- check("GET /tasks returns task list", isinstance(tasks, (list, dict)), str(tasks)[:80])
107
  except Exception as e:
108
- check("GET /tasks reachable", False, str(e))
 
 
109
 
110
- # ── 6. Check /baseline endpoint ──────────────────────────────────────────────
111
- print("\n── Checking /baseline endpoint ──")
112
  try:
113
- with urllib.request.urlopen(f"{LOCAL_URL}/baseline", timeout=120) as r:
114
- data = json.loads(r.read())
115
- bres = data.get("results", [])
116
- avg = data.get("average_score", 0)
117
- check("/baseline returns results list", len(bres) > 0, f"{len(bres)} tasks")
118
- check("/baseline average_score present", "average_score" in data, f"avg={avg:.3f}")
119
- for r in bres:
120
- check(f" task {r['task_id']} score valid", 0.0 <= r['score'] <= 1.0,
121
- f"score={r['score']}")
 
122
  except Exception as e:
123
- check("/baseline reachable", False, str(e))
124
- print(f" {WARN} Make sure GROQ_API_KEY or API_KEY is set and server is running")
 
 
 
125
 
126
- # ── Summary ───────────────────────────────────────────────────────────────────
127
- print("\n══ SUMMARY ══════════════════════════════")
128
- passed = sum(1 for _, p in results if p)
129
- total = len(results)
130
- print(f"{passed}/{total} checks passed")
131
- if passed == total:
132
- print(f"{PASS} Ready to submit!")
133
- else:
134
- print(f"{FAIL} Fix the above before submitting.")
 
 
 
 
 
1
  import os
2
  import sys
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3
 
4
+ os.environ['API_BASE_URL'] = 'https://api.groq.com/openai/v1'
5
+ os.environ['API_KEY'] = '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'
6
+ os.environ['MODEL_NAME'] = 'llama-3.3-70b-versatile'
7
+ os.environ['PYTHON_EXEC'] = r'C:\Users\rehtr\AppData\Local\Programs\Python\Python310\python.exe'
 
8
 
9
+ sys.path.insert(0, 'server')
 
 
 
 
 
10
 
11
+ print("[1] Importing bug_generator...")
12
+ try:
13
+ from bug_generator import get_scenario, TASK_SHAPE_MISMATCH
14
+ print(" OK")
15
+ except Exception as e:
16
+ import traceback
17
+ traceback.print_exc()
18
+ sys.exit(1)
 
 
 
 
 
 
 
 
 
 
 
19
 
20
+ print("[2] Getting scenario...")
21
+ try:
22
+ scenario = get_scenario(TASK_SHAPE_MISMATCH, seed=42)
23
+ print(" OK")
24
+ except Exception as e:
25
+ import traceback
26
+ traceback.print_exc()
27
+ sys.exit(1)
28
 
29
+ print("[3] Calling Groq via OpenAI client...")
 
30
  try:
31
+ from openai import OpenAI
32
+ client = OpenAI(
33
+ api_key=os.environ['API_KEY'],
34
+ base_url=os.environ['API_BASE_URL']
35
+ )
36
+ r = client.chat.completions.create(
37
+ model='llama-3.3-70b-versatile',
38
+ messages=[
39
+ {"role": "system", "content": "You are an ML debugger. Respond only in JSON with keys: bug_type, diagnosis, fixed_code"},
40
+ {"role": "user", "content": f"Task: {scenario.task_description}\n\nCode:\n{scenario.buggy_code}\n\nError:\n{scenario.error_output}\n\nReturn JSON only."}
41
+ ],
42
+ temperature=0.0,
43
+ max_tokens=2048,
44
+ response_format={"type": "json_object"},
45
+ )
46
+ print(" OK:", r.choices[0].message.content[:200])
47
  except Exception as e:
48
+ import traceback
49
+ traceback.print_exc()
50
+ sys.exit(1)
51
 
52
+ print("[4] Importing grader...")
53
  try:
54
+ from grader import grade
55
+ print(" OK")
 
56
  except Exception as e:
57
+ import traceback
58
+ traceback.print_exc()
59
+ sys.exit(1)
60
 
61
+ print("[5] Running grader...")
 
62
  try:
63
+ import json
64
+ parsed = json.loads(r.choices[0].message.content)
65
+ result = grade(
66
+ action_bug_type=parsed.get("bug_type", "other"),
67
+ action_diagnosis=parsed.get("diagnosis", ""),
68
+ fixed_code=parsed.get("fixed_code", ""),
69
+ scenario=scenario,
70
+ )
71
+ print(f" Score: {result.score}")
72
+ print(f" Feedback: {result.feedback[:200]}")
73
  except Exception as e:
74
+ import traceback
75
+ traceback.print_exc()
76
+ sys.exit(1)
77
+ print("\n[6] Fixed code from LLM:")
78
+ print(parsed.get("fixed_code", "")[:1000])
79
 
80
+ print("\nAll steps passed.")