Dishaaa25 commited on
Commit
c0a3e8d
·
1 Parent(s): e8813d0

Smoke-train deployment update

Browse files
CODEBASE_STRUCTURE_MAIN_V5.md ADDED
@@ -0,0 +1,1564 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # ADAPT Codebase Structure (LLM Working Map)
2
+
3
+ ## Scope And Assumption
4
+
5
+ This document was generated from the repository rooted at `/Users/dishagoyal/Documents/meta-rl-dsa-solver`.
6
+
7
+ Important assumption:
8
+ - There is no literal `main-v5/` directory in this workspace.
9
+ - This document therefore treats the current repository as the codebase you meant by "main-v5".
10
+
11
+ Goal of this document:
12
+ - give an LLM enough structure to navigate, modify, and extend the codebase safely
13
+ - explain not just file names, but runtime flow, ownership boundaries, data models, and cross-module dependencies
14
+ - surface invariants and hidden coupling points that matter when changing the code
15
+
16
+ Repository size snapshot:
17
+ - Python/source/documentation lines inspected: about 6,981 lines across the main tracked source files
18
+ - Core subsystems: `env`, `verifier`, `training`, `server`, top-level client/inference/test entrypoints, and `scripts`
19
+ - Empty or currently unused directories: `rewards/`, `outputs/` in this snapshot
20
+
21
+ ## One-Screen Summary
22
+
23
+ This repo implements **ADAPT**, an adversarial DSA tutoring environment for reinforcement learning and evaluation of code-generating models.
24
+
25
+ The system is organized around one central loop:
26
+
27
+ 1. A problem family is chosen by the generator.
28
+ 2. The environment exposes only the public statement plus two visible examples.
29
+ 3. A model submits Python code.
30
+ 4. The verifier statically checks the code, runs it in a subprocess sandbox, scores visible and hidden tests, and computes correctness/efficiency signals.
31
+ 5. The environment converts those signals into repair-oriented feedback and an episode reward.
32
+ 6. Training uses this environment as a reward source inside a GRPO loop.
33
+ 7. The server layer exposes reset/step/state plus training/model management endpoints.
34
+
35
+ The three most important files in the repo are:
36
+ - `env/adapt_env.py`: the runtime environment and episode state machine
37
+ - `env/generator.py`: the full problem-family registry plus deterministic problem generation
38
+ - `training/train_grpo.py`: prompt construction, curriculum logic, reward hookup, evaluation, and GRPO training
39
+
40
+ ## High-Level Architecture
41
+
42
+ ```text
43
+ Problem Generator -> AdaptEnvironment -> Verifier -> Reward/Feedback
44
+ | | | |
45
+ | | | |
46
+ v v v v
47
+ Problem family Session state Complexity + Repair-aware
48
+ sampling + cases + observation sandboxing training signal
49
+
50
+ Server API / Space Runtime
51
+ |
52
+ v
53
+ Training jobs, model loading, code generation
54
+ ```
55
+
56
+ ## Top-Level Files And Directories
57
+
58
+ ### Root
59
+
60
+ - `README.md`
61
+ Project narrative, architecture overview, run instructions, and problem-family summary.
62
+
63
+ - `pyproject.toml`
64
+ Main packaging and dependency definition.
65
+ Registers the installable packages: `env`, `server`, `training`, `verifier`.
66
+ Exposes script entrypoint `server = "server.app:main"`.
67
+
68
+ - `requirements.txt`
69
+ Inference/server/runtime dependencies only.
70
+
71
+ - `openenv.yaml`
72
+ OpenEnv metadata:
73
+ - runtime: `fastapi`
74
+ - app: `server.app:app`
75
+ - port: `7860`
76
+
77
+ - `Dockerfile`
78
+ Python 3.11 slim image, installs package with training extras, runs `uvicorn server.app:app`.
79
+
80
+ - `app.py`
81
+ Thin re-export of `server.app`.
82
+
83
+ - `client.py`
84
+ Local Python HTTP client for the environment and training endpoints.
85
+
86
+ - `models.py`
87
+ Pydantic schemas for actions, observations, and state.
88
+
89
+ - `inference.py`
90
+ Benchmark/evaluation runner that loops through tasks, calls an OpenAI-compatible API, and logs standardized `[START]/[STEP]/[END]` traces.
91
+
92
+ - `test.py`
93
+ Repository smoke-test aggregator; calls several script-level tests plus a verifier check.
94
+
95
+ ### `env/`
96
+
97
+ Owns problem generation, environment state, subprocess execution, and the local Gradio demo.
98
+
99
+ - `env/adapt_env.py`
100
+ - `env/generator.py`
101
+ - `env/executor.py`
102
+ - `env/test_cases.py`
103
+ - `env/app.py`
104
+
105
+ ### `verifier/`
106
+
107
+ Owns submission validation, execution scoring, reward metrics, and lightweight complexity analysis.
108
+
109
+ - `verifier/verifier.py`
110
+ - `verifier/sandbox.py`
111
+ - `verifier/metrics.py`
112
+ - `verifier/complexity.py`
113
+
114
+ ### `training/`
115
+
116
+ Owns prompt formatting, curriculum and generator control, GRPO training, evaluation, trace artifacts, and plotting.
117
+
118
+ - `training/train_grpo.py`
119
+ - `training/trace_logging.py`
120
+ - `training/plot_results.py`
121
+
122
+ ### `server/`
123
+
124
+ Owns the FastAPI service layer and Hugging Face Space runtime/training orchestration.
125
+
126
+ - `server/app.py`
127
+ - `server/runtime.py`
128
+ - `server/requirements.txt`
129
+
130
+ ### `scripts/`
131
+
132
+ Operational and smoke-test scripts.
133
+
134
+ - `scripts/deploy_and_smoke_train.py`
135
+ - `scripts/test_env.py`
136
+ - `scripts/test_space_api.py`
137
+ - `scripts/test_trace_logging.py`
138
+ - `scripts/test_training_config.py`
139
+ - `scripts/test_verifier.py`
140
+
141
+ ### Other
142
+
143
+ - `notebooks/colab_kaggle_smoke_train.ipynb`
144
+ Notebook-based smoke training support.
145
+
146
+ - `Qwen2.5_(3B)-GRPO.ipynb`
147
+ Large notebook artifact related to GRPO experimentation.
148
+
149
+ - `rewards/`
150
+ Present but empty in this snapshot.
151
+
152
+ - `outputs/`
153
+ Present but empty in this snapshot.
154
+
155
+ ## Core Runtime Concepts
156
+
157
+ ### 1. Problem
158
+
159
+ A generated problem is a dictionary produced mainly by `GeneratorAgent.generate_problem(...)` in `env/generator.py`.
160
+
161
+ Expected keys:
162
+ - `problem_id`
163
+ - `problem_type`
164
+ - `difficulty`
165
+ - `difficulty_label`
166
+ - `problem`
167
+ - `input_format`
168
+ - `constraints`
169
+ - `test_cases`
170
+ - `visible_problem`
171
+ - `generation_mode`
172
+ - `validity_bonus`
173
+
174
+ Important split:
175
+ - `test_cases` contains both visible and hidden tests
176
+ - `visible_problem` is the sanitized public-facing subset
177
+ - hidden tests must never leak into the observation payload
178
+
179
+ ### 2. Episode
180
+
181
+ An episode is one problem solved over up to `MAX_STEPS_PER_EPISODE = 3` attempts.
182
+
183
+ Attempt flow:
184
+ - attempt 1: initial submission
185
+ - attempt 2/3: repair attempt based on feedback
186
+ - episode ends early when hidden correctness reaches 1.0 and efficiency target is met, or after step 3
187
+
188
+ ### 3. Reward
189
+
190
+ There are two related reward layers:
191
+
192
+ - verifier-level reward in `verifier.metrics.compute_pass_rate(...)`
193
+ Used as raw execution-derived correctness reward.
194
+
195
+ - environment episode reward in `verifier.metrics.compute_episode_reward(...)`
196
+ This is the more important repair-aware reward used by `AdaptEnvironment`.
197
+ It accounts for:
198
+ - hidden pass rate
199
+ - step discount
200
+ - improvement over previous attempt
201
+ - efficiency score
202
+ - optimization completion target
203
+
204
+ ### 4. Generator Reward Signal
205
+
206
+ `env.generator.generator_reward(...)` measures how educational a generated problem is, targeting pass rates near 0.5.
207
+
208
+ This signal is used by training-side reward-aware family sampling:
209
+ - productive families become more likely to be sampled
210
+ - implemented by `GeneratorController.family_productivity`
211
+
212
+ ## Shared Schemas (`models.py`)
213
+
214
+ ### `AdaptAction`
215
+
216
+ Fields:
217
+ - `session_id`
218
+ - `code`
219
+
220
+ Meaning:
221
+ - server-routed step requests require a session id
222
+ - local environment calls may omit it because the environment can self-bootstrap
223
+
224
+ ### `AdaptObservation`
225
+
226
+ This is the main environment-facing payload.
227
+
228
+ Key fields:
229
+ - identity/context:
230
+ - `session_id`
231
+ - `problem_id`
232
+ - `problem_type`
233
+ - `difficulty`
234
+ - episode progress:
235
+ - `attempt_number`
236
+ - `max_steps`
237
+ - `done`
238
+ - public task content:
239
+ - `problem`
240
+ - `input_format`
241
+ - `constraints`
242
+ - `feedback`
243
+ - evaluation signals:
244
+ - `pass_rate`
245
+ - `visible_pass_rate`
246
+ - `hidden_pass_rate`
247
+ - `syntax_valid`
248
+ - `execution_status`
249
+ - `timeout_count`
250
+ - `runtime_error_count`
251
+ - `invalid_output_count`
252
+ - `wrong_answer_count`
253
+ - `format_compliance`
254
+ - `reward_components`
255
+ - `generator_reward_signal`
256
+ - `reward`
257
+
258
+ ### `AdaptState`
259
+
260
+ Server/state snapshot object; broader than the observation.
261
+
262
+ Important fields:
263
+ - active problem metadata
264
+ - generator mode
265
+ - generated public problem
266
+ - last reward / pass rate / feedback / execution status
267
+ - attempt history
268
+ - recent metrics
269
+ - generator reward signal
270
+
271
+ ## Subsystem 1: Environment (`env/`)
272
+
273
+ ### `env/adapt_env.py`
274
+
275
+ This is the environment state machine and the single most important runtime module.
276
+
277
+ #### Main class: `AdaptEnvironment`
278
+
279
+ Inheritance:
280
+ - attempts to inherit from `openenv.core.env_server.interfaces.Environment`
281
+ - falls back to a local generic stub if OpenEnv is unavailable
282
+
283
+ Important constants:
284
+ - `MAX_STEPS_PER_EPISODE = 3`
285
+ - `TARGET_EFFICIENCY_SCORE = 0.95`
286
+
287
+ Internal mutable state:
288
+ - `self.generator`
289
+ - `self.generator_mode`
290
+ - `self.session_id`
291
+ - `self.problem`
292
+ - `self.test_cases`
293
+ - `self.last_results`
294
+ - `self.history`
295
+ - `self.attempt_history`
296
+ - `self.previous_execution_status`
297
+ - `self.episode_done`
298
+ - `self._state` (`AdaptState`)
299
+
300
+ #### `reset(...)`
301
+
302
+ Responsibilities:
303
+ - optionally update session id / generator mode / difficulty
304
+ - choose or load a problem
305
+ - copy test cases into the environment
306
+ - clear prior attempt/results state
307
+ - initialize `AdaptState`
308
+ - return an initial observation with public problem text and instructions
309
+
310
+ Supports:
311
+ - forced `problem_id`
312
+ - forced `difficulty`
313
+ - externally injected `generated_problem`
314
+ - family weighting via `family_weights`
315
+
316
+ #### `step(action, ...)`
317
+
318
+ Responsibilities:
319
+ - lazy-reset if no problem is loaded
320
+ - return terminal "call reset" observation if episode already ended
321
+ - increment attempt number
322
+ - call `_verify_submission`
323
+ - compute hidden/visible pass rates and execution status
324
+ - compute repair-aware reward via `compute_episode_reward`
325
+ - build human-readable feedback
326
+ - record metrics/state
327
+ - finalize the episode if terminal
328
+
329
+ Terminal conditions:
330
+ - hidden pass rate is 1.0 and efficiency score >= 0.95
331
+ - or step number reaches 3
332
+
333
+ Important nuance:
334
+ - a fully correct but less efficient solution may produce `pass_rate == 1.0` but `done == False`
335
+ - the agent is then nudged to optimize further
336
+
337
+ #### `_verify_submission(...)`
338
+
339
+ Delegates to `verifier.verify(...)`.
340
+
341
+ Then augments the verifier metadata with:
342
+ - diversity bonus
343
+ - validity bonus
344
+ - generator reward signal
345
+
346
+ #### `_format_feedback(...)`
347
+
348
+ Produces repair-facing natural language feedback.
349
+
350
+ Behavior depends on status:
351
+ - syntax/safety violations get short static feedback
352
+ - other cases get:
353
+ - attempt number
354
+ - previous status
355
+ - current status
356
+ - hidden and visible pass rates
357
+ - efficiency score
358
+ - failed test summaries
359
+ - optional optimization hints
360
+
361
+ #### `_public_problem_view(...)`
362
+
363
+ Critical leakage-control function.
364
+
365
+ It uses:
366
+ - `visible_problem`
367
+ - formatted visible examples from `_format_examples()`
368
+
369
+ This is what gets surfaced to the agent.
370
+ Hidden tests stay internal.
371
+
372
+ #### History and curriculum-relevant state
373
+
374
+ `AdaptEnvironment` tracks:
375
+ - recent pass rates
376
+ - recent problem families
377
+ - generator rewards
378
+ - problem signatures
379
+ - episode index
380
+
381
+ This history influences future generation diversity and can be used by higher-level curriculum logic.
382
+
383
+ ### `env/generator.py`
384
+
385
+ This file is the full problem bank, generator, case synthesis engine, and reference-solver library.
386
+ It is the largest single file in the repo.
387
+
388
+ #### Core constants
389
+
390
+ - `VISIBLE_TEST_COUNT = 2`
391
+ - `HIDDEN_TEST_COUNT = 8`
392
+ - `TOTAL_TEST_CASES = 10`
393
+ - `MIN_TEST_CASES = 10`
394
+
395
+ #### `ProblemTemplate`
396
+
397
+ Dataclass fields:
398
+ - `problem_type`
399
+ - `difficulty_tier`
400
+ - `title`
401
+ - `input_format`
402
+ - `constraints`
403
+ - `statement_builder`
404
+ - `solver`
405
+ - `case_builder`
406
+
407
+ This is the canonical template unit from which all problems are built.
408
+
409
+ #### `generator_reward(pass_rate, diversity_bonus, validity_bonus)`
410
+
411
+ Educational-value reward:
412
+ - highest when pass rate is near 0.5
413
+ - clipped into `[0.0, 1.5]`
414
+ - later used by the reward-aware family sampler in training
415
+
416
+ #### `validate_problem(problem_dict)`
417
+
418
+ Hard validation gate for generated problems.
419
+
420
+ Checks:
421
+ - required keys exist
422
+ - public text fields are non-empty
423
+ - scalar difficulty is between 0 and 1
424
+ - exactly 10 test cases
425
+ - visible/hidden ordering and flags are correct
426
+ - test inputs are unique
427
+ - output diversity is high enough to avoid degenerate problems
428
+
429
+ This function is critical if changing generator behavior.
430
+
431
+ #### `normalize_problem(problem_dict)`
432
+
433
+ Normalizes string fields and shallow copies test cases / visible problem data.
434
+
435
+ #### `GeneratorAgent`
436
+
437
+ Dependency-free deterministic generator by default.
438
+
439
+ Main behaviors:
440
+ - `generate_problem(...)`
441
+ - `generate(...)` thin alias
442
+ - `_choose_template(...)`
443
+ - `_rng_for(...)`
444
+ - `_problem_signature(...)`
445
+ - `_tier_to_scalar(...)`
446
+
447
+ Determinism strategy:
448
+ - hashes a seed material bundle containing difficulty, recent pass rates, recent problem types, episode index, and family weights
449
+ - creates a `random.Random` instance from that hash
450
+
451
+ This means generation is reproducible for the same history and settings.
452
+
453
+ #### Template registry
454
+
455
+ `_build_templates()` registers 20 problem families:
456
+
457
+ Easy:
458
+ - `sum_even_numbers`
459
+ - `range_span`
460
+ - `count_vowels`
461
+ - `max_consecutive_ones`
462
+ - `fizzbuzz_variant`
463
+ - `running_total`
464
+
465
+ Medium:
466
+ - `count_local_peaks`
467
+ - `longest_non_decreasing_run`
468
+ - `two_sum_count`
469
+ - `max_subarray_sum`
470
+ - `group_anagrams_count`
471
+ - `balanced_brackets`
472
+ - `matrix_diagonal_sum`
473
+
474
+ Hard:
475
+ - `smallest_most_frequent`
476
+ - `reverse_words`
477
+ - `longest_common_subsequence`
478
+ - `word_ladder_steps`
479
+ - `merge_intervals`
480
+ - `min_coins`
481
+ - `rotate_matrix_90`
482
+
483
+ For each family, the file contains:
484
+ - statement text
485
+ - input format
486
+ - constraints
487
+ - visible/hidden randomized case builders
488
+ - a reference solver used to derive expected outputs
489
+
490
+ #### Bottom half of the file
491
+
492
+ The remainder of `env/generator.py` contains:
493
+ - case factory helpers for each family
494
+ - solver implementations for each family
495
+ - parsers for input formats
496
+ - random instance builders for bracket/path/word-ladder style tasks
497
+ - formatting helpers for arrays, targets, intervals, matrices, coins, etc.
498
+
499
+ Practical meaning:
500
+ - if you add a new problem family, most of the work belongs here
501
+ - the generator is intentionally self-contained; it does not depend on external datasets
502
+
503
+ ### `env/executor.py`
504
+
505
+ Owns actual subprocess execution of user code.
506
+
507
+ Execution strategy:
508
+ - writes submission code to a temporary file under `.adapt_tmp` or `ADAPT_TMP_DIR`
509
+ - runs `python -I -S submission.py`
510
+ - feeds stdin
511
+ - captures stdout/stderr
512
+ - enforces timeout
513
+ - truncates outputs to `OUTPUT_LIMIT_BYTES`
514
+
515
+ Important constants:
516
+ - timeout: `1.0s`
517
+ - memory limit: `512 MB`
518
+ - output limit: `256 KB`
519
+
520
+ Linux-only hardening:
521
+ - RLIMIT for address space, CPU, file size, and file descriptors
522
+
523
+ Portable hardening:
524
+ - isolated Python mode
525
+ - stripped environment variables
526
+ - temp HOME/TMP
527
+
528
+ ### `env/test_cases.py`
529
+
530
+ Convenience layer over the generator.
531
+
532
+ Main functions:
533
+ - `load_problem_bank()`
534
+ Builds one representative problem per template for task listing.
535
+ - `load_problem(problem_id=None, difficulty=None)`
536
+ Loads or samples a single problem.
537
+ - `get_test_cases(...)`
538
+ Returns copied test cases.
539
+ - `split_test_cases(...)`
540
+ Splits visible vs hidden tests.
541
+
542
+ Server and inference logic depend on this file for task enumeration.
543
+
544
+ ### `env/app.py`
545
+
546
+ Gradio demo app, separate from the FastAPI/OpenEnv server.
547
+
548
+ Global state:
549
+ - `TRAINING_MANAGER = SpaceTrainingManager()`
550
+ - `SESSIONS: dict[str, AdaptEnvironment]`
551
+
552
+ Main interactive functions:
553
+ - `_get_env(...)`
554
+ - `_problem_markdown(...)`
555
+ - `sample_problem(...)`
556
+ - `evaluate_submission(...)`
557
+ - `model_attempt(...)`
558
+
559
+ Use case:
560
+ - manual exploration of sampled problems
561
+ - manual verification of code
562
+ - comparison against the current loaded model's generated code
563
+
564
+ Notable design choice:
565
+ - `model_attempt(...)` calls `TRAINING_MANAGER.generate_code(...)`, then immediately feeds that code back through `evaluate_submission(...)`
566
+ - so the UI path uses the exact same verifier/environment loop as manual submissions
567
+
568
+ ## Subsystem 2: Verification (`verifier/`)
569
+
570
+ ### `verifier/verifier.py`
571
+
572
+ Central verification orchestration.
573
+
574
+ Function: `verify(code, test_cases, step_number=1)`
575
+
576
+ Pipeline:
577
+ 1. `validate_code(code)` from `verifier.sandbox`
578
+ 2. `analyze_code_complexity(code)` from `verifier.complexity`
579
+ 3. If syntax/safety fails:
580
+ - compute status/reward with empty results
581
+ - return immediately
582
+ 4. Else execute against each test case via `verifier.sandbox.run_code(...)`
583
+ 5. Build per-test result records
584
+ 6. Compute pass-rate metrics with `compute_pass_rate(...)`
585
+ 7. Build hidden-facing feedback summary
586
+
587
+ Per-test result record includes:
588
+ - index
589
+ - status
590
+ - passed
591
+ - format_ok
592
+ - stdout/stderr
593
+ - expected/input only for visible tests
594
+ - timed_out
595
+ - exit_code
596
+ - duration_ms
597
+ - sandbox metadata
598
+ - visibility
599
+
600
+ Security boundary:
601
+ - visible tests expose expected/actual/input in the result metadata
602
+ - hidden tests intentionally suppress those values
603
+
604
+ ### `verifier/sandbox.py`
605
+
606
+ Static validation plus runtime delegation.
607
+
608
+ Forbidden imports:
609
+ - `ctypes`
610
+ - `os`
611
+ - `pathlib`
612
+ - `resource`
613
+ - `shutil`
614
+ - `signal`
615
+ - `socket`
616
+ - `subprocess`
617
+
618
+ Forbidden calls:
619
+ - `__import__`
620
+ - `breakpoint`
621
+ - `compile`
622
+ - `eval`
623
+ - `exec`
624
+ - `open`
625
+
626
+ Main functions:
627
+ - `validate_code(code)`
628
+ - `run_code(code, stdin, timeout=1)`
629
+
630
+ Important nuance:
631
+ - runtime isolation is actually implemented in `env.executor.run_code`
632
+ - this file adds AST-level policy checks before execution
633
+
634
+ ### `verifier/metrics.py`
635
+
636
+ Owns reward math.
637
+
638
+ Constants:
639
+ - `STEP_DISCOUNTS = {1: 1.0, 2: 0.85, 3: 0.70}`
640
+ - `TERMINAL_ZERO_STATUSES = {"syntax_error", "safety_violation", "timeout"}`
641
+
642
+ Main functions:
643
+ - `step_discount(...)`
644
+ - `compute_reward(...)`
645
+ - `compute_pass_rate(...)`
646
+ - `compute_episode_reward(...)`
647
+
648
+ #### `compute_pass_rate(...)`
649
+
650
+ Produces:
651
+ - pass counts
652
+ - hidden/visible pass rates
653
+ - execution status classification
654
+ - format compliance
655
+ - reward components
656
+ - verifier components
657
+
658
+ Execution status priority order:
659
+ - syntax error
660
+ - safety violation
661
+ - precheck status
662
+ - timeout
663
+ - runtime error
664
+ - invalid output format
665
+ - wrong answer
666
+ - completed
667
+
668
+ #### `compute_episode_reward(...)`
669
+
670
+ This is the repair-aware environment reward.
671
+
672
+ Behavior:
673
+ - syntax/safety/timeout => `0.0`
674
+ - correct solution => discounted reward blended with efficiency score
675
+ - incorrect but non-terminal and improved => small progress reward (`0.1 * delta`)
676
+ - final failed attempt => `0.0`
677
+
678
+ This function is one of the most behavior-critical points in the codebase.
679
+
680
+ ### `verifier/complexity.py`
681
+
682
+ Static efficiency heuristic.
683
+
684
+ Signals tracked:
685
+ - nested loop depth
686
+ - list/set/dict comprehensions
687
+ - generator expressions
688
+ - sorting calls
689
+ - materialized builtin inputs
690
+
691
+ Outputs:
692
+ - `time_complexity_score`
693
+ - `space_complexity_score`
694
+ - `efficiency_score`
695
+ - `optimization_hints`
696
+ - `complexity_signals`
697
+
698
+ Role in the system:
699
+ - not a formal complexity analyzer
700
+ - acts as a lightweight shaping mechanism for "correct but inefficient" solutions
701
+
702
+ ## Subsystem 3: Training (`training/`)
703
+
704
+ ### `training/train_grpo.py`
705
+
706
+ This is the second-most important file after `env/generator.py`.
707
+ It owns the full RL training path.
708
+
709
+ #### Global constants
710
+
711
+ - `SYSTEM_PROMPT`
712
+ Shared solver instruction string.
713
+
714
+ - `CRITICAL_PROJECTION_NAMES`
715
+ Used during precision audits.
716
+
717
+ - `SMOKE_PREFERRED_PRECISION = "fp16"`
718
+
719
+ #### `TrainingConfig`
720
+
721
+ Dataclass with the full trainable configuration surface.
722
+
723
+ Important fields:
724
+ - model/runtime:
725
+ - `model_name`
726
+ - `output_dir`
727
+ - `load_in_4bit`
728
+ - `gradient_checkpointing`
729
+ - `bf16`
730
+ - `save_merged_model`
731
+ - data/training:
732
+ - `dataset_size`
733
+ - `max_steps`
734
+ - `batch_size`
735
+ - `gradient_accumulation_steps`
736
+ - `num_generations`
737
+ - `learning_rate`
738
+ - token lengths:
739
+ - `max_seq_length`
740
+ - `max_prompt_length`
741
+ - `max_completion_length`
742
+ - LoRA:
743
+ - `lora_rank`
744
+ - `lora_alpha`
745
+ - evaluation:
746
+ - `baseline_eval`
747
+ - `evaluation_episodes`
748
+ - `eval_max_new_tokens`
749
+ - logging/control:
750
+ - `disable_wandb`
751
+ - `wandb_project`
752
+ - `wandb_run_name`
753
+ - `generator_mode`
754
+ - `non_deterministic_generator`
755
+ - `trace_logging_enabled`
756
+ - `checkpoint_log_interval_steps`
757
+
758
+ #### Training presets
759
+
760
+ Defined in `TRAINING_PRESETS`:
761
+ - `smoke`
762
+ - `l4`
763
+ - `default`
764
+
765
+ `smoke`:
766
+ - tiny, CPU/fallback-friendly
767
+ - no 4-bit
768
+ - no gradient checkpointing
769
+ - short run
770
+
771
+ `l4` and `default`:
772
+ - intended for real LoRA + 4-bit training
773
+ - default model: `Qwen/Qwen2.5-3B-Instruct`
774
+
775
+ #### Prompt helpers
776
+
777
+ - `extract_code(...)`
778
+ - `format_examples(...)`
779
+ - `build_solver_prompt(...)`
780
+ - `build_prompt_from_problem(...)`
781
+
782
+ Important prompt shape:
783
+ - problem id/family/difficulty
784
+ - attempt number
785
+ - problem statement with examples
786
+ - input format
787
+ - constraints
788
+ - feedback
789
+
790
+ This exact shape is reused by training, evaluation, and server-side model generation.
791
+
792
+ #### Config helpers
793
+
794
+ - `build_training_config(...)`
795
+ - `namespace_to_config(...)`
796
+
797
+ These are used both by CLI and by the server runtime manager.
798
+
799
+ #### `CurriculumManager`
800
+
801
+ Adaptive difficulty controller.
802
+
803
+ Defaults:
804
+ - difficulties: `easy`, `medium`, `hard`
805
+ - moving window: `10`
806
+ - promote threshold: `0.70`
807
+ - demote threshold: `0.30`
808
+
809
+ Behavior:
810
+ - tracks recent episode pass rates
811
+ - promotes/demotes based on moving average
812
+ - clears history after each level change
813
+
814
+ #### `GeneratorController`
815
+
816
+ Training-side wrapper over `GeneratorAgent`.
817
+
818
+ Responsibilities:
819
+ - sample problems for a target difficulty
820
+ - register prompt -> problem mapping
821
+ - compute reward-aware family weights
822
+ - track family productivity EMA
823
+ - update generator-side history after each episode
824
+
825
+ Key feature:
826
+ - in `reward_aware` mode, family selection is biased by a softmax over `family_productivity / temperature`
827
+
828
+ #### `GeneratorRolloutDataset`
829
+
830
+ Minimal dataset adapter used by GRPO.
831
+
832
+ Each item:
833
+ - samples a new problem at the current curriculum difficulty
834
+ - returns a dict with only `{"prompt": prompt}`
835
+
836
+ This means the dataset is not static; prompts are generated online.
837
+
838
+ #### `TrainingLogger`
839
+
840
+ Owns:
841
+ - in-memory reward/event rows
842
+ - optional Weights & Biases logging
843
+ - optional trace artifacts through `TraceArtifactLogger`
844
+ - CSV writing
845
+
846
+ Every logged row includes:
847
+ - phase
848
+ - episode reward
849
+ - pass rate
850
+ - visible pass rate
851
+ - difficulty
852
+ - family
853
+ - curriculum level
854
+ - execution status
855
+ - attempt number
856
+ - family productivity columns
857
+ - extra metadata like prompt/completion/code/feedback
858
+
859
+ #### `build_reward_func(...)`
860
+
861
+ This is the bridge between GRPOTrainer and the environment.
862
+
863
+ For each `(prompt, completion)` pair:
864
+ - resolve original problem from prompt registry
865
+ - spin up `AdaptEnvironment`
866
+ - reset with the exact generated problem
867
+ - run one environment step using extracted code
868
+ - record reward
869
+ - update generator controller
870
+ - update curriculum
871
+ - log event
872
+ - emit progress callback payload
873
+
874
+ This function is the key glue layer for RL.
875
+
876
+ #### `generate_completion(...)`
877
+
878
+ Shared inference helper for local model objects.
879
+
880
+ Responsibilities:
881
+ - render chat template if tokenizer supports it
882
+ - choose generation device from HF device map or model device
883
+ - call `model.generate(...)`
884
+ - decode only newly generated tokens
885
+
886
+ Used by:
887
+ - evaluation
888
+ - server runtime generation
889
+
890
+ #### `run_policy_evaluation(...)`
891
+
892
+ Offline evaluator over a schedule of easy/medium/hard episodes.
893
+
894
+ Loop:
895
+ - sample a problem
896
+ - run up to 3 repair attempts
897
+ - log final result
898
+ - aggregate pass rates by tier
899
+
900
+ Outputs summary:
901
+ - easy
902
+ - medium
903
+ - hard
904
+ - overall
905
+
906
+ #### Precision/runtime utilities
907
+
908
+ - `get_runtime_versions(...)`
909
+ - `validate_runtime_versions(...)`
910
+ - `resolve_precision_policy(...)`
911
+ - `normalize_model_precision(...)`
912
+ - `audit_critical_module_precision(...)`
913
+
914
+ These guardrails exist because the training path is tuned around Unsloth + GRPO + mixed precision.
915
+
916
+ #### `run_training(...)`
917
+
918
+ The full training entrypoint.
919
+
920
+ Major stages:
921
+ 1. Normalize config
922
+ 2. Import `torch`, `trl`, `unsloth`, and transformers callback support
923
+ 3. Create output directory
924
+ 4. Patch Unsloth RL stack
925
+ 5. Resolve runtime versions and precision policy
926
+ 6. Load base model/tokenizer via `FastLanguageModel.from_pretrained`
927
+ 7. Apply LoRA with `FastLanguageModel.get_peft_model`
928
+ 8. Run precision audit(s)
929
+ 9. Create curriculum, generator controller, and logger
930
+ 10. Optionally run baseline evaluation
931
+ 11. Build `GRPOConfig`
932
+ 12. Build `GRPOTrainer` with online prompt dataset and environment reward function
933
+ 13. Train
934
+ 14. Save adapter or merged model
935
+ 15. Optionally run trained evaluation
936
+ 16. Write reward CSV and finalize trace artifacts
937
+ 17. Return a summary payload containing paths and metrics
938
+
939
+ Output summary includes:
940
+ - config
941
+ - runtime versions
942
+ - precision mode/policy/audits
943
+ - `output_dir`
944
+ - `reward_curve_csv`
945
+ - trace artifact paths
946
+ - baseline/trained summaries
947
+ - completed steps
948
+
949
+ #### CLI
950
+
951
+ `build_parser()` exposes the training surface as a command-line interface.
952
+
953
+ Entrypoint:
954
+ - `python training/train_grpo.py ...`
955
+
956
+ ### `training/trace_logging.py`
957
+
958
+ Structured artifact logger for Space/server integration and post-run analysis.
959
+
960
+ Main class: `TraceArtifactLogger`
961
+
962
+ Artifacts under `output_dir/logs/`:
963
+ - `run_manifest.json`
964
+ - `events.jsonl`
965
+ - `latest_checkpoint.json`
966
+ - `checkpoint_step_XXXXX.json`
967
+ - `run_summary.json`
968
+
969
+ Tracked rolling metrics:
970
+ - average reward
971
+ - average pass rate
972
+ - average efficiency score
973
+
974
+ Used by:
975
+ - `TrainingLogger` in `training/train_grpo.py`
976
+ - surfaced through `server.runtime.SpaceTrainingManager`
977
+
978
+ ### `training/plot_results.py`
979
+
980
+ Offline plotting tool for `reward_curve.csv`.
981
+
982
+ Outputs:
983
+ - `reward_curve.png`
984
+ - `pass_rate_by_difficulty.png`
985
+ - `family_productivity.png`
986
+
987
+ Important note:
988
+ - `plot_pass_rate_by_difficulty(...)` groups rows by `difficulty_tier`
989
+ - the function then iterates over `"easy"`, `"medium"`, `"hard"`
990
+ - this assumes the CSV contains string difficulty labels, which it currently does
991
+
992
+ ## Subsystem 4: Server And Space Runtime (`server/`)
993
+
994
+ ### `server/app.py`
995
+
996
+ FastAPI surface for OpenEnv plus training/model utilities.
997
+
998
+ Global state:
999
+ - `SESSIONS`
1000
+ - `SESSION_LAST_ACCESSED`
1001
+ - `TRAINING_MANAGER = SpaceTrainingManager()`
1002
+ - `TASKS` generated from `env.test_cases.load_problem_bank()`
1003
+
1004
+ Session policy:
1005
+ - TTL: 30 minutes
1006
+ - unknown/expired sessions raise 404
1007
+
1008
+ #### Request models
1009
+
1010
+ - `ResetRequest`
1011
+ - `TrainRequest`
1012
+ - `RunTrainedPolicyRequest`
1013
+ - `GenerateCodeRequest`
1014
+
1015
+ #### Informational endpoints
1016
+
1017
+ - `GET /`
1018
+ Root metadata plus training/model status and active session count
1019
+ - `GET /health`
1020
+ - `GET /metadata`
1021
+ - `GET /tasks`
1022
+ - `GET /schema`
1023
+ - `GET /train/status`
1024
+ - `GET /model/status`
1025
+
1026
+ #### Environment endpoints
1027
+
1028
+ - `POST /reset`
1029
+ Creates a new `AdaptEnvironment`, stores it in `SESSIONS`, returns initial observation.
1030
+
1031
+ - `POST /step`
1032
+ Accepts either raw action JSON or `{ "action": ... }`.
1033
+ Requires `session_id`.
1034
+ Returns:
1035
+ - `observation`
1036
+ - `reward`
1037
+ - `done`
1038
+ - compact `info`
1039
+
1040
+ - `GET /state`
1041
+ Returns `AdaptState`.
1042
+
1043
+ #### Model/training endpoints
1044
+
1045
+ - `POST /train`
1046
+ Starts async training via `SpaceTrainingManager.start_training(...)`
1047
+
1048
+ - `POST /run-trained-policy`
1049
+ Runs current model through a full 3-attempt environment episode
1050
+
1051
+ - `POST /generate-code`
1052
+ One-shot generation for a supplied problem payload
1053
+
1054
+ #### MCP endpoint
1055
+
1056
+ - `POST /mcp`
1057
+ Stub only; always returns "not implemented"
1058
+
1059
+ ### `server/runtime.py`
1060
+
1061
+ Owns asynchronous training and model-loading behavior for a Space-like deployment.
1062
+
1063
+ This file has two major classes:
1064
+ - `SpaceModelRegistry`
1065
+ - `SpaceTrainingManager`
1066
+
1067
+ It also defines two dataclasses used as persisted status:
1068
+ - `ModelState`
1069
+ - `TrainingJobState`
1070
+
1071
+ #### `ModelState`
1072
+
1073
+ Tracks:
1074
+ - whether a model is loaded
1075
+ - whether the active generation source is `trained`, `base`, or `unavailable`
1076
+ - repo id / local path / revision
1077
+ - base model name
1078
+ - load time
1079
+ - last error
1080
+
1081
+ #### `TrainingJobState`
1082
+
1083
+ Tracks:
1084
+ - job status and run id
1085
+ - config
1086
+ - timestamps
1087
+ - artifact paths
1088
+ - uploaded model revision
1089
+ - logs location
1090
+ - current phase and progress
1091
+ - precision/runtime audit metadata
1092
+ - last seen train metrics
1093
+ - baseline/trained summaries
1094
+ - error and traceback
1095
+
1096
+ This is what backs `/train/status`.
1097
+
1098
+ #### `SpaceModelRegistry`
1099
+
1100
+ Primary responsibilities:
1101
+ - load base model when no trained model is available
1102
+ - load trained artifact from local disk or Hugging Face Hub
1103
+ - choose generation stack
1104
+ - fallback from trained model to base model if trained generation fails
1105
+ - run policy episodes and one-shot code generation
1106
+
1107
+ Important methods:
1108
+ - `_require_runtime_dependencies(...)`
1109
+ - `load_base_model()`
1110
+ - `load_from_local(...)`
1111
+ - `load_latest_from_hub()`
1112
+ - `run_policy(...)`
1113
+ - `generate_code(...)`
1114
+
1115
+ Environment variables it cares about:
1116
+ - `HF_MODEL_REPO_ID`
1117
+ - `HF_TOKEN`
1118
+ - `BASE_MODEL_NAME`
1119
+ - `MODEL_NAME`
1120
+
1121
+ Fallback behavior:
1122
+ - if no trained model is loadable, base-model generation can still work
1123
+ - if trained-model generation errors, it can fall back to base generation and record the fallback reason
1124
+
1125
+ #### `SpaceTrainingManager`
1126
+
1127
+ Primary responsibilities:
1128
+ - persist training job state to disk
1129
+ - prevent concurrent training runs
1130
+ - build output directory for each run
1131
+ - spawn background training thread
1132
+ - upload artifacts to the Hub
1133
+ - delete local logs after upload
1134
+ - refresh active model after successful upload
1135
+
1136
+ State files:
1137
+ - status persisted in `training_status.json` under `SPACE_OUTPUT_ROOT` or `/tmp/adapt-space`
1138
+
1139
+ Key methods:
1140
+ - `_restore_status()`
1141
+ - `_persist_status()`
1142
+ - `_update_progress(...)`
1143
+ - `status_payload()`
1144
+ - `start_training(...)`
1145
+ - `_run_training_job(...)`
1146
+ - `_upload_artifacts(...)`
1147
+ - `_cleanup_local_logs(...)`
1148
+ - `load_latest_model()`
1149
+ - `run_trained_policy(...)`
1150
+ - `generate_code(...)`
1151
+
1152
+ Background training behavior:
1153
+ - `start_training(...)` creates a `TrainingConfig` using `build_training_config(...)`
1154
+ - output dir is namespaced by `run_id`
1155
+ - training happens on a daemon thread
1156
+ - progress updates come from `run_training(..., progress_callback=...)`
1157
+
1158
+ Successful completion path:
1159
+ 1. run training
1160
+ 2. upload artifacts to HF model repo
1161
+ 3. delete local logs
1162
+ 4. load latest trained model from Hub
1163
+ 5. mark status as `succeeded`
1164
+
1165
+ Failure path:
1166
+ - mark status as `failed`
1167
+ - persist traceback
1168
+ - optionally clean logs first
1169
+
1170
+ ## Top-Level Utility Entrypoints
1171
+
1172
+ ### `client.py`
1173
+
1174
+ `AdaptEnvClient` wraps HTTP operations for:
1175
+ - `reset`
1176
+ - `step`
1177
+ - `state`
1178
+ - `train`
1179
+ - `train_status`
1180
+ - `model_status`
1181
+ - `run_trained_policy`
1182
+ - `generate_code`
1183
+
1184
+ Useful for:
1185
+ - local automation
1186
+ - notebooks
1187
+ - scripted experimentation against the FastAPI app
1188
+
1189
+ ### `inference.py`
1190
+
1191
+ OpenAI-compatible external inference runner.
1192
+
1193
+ Key behaviors:
1194
+ - loads tasks from `env.test_cases.load_problem_bank()`
1195
+ - resets an `AdaptEnvironment` per task
1196
+ - builds a JSON user payload from the observation
1197
+ - calls `OpenAI(...).chat.completions.create(...)`
1198
+ - extracts code
1199
+ - runs up to 3 repair attempts
1200
+ - logs machine-readable stdout lines
1201
+
1202
+ Important env vars:
1203
+ - `HF_TOKEN`
1204
+ - `API_BASE_URL` default `https://router.huggingface.co/v1`
1205
+ - `MODEL_NAME` default `openai/gpt-oss-120b`
1206
+
1207
+ This file is separate from the local model runtime in `server/runtime.py`.
1208
+ It is for remote API-based evaluation, not local HF model objects.
1209
+
1210
+ ### `test.py`
1211
+
1212
+ Simple "run several smokes in sequence" script.
1213
+
1214
+ Calls:
1215
+ - `scripts.test_env.main`
1216
+ - `scripts.test_space_api.main`
1217
+ - `scripts.test_training_config.main`
1218
+ - `scripts.test_trace_logging.main`
1219
+ - then a direct verifier correctness check
1220
+
1221
+ ## Scripts (`scripts/`)
1222
+
1223
+ ### `scripts/deploy_and_smoke_train.py`
1224
+
1225
+ Operational helper for pushing to a Space and smoke-running training remotely.
1226
+
1227
+ Capabilities:
1228
+ - optionally auto-commit and push repo changes
1229
+ - wait for `/health`
1230
+ - start `/train`
1231
+ - poll `/train/status`
1232
+ - follow an already-running job if requested
1233
+
1234
+ Key modes/flags:
1235
+ - `--skip-push`
1236
+ - `--skip-health-check`
1237
+ - `--trigger-only`
1238
+ - `--status-only`
1239
+ - `--follow-running`
1240
+
1241
+ This is a deployment/ops script, not part of core runtime.
1242
+
1243
+ ### `scripts/test_env.py`
1244
+
1245
+ Best environment behavior reference in the repo.
1246
+
1247
+ It verifies:
1248
+ - public examples are exposed
1249
+ - hidden tests are not exposed
1250
+ - correct first-attempt solution gets full reward
1251
+ - repair trajectory produces discounted reward
1252
+ - correct-but-less-efficient solution remains open for optimization
1253
+ - syntax/runtime/timeout/safety states behave as expected
1254
+
1255
+ If you need to understand intended environment semantics, start here after `adapt_env.py`.
1256
+
1257
+ ### `scripts/test_space_api.py`
1258
+
1259
+ FastAPI smoke tests with patched training manager behavior.
1260
+
1261
+ Verifies:
1262
+ - root/model/train status endpoints
1263
+ - reset/step/state flow
1264
+ - expected 409 when no trained model is available for policy run
1265
+ - generate-code path
1266
+ - training start and conflict handling
1267
+
1268
+ ### `scripts/test_trace_logging.py`
1269
+
1270
+ Smoke tests trace artifact creation.
1271
+
1272
+ Checks:
1273
+ - manifest creation
1274
+ - event JSONL writing
1275
+ - checkpoint writing
1276
+ - summary finalization
1277
+
1278
+ ### `scripts/test_training_config.py`
1279
+
1280
+ Tests:
1281
+ - preset construction
1282
+ - precision policy resolution under fake torch/cuda conditions
1283
+ - reward behavior for fully correct but sub-target efficiency case
1284
+
1285
+ ### `scripts/test_verifier.py`
1286
+
1287
+ Manual verification sanity suite over:
1288
+ - correct
1289
+ - wrong
1290
+ - less optimized
1291
+ - invalid output
1292
+ - timeout
1293
+ - runtime error
1294
+ - safety violation
1295
+
1296
+ Also confirms the complexity/efficiency signal differentiates optimized from less-optimized code.
1297
+
1298
+ ## End-To-End Flows
1299
+
1300
+ ### Flow A: API Environment Interaction
1301
+
1302
+ 1. Client calls `POST /reset`
1303
+ 2. `server.app.reset(...)` creates a new `AdaptEnvironment`
1304
+ 3. `AdaptEnvironment.reset(...)` loads/generates a problem and returns public observation
1305
+ 4. Client submits code via `POST /step`
1306
+ 5. `server.app.step(...)` validates `AdaptAction`
1307
+ 6. `AdaptEnvironment.step(...)` calls `verify(...)`
1308
+ 7. `verify(...)` validates AST safety, runs sandboxed code on tests, computes metrics
1309
+ 8. Environment converts metrics into feedback + reward + updated observation
1310
+ 9. `GET /state` returns broader session state if needed
1311
+
1312
+ ### Flow B: Local Training
1313
+
1314
+ 1. CLI or server builds `TrainingConfig`
1315
+ 2. `run_training(...)` loads base model and applies LoRA
1316
+ 3. `GeneratorRolloutDataset` emits prompts for current curriculum difficulty
1317
+ 4. GRPO generates completions
1318
+ 5. `build_reward_func(...)` routes completions through `AdaptEnvironment`
1319
+ 6. Environment returns reward
1320
+ 7. `CurriculumManager` updates difficulty
1321
+ 8. `GeneratorController` updates family productivity
1322
+ 9. `TrainingLogger` writes CSV/events/checkpoints
1323
+ 10. Model and tokenizer are saved
1324
+ 11. Optional trained evaluation runs
1325
+
1326
+ ### Flow C: Server-Side Training On Space
1327
+
1328
+ 1. `POST /train`
1329
+ 2. `SpaceTrainingManager.start_training(...)`
1330
+ 3. Background thread calls `_run_training_job(...)`
1331
+ 4. `run_training(...)` emits progress updates
1332
+ 5. Status persisted to `training_status.json`
1333
+ 6. Artifacts uploaded to HF repo
1334
+ 7. Local logs optionally deleted
1335
+ 8. Latest model reloaded from Hub
1336
+ 9. `/train/status` and `/model/status` reflect final state
1337
+
1338
+ ### Flow D: Model-Powered Generation In Demo Or API
1339
+
1340
+ 1. A problem payload is turned into a solver prompt with `build_solver_prompt(...)`
1341
+ 2. `SpaceModelRegistry.generate_code(...)` picks trained or base model
1342
+ 3. `generate_completion(...)` runs local generation
1343
+ 4. Returned completion is cleaned with `extract_code(...)`
1344
+ 5. In Gradio demo, generated code is immediately evaluated in the same environment loop
1345
+
1346
+ ## Important Invariants And Hidden Couplings
1347
+
1348
+ ### Hidden tests must never leak
1349
+
1350
+ Critical files:
1351
+ - `env/adapt_env.py`
1352
+ - `verifier/verifier.py`
1353
+ - `scripts/test_env.py`
1354
+
1355
+ If you change observation formatting, server payloads, or debug output, verify that hidden tests are still suppressed.
1356
+
1357
+ ### Prompt text is reused across training, evaluation, and generation
1358
+
1359
+ Critical file:
1360
+ - `training/train_grpo.py`
1361
+
1362
+ If you change `SYSTEM_PROMPT`, `build_solver_prompt(...)`, or `extract_code(...)`, you affect:
1363
+ - GRPO training
1364
+ - baseline/trained evaluation
1365
+ - `server.runtime` generation
1366
+ - `env.app` model attempt path
1367
+
1368
+ ### Reward semantics are distributed
1369
+
1370
+ Files:
1371
+ - `verifier/metrics.py`
1372
+ - `env/adapt_env.py`
1373
+ - `env/generator.py`
1374
+
1375
+ There are three conceptually different signals:
1376
+ - verifier correctness reward
1377
+ - episode reward
1378
+ - generator reward signal
1379
+
1380
+ Do not change one assuming it is the only reward in the system.
1381
+
1382
+ ### The generator is both data source and answer key
1383
+
1384
+ `env/generator.py` does all of these:
1385
+ - defines task families
1386
+ - generates inputs
1387
+ - defines reference solvers
1388
+ - defines expected outputs
1389
+
1390
+ If a reference solver is wrong, the entire environment silently becomes wrong.
1391
+
1392
+ ### Correctness is based on hidden tests, not visible tests
1393
+
1394
+ Visible tests are for examples and repair feedback.
1395
+ Core reward/curriculum decisions are driven by hidden pass rate.
1396
+
1397
+ ### Efficiency can block episode completion
1398
+
1399
+ An all-tests-passing answer can still have `done == False` if efficiency target is not met.
1400
+ This is intentional and affects reward, feedback, and repair behavior.
1401
+
1402
+ ### Space runtime is designed to degrade gracefully
1403
+
1404
+ `server/runtime.py` allows:
1405
+ - base model use when trained model is unavailable
1406
+ - trained-to-base fallback on generation error
1407
+
1408
+ This is useful operationally, but it means "generation works" does not always imply "trained model is active".
1409
+
1410
+ ## Change Impact Guide
1411
+
1412
+ ### If you want to add a new problem family
1413
+
1414
+ Primary file:
1415
+ - `env/generator.py`
1416
+
1417
+ You will usually need to add:
1418
+ - template entry in `_build_templates()`
1419
+ - case builder(s)
1420
+ - reference solver
1421
+ - any parser/helper utilities
1422
+
1423
+ Then verify:
1424
+ - `validate_problem(...)` still passes
1425
+ - `load_problem_bank()` includes the new family
1426
+ - Gradio dropdown in `env/app.py` optionally includes it
1427
+ - README family list stays aligned
1428
+
1429
+ ### If you want to change reward behavior
1430
+
1431
+ Primary files:
1432
+ - `verifier/metrics.py`
1433
+ - `env/adapt_env.py`
1434
+ - maybe `env/generator.py` if generator curriculum shaping should change
1435
+
1436
+ Then re-check:
1437
+ - `scripts/test_env.py`
1438
+ - `scripts/test_training_config.py`
1439
+ - training summaries and trace logging assumptions
1440
+
1441
+ ### If you want to change prompt style or model behavior
1442
+
1443
+ Primary file:
1444
+ - `training/train_grpo.py`
1445
+
1446
+ Also impacts:
1447
+ - `server/runtime.py`
1448
+ - `inference.py`
1449
+ - `env/app.py`
1450
+
1451
+ ### If you want to harden execution/security
1452
+
1453
+ Primary files:
1454
+ - `verifier/sandbox.py`
1455
+ - `env/executor.py`
1456
+
1457
+ Be careful to preserve:
1458
+ - valid competitive-programming style solutions
1459
+ - output capture behavior
1460
+ - timeout and isolation semantics
1461
+
1462
+ ### If you want to modify server payloads or API
1463
+
1464
+ Primary files:
1465
+ - `models.py`
1466
+ - `server/app.py`
1467
+ - `client.py`
1468
+ - possibly `scripts/test_space_api.py`
1469
+
1470
+ ## Recommended Reading Order For A New LLM
1471
+
1472
+ 1. `README.md`
1473
+ 2. `models.py`
1474
+ 3. `env/adapt_env.py`
1475
+ 4. `verifier/verifier.py`
1476
+ 5. `verifier/metrics.py`
1477
+ 6. `env/generator.py`
1478
+ 7. `training/train_grpo.py`
1479
+ 8. `server/app.py`
1480
+ 9. `server/runtime.py`
1481
+ 10. `scripts/test_env.py`
1482
+ 11. `scripts/test_space_api.py`
1483
+
1484
+ Why this order:
1485
+ - it starts with conceptual context
1486
+ - then schemas
1487
+ - then the core environment loop
1488
+ - then generation and reward internals
1489
+ - then training and serving
1490
+ - then tests as executable specification
1491
+
1492
+ ## File-By-File Responsibility Index
1493
+
1494
+ ### Root
1495
+
1496
+ - `README.md`: project rationale, architecture, usage docs
1497
+ - `pyproject.toml`: package/dependency metadata
1498
+ - `requirements.txt`: lighter runtime dependencies
1499
+ - `openenv.yaml`: OpenEnv deployment metadata
1500
+ - `Dockerfile`: container entrypoint
1501
+ - `app.py`: re-export server app
1502
+ - `client.py`: Python HTTP client wrapper
1503
+ - `models.py`: shared action/observation/state schemas
1504
+ - `inference.py`: remote API-based benchmark runner
1505
+ - `test.py`: combined smoke test launcher
1506
+
1507
+ ### `env/`
1508
+
1509
+ - `env/__init__.py`: re-exports generator utilities
1510
+ - `env/adapt_env.py`: main environment state machine
1511
+ - `env/app.py`: Gradio demo app
1512
+ - `env/executor.py`: subprocess execution sandbox
1513
+ - `env/generator.py`: problem registry, generators, solvers, helpers
1514
+ - `env/test_cases.py`: problem bank and split helpers
1515
+
1516
+ ### `verifier/`
1517
+
1518
+ - `verifier/__init__.py`: re-exports `verify`
1519
+ - `verifier/complexity.py`: static complexity heuristics
1520
+ - `verifier/metrics.py`: reward and scoring math
1521
+ - `verifier/sandbox.py`: AST safety checks and runtime bridge
1522
+ - `verifier/verifier.py`: verification orchestration
1523
+
1524
+ ### `training/`
1525
+
1526
+ - `training/__init__.py`: package marker
1527
+ - `training/plot_results.py`: offline plotting utilities
1528
+ - `training/trace_logging.py`: structured logs/checkpoints/summary artifacts
1529
+ - `training/train_grpo.py`: training, evaluation, prompts, config, curriculum
1530
+
1531
+ ### `server/`
1532
+
1533
+ - `server/__init__.py`: re-export app/main
1534
+ - `server/app.py`: FastAPI routes and session handling
1535
+ - `server/requirements.txt`: server/runtime dependency subset
1536
+ - `server/runtime.py`: model registry and training job manager
1537
+
1538
+ ### `scripts/`
1539
+
1540
+ - `scripts/deploy_and_smoke_train.py`: deploy-and-train automation
1541
+ - `scripts/test_env.py`: environment smoke spec
1542
+ - `scripts/test_space_api.py`: API smoke spec
1543
+ - `scripts/test_trace_logging.py`: trace logger smoke spec
1544
+ - `scripts/test_training_config.py`: config/precision/reward smoke spec
1545
+ - `scripts/test_verifier.py`: verifier behavior sanity script
1546
+
1547
+ ## Known Gaps Or Non-Core Artifacts
1548
+
1549
+ - No `main-v5/` directory exists in this workspace.
1550
+ - `rewards/` is empty in this snapshot.
1551
+ - `outputs/` is empty in this snapshot, so no completed run artifacts were available to inspect.
1552
+ - The notebook artifacts were not treated as the source of truth; the Python modules are the canonical implementation.
1553
+
1554
+ ## Bottom Line
1555
+
1556
+ If another LLM needs to work effectively in this repo, it should think of the codebase as five tightly linked layers:
1557
+
1558
+ 1. `env/generator.py` defines what the tasks are.
1559
+ 2. `verifier/*` defines how code is checked and scored.
1560
+ 3. `env/adapt_env.py` turns scoring into an interactive repair environment.
1561
+ 4. `training/train_grpo.py` plugs that environment into GRPO and curriculum learning.
1562
+ 5. `server/*` makes the environment, model runtime, and training lifecycle available over HTTP and in a Hugging Face Space.
1563
+
1564
+ That is the true structural spine of the repository.
env/adapt_env.py CHANGED
@@ -33,10 +33,15 @@ class AdaptEnvironment(Environment[AdaptAction, AdaptObservation, AdaptState]):
33
  self,
34
  generator: GeneratorAgent | None = None,
35
  generator_mode: str = "heuristic",
 
 
36
  session_id: str | None = None,
37
  ) -> None:
38
  super().__init__()
39
- self.generator = generator or GeneratorAgent()
 
 
 
40
  self.generator_mode = generator_mode
41
  self.session_id = session_id or str(uuid4())
42
  self.problem: dict[str, Any] = {}
 
33
  self,
34
  generator: GeneratorAgent | None = None,
35
  generator_mode: str = "heuristic",
36
+ use_dataset: bool = False,
37
+ dataset_kwargs: dict[str, Any] | None = None,
38
  session_id: str | None = None,
39
  ) -> None:
40
  super().__init__()
41
+ self.generator = generator or GeneratorAgent(
42
+ use_dataset=use_dataset,
43
+ dataset_kwargs=dataset_kwargs or {},
44
+ )
45
  self.generator_mode = generator_mode
46
  self.session_id = session_id or str(uuid4())
47
  self.problem: dict[str, Any] = {}
env/dataset_loader.py ADDED
@@ -0,0 +1,316 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ import hashlib
4
+ import random
5
+ import re
6
+ from typing import Any
7
+
8
+ from env.generator import HIDDEN_TEST_COUNT, TOTAL_TEST_CASES, VISIBLE_TEST_COUNT
9
+
10
+ MAX_IO_CHARS = 4096
11
+ DEFAULT_DATASET_NAME = "deepmind/code_contests"
12
+ DEFAULT_SPLIT = "train"
13
+ DEFAULT_MAX_PROBLEMS = 5000
14
+
15
+
16
+ def _load_raw_dataset(
17
+ dataset_name: str,
18
+ split: str = DEFAULT_SPLIT,
19
+ max_problems: int = DEFAULT_MAX_PROBLEMS,
20
+ ) -> list[dict[str, Any]]:
21
+ from datasets import load_dataset
22
+
23
+ dataset = load_dataset(dataset_name, split=split, trust_remote_code=True)
24
+ rows: list[dict[str, Any]] = []
25
+ for raw_row in dataset:
26
+ row = dict(raw_row)
27
+ statement = str(row.get("description") or row.get("question") or "").strip()
28
+ if not statement:
29
+ continue
30
+ if not _extract_pairs(row.get("public_tests")):
31
+ continue
32
+ if not (_extract_pairs(row.get("private_tests")) or _extract_pairs(row.get("generated_tests"))):
33
+ continue
34
+ rows.append(row)
35
+ if len(rows) >= int(max_problems):
36
+ break
37
+ return rows
38
+
39
+
40
+ def _normalise_row(raw_row: dict[str, Any], dataset_name: str) -> dict[str, Any] | None:
41
+ statement = _extract_problem_statement(raw_row)
42
+ if not statement:
43
+ return None
44
+
45
+ public_pairs = _extract_pairs(raw_row.get("public_tests"))
46
+ private_pairs = _extract_pairs(raw_row.get("private_tests"))
47
+ generated_pairs = _extract_pairs(raw_row.get("generated_tests"))
48
+ visible_pairs = public_pairs[:VISIBLE_TEST_COUNT]
49
+ hidden_pairs = (private_pairs + generated_pairs)[:HIDDEN_TEST_COUNT]
50
+ if len(visible_pairs) != VISIBLE_TEST_COUNT or len(hidden_pairs) != HIDDEN_TEST_COUNT:
51
+ return None
52
+
53
+ test_cases: list[dict[str, Any]] = []
54
+ seen_inputs: set[str] = set()
55
+ for index, (raw_input, raw_output) in enumerate(visible_pairs + hidden_pairs):
56
+ normalized_input = _normalize_io_text(raw_input, ensure_trailing_newline=True)
57
+ normalized_output = _normalize_io_text(raw_output, ensure_trailing_newline=False)
58
+ if not normalized_input or normalized_input in seen_inputs:
59
+ return None
60
+ seen_inputs.add(normalized_input)
61
+ test_cases.append(
62
+ {
63
+ "input": normalized_input,
64
+ "output": normalized_output,
65
+ "is_visible": index < VISIBLE_TEST_COUNT,
66
+ }
67
+ )
68
+
69
+ if len(test_cases) != TOTAL_TEST_CASES:
70
+ return None
71
+
72
+ difficulty_label, difficulty_value = _difficulty_fields(raw_row, dataset_name)
73
+ input_format = _extract_section(statement, "input") or "Read from stdin."
74
+ constraints = _extract_constraints(statement)
75
+ problem_type = _infer_problem_type(raw_row, statement)
76
+ problem_id = _problem_id(raw_row, dataset_name)
77
+
78
+ visible_examples = [dict(test_case) for test_case in test_cases[:VISIBLE_TEST_COUNT]]
79
+ return {
80
+ "problem_id": problem_id,
81
+ "problem_type": problem_type,
82
+ "difficulty": difficulty_value,
83
+ "difficulty_label": difficulty_label,
84
+ "problem": statement,
85
+ "input_format": input_format,
86
+ "constraints": constraints,
87
+ "test_cases": test_cases,
88
+ "visible_problem": {
89
+ "problem": statement,
90
+ "input_format": input_format,
91
+ "constraints": constraints,
92
+ "examples": visible_examples,
93
+ },
94
+ "generation_mode": "dataset",
95
+ "validity_bonus": 1.0,
96
+ }
97
+
98
+
99
+ class DatasetProblemBank:
100
+ def __init__(
101
+ self,
102
+ dataset_name: str = DEFAULT_DATASET_NAME,
103
+ split: str = DEFAULT_SPLIT,
104
+ max_problems: int = DEFAULT_MAX_PROBLEMS,
105
+ ) -> None:
106
+ self.dataset_name = dataset_name
107
+ self.split = split
108
+ self.max_problems = int(max_problems)
109
+ self._by_difficulty: dict[str, list[dict[str, Any]]] = {
110
+ "easy": [],
111
+ "medium": [],
112
+ "hard": [],
113
+ }
114
+ self._by_id: dict[str, dict[str, Any]] = {}
115
+
116
+ raw_rows = _load_raw_dataset(dataset_name=dataset_name, split=split, max_problems=max_problems)
117
+ for raw_row in raw_rows:
118
+ normalized = _normalise_row(raw_row, dataset_name)
119
+ if normalized is None:
120
+ continue
121
+ problem_id = str(normalized["problem_id"])
122
+ if problem_id in self._by_id:
123
+ continue
124
+ difficulty = str(normalized.get("difficulty_label", "medium")).lower()
125
+ if difficulty not in self._by_difficulty:
126
+ difficulty = "medium"
127
+ normalized["difficulty_label"] = difficulty
128
+ stored = _copy_problem(normalized)
129
+ self._by_difficulty[difficulty].append(stored)
130
+ self._by_id[problem_id] = stored
131
+
132
+ if not self._by_id:
133
+ raise ValueError(
134
+ f"No usable problems were found in dataset `{dataset_name}` split `{split}` with max_problems={max_problems}."
135
+ )
136
+
137
+ def sample(self, difficulty: str, rng: random.Random, recent_types: list[str]) -> dict[str, Any] | None:
138
+ requested = str(difficulty).strip().lower()
139
+ candidates = list(self._by_difficulty.get(requested, []))
140
+ if not candidates:
141
+ candidates = [problem for bucket in self._by_difficulty.values() for problem in bucket]
142
+ if not candidates:
143
+ return None
144
+
145
+ recent = {problem_type for problem_type in recent_types[-3:] if problem_type}
146
+ diverse = [problem for problem in candidates if str(problem.get("problem_type", "")) not in recent]
147
+ pool = diverse or candidates
148
+ return _copy_problem(rng.choice(pool))
149
+
150
+ def all_problem_ids(self) -> list[str]:
151
+ return sorted(self._by_id)
152
+
153
+ def get_by_id(self, problem_id: str) -> dict[str, Any]:
154
+ return _copy_problem(self._by_id[str(problem_id)])
155
+
156
+ def problem_types_for_difficulty(self, difficulty: str) -> list[str]:
157
+ requested = str(difficulty).strip().lower()
158
+ candidates = self._by_difficulty.get(requested, [])
159
+ return sorted({str(problem.get("problem_type", "")) for problem in candidates if problem.get("problem_type")})
160
+
161
+
162
+ _BANK: DatasetProblemBank | None = None
163
+ _BANK_CONFIG: tuple[str, str, int] | None = None
164
+
165
+
166
+ def get_problem_bank(**kwargs: Any) -> DatasetProblemBank:
167
+ global _BANK, _BANK_CONFIG
168
+
169
+ dataset_name = str(kwargs.get("dataset_name", DEFAULT_DATASET_NAME))
170
+ split = str(kwargs.get("split", DEFAULT_SPLIT))
171
+ max_problems = int(kwargs.get("max_problems", DEFAULT_MAX_PROBLEMS))
172
+ config = (dataset_name, split, max_problems)
173
+ if _BANK is None or _BANK_CONFIG != config:
174
+ _BANK = DatasetProblemBank(
175
+ dataset_name=dataset_name,
176
+ split=split,
177
+ max_problems=max_problems,
178
+ )
179
+ _BANK_CONFIG = config
180
+ return _BANK
181
+
182
+
183
+ def _extract_problem_statement(raw_row: dict[str, Any]) -> str:
184
+ value = raw_row.get("description") or raw_row.get("question") or raw_row.get("problem")
185
+ return str(value or "").strip()
186
+
187
+
188
+ def _extract_pairs(raw_value: Any) -> list[tuple[str, str]]:
189
+ pairs: list[tuple[str, str]] = []
190
+ if raw_value is None:
191
+ return pairs
192
+
193
+ if isinstance(raw_value, dict):
194
+ inputs = raw_value.get("input") or raw_value.get("inputs") or raw_value.get("stdin") or []
195
+ outputs = raw_value.get("output") or raw_value.get("outputs") or raw_value.get("stdout") or []
196
+ if isinstance(inputs, str):
197
+ inputs = [inputs]
198
+ if isinstance(outputs, str):
199
+ outputs = [outputs]
200
+ for raw_input, raw_output in zip(list(inputs), list(outputs)):
201
+ pairs.append((str(raw_input), str(raw_output)))
202
+ return pairs
203
+
204
+ if isinstance(raw_value, list):
205
+ for item in raw_value:
206
+ if isinstance(item, dict):
207
+ raw_input = item.get("input") or item.get("stdin") or item.get("in")
208
+ raw_output = item.get("output") or item.get("stdout") or item.get("out")
209
+ if raw_input is None or raw_output is None:
210
+ continue
211
+ pairs.append((str(raw_input), str(raw_output)))
212
+ elif isinstance(item, (list, tuple)) and len(item) >= 2:
213
+ pairs.append((str(item[0]), str(item[1])))
214
+ return pairs
215
+
216
+ return pairs
217
+
218
+
219
+ def _normalize_io_text(value: Any, *, ensure_trailing_newline: bool) -> str:
220
+ text = str(value or "")[:MAX_IO_CHARS]
221
+ if ensure_trailing_newline:
222
+ text = text.rstrip("\n")
223
+ return f"{text}\n" if text else "\n"
224
+ return text.strip()
225
+
226
+
227
+ def _difficulty_fields(raw_row: dict[str, Any], dataset_name: str) -> tuple[str, float]:
228
+ dataset_key = dataset_name.lower()
229
+ if "code_contests" in dataset_key or "code-contests" in dataset_key:
230
+ raw_difficulty = raw_row.get("difficulty")
231
+ try:
232
+ rating = float(raw_difficulty)
233
+ except (TypeError, ValueError):
234
+ return "medium", 0.5
235
+ normalized = max(0.0, min((rating - 800.0) / (3500.0 - 800.0), 1.0))
236
+ if rating <= 1200:
237
+ label = "easy"
238
+ elif rating <= 1800:
239
+ label = "medium"
240
+ else:
241
+ label = "hard"
242
+ return label, round(normalized, 4)
243
+
244
+ raw_difficulty = str(raw_row.get("difficulty") or "").strip().lower()
245
+ if raw_difficulty in {"easy", "medium", "hard"}:
246
+ return raw_difficulty, {"easy": 0.25, "medium": 0.5, "hard": 0.75}[raw_difficulty]
247
+ return "medium", 0.5
248
+
249
+
250
+ def _problem_id(raw_row: dict[str, Any], dataset_name: str) -> str:
251
+ prefix = "cc" if "code_contests" in dataset_name.lower() else "ds"
252
+ for key in ("problem_id", "id", "name", "source"):
253
+ value = raw_row.get(key)
254
+ if value is not None and str(value).strip():
255
+ candidate = re.sub(r"[^a-zA-Z0-9_]+", "_", str(value).strip()).strip("_")
256
+ if candidate:
257
+ return f"{prefix}_{candidate}"
258
+ digest = hashlib.sha256(repr(sorted(raw_row.items())).encode("utf-8")).hexdigest()
259
+ return f"{prefix}_{digest[:12]}"
260
+
261
+
262
+ def _extract_section(statement: str, heading: str) -> str:
263
+ pattern = re.compile(
264
+ rf"{heading}\s*:?[\r\n]+(.*?)(?=\n[A-Z][A-Za-z ]{{1,30}}:?[\r\n]|\Z)",
265
+ flags=re.IGNORECASE | re.DOTALL,
266
+ )
267
+ match = pattern.search(statement)
268
+ return match.group(1).strip() if match else ""
269
+
270
+
271
+ def _extract_constraints(statement: str) -> str:
272
+ constraints = _extract_section(statement, "constraints")
273
+ return constraints or "See problem statement."
274
+
275
+
276
+ def _infer_problem_type(raw_row: dict[str, Any], statement: str) -> str:
277
+ parts: list[str] = [statement]
278
+ tags = raw_row.get("tags")
279
+ if isinstance(tags, list):
280
+ parts.extend(str(tag) for tag in tags)
281
+ elif isinstance(tags, str):
282
+ parts.append(tags)
283
+ for key in ("source", "name"):
284
+ value = raw_row.get(key)
285
+ if value:
286
+ parts.append(str(value))
287
+ text = " ".join(parts).lower()
288
+
289
+ keyword_map = {
290
+ "graph": "graph",
291
+ "tree": "tree",
292
+ "dynamic programming": "dp",
293
+ " dp ": "dp",
294
+ "string": "string",
295
+ "array": "array",
296
+ "greedy": "greedy",
297
+ "math": "math",
298
+ "sort": "sorting",
299
+ "binary search": "search",
300
+ }
301
+ padded = f" {text} "
302
+ for needle, problem_type in keyword_map.items():
303
+ haystack = padded if needle.startswith(" ") and needle.endswith(" ") else text
304
+ if needle in haystack:
305
+ return problem_type
306
+ return "implementation"
307
+
308
+
309
+ def _copy_problem(problem: dict[str, Any]) -> dict[str, Any]:
310
+ copied = dict(problem)
311
+ copied["test_cases"] = [dict(test_case) for test_case in problem.get("test_cases", [])]
312
+ copied["visible_problem"] = dict(problem.get("visible_problem", {}))
313
+ examples = copied["visible_problem"].get("examples")
314
+ if isinstance(examples, list):
315
+ copied["visible_problem"]["examples"] = [dict(example) for example in examples]
316
+ return copied
env/generator.py CHANGED
@@ -109,17 +109,43 @@ def normalize_problem(problem_dict: dict[str, Any]) -> dict[str, Any]:
109
  normalized["problem"] = str(problem_dict.get("problem", "")).strip()
110
  normalized["input_format"] = str(problem_dict.get("input_format", "")).strip()
111
  normalized["constraints"] = str(problem_dict.get("constraints", "")).strip()
112
- normalized["test_cases"] = [dict(test_case) for test_case in problem_dict.get("test_cases", [])]
113
- normalized["visible_problem"] = dict(problem_dict.get("visible_problem", {}))
 
 
 
 
 
 
 
 
 
 
 
 
114
  return normalized
115
 
116
 
117
  class GeneratorAgent:
118
  """Deterministic, dependency-free generator for DSA-style problems."""
119
 
120
- def __init__(self, deterministic: bool = True) -> None:
 
 
 
 
 
121
  self.deterministic = deterministic
 
 
122
  self.templates = _build_templates()
 
 
 
 
 
 
 
123
 
124
  def generate_problem(
125
  self,
@@ -130,6 +156,30 @@ class GeneratorAgent:
130
  ) -> dict[str, Any]:
131
  history = history or {}
132
  target_tier = _difficulty_to_tier(difficulty_level)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
133
  rng = self._rng_for(target_tier, history, problem_id, family_weights or {})
134
  template = self._choose_template(
135
  target_tier,
@@ -250,6 +300,27 @@ class GeneratorAgent:
250
  def _tier_to_scalar(self, tier: int) -> float:
251
  return {1: 0.25, 2: 0.5, 3: 0.75}.get(tier, 0.5)
252
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
253
 
254
  def _difficulty_to_tier(difficulty_level: int | float | str) -> int:
255
  if isinstance(difficulty_level, str):
 
109
  normalized["problem"] = str(problem_dict.get("problem", "")).strip()
110
  normalized["input_format"] = str(problem_dict.get("input_format", "")).strip()
111
  normalized["constraints"] = str(problem_dict.get("constraints", "")).strip()
112
+ normalized_cases: list[dict[str, Any]] = []
113
+ for test_case in problem_dict.get("test_cases", []):
114
+ case = dict(test_case)
115
+ if "output" not in case and "expected_output" in case:
116
+ case["output"] = case.get("expected_output")
117
+ if "is_visible" not in case and "visible" in case:
118
+ case["is_visible"] = bool(case.get("visible"))
119
+ normalized_cases.append(case)
120
+ normalized["test_cases"] = normalized_cases
121
+ normalized_visible = dict(problem_dict.get("visible_problem", {}))
122
+ examples = normalized_visible.get("examples")
123
+ if isinstance(examples, list):
124
+ normalized_visible["examples"] = [dict(example) for example in examples]
125
+ normalized["visible_problem"] = normalized_visible
126
  return normalized
127
 
128
 
129
  class GeneratorAgent:
130
  """Deterministic, dependency-free generator for DSA-style problems."""
131
 
132
+ def __init__(
133
+ self,
134
+ deterministic: bool = True,
135
+ use_dataset: bool = False,
136
+ dataset_kwargs: dict[str, Any] | None = None,
137
+ ) -> None:
138
  self.deterministic = deterministic
139
+ self._use_dataset = use_dataset
140
+ self._dataset_kwargs = dict(dataset_kwargs or {})
141
  self.templates = _build_templates()
142
+ self._bank = None
143
+
144
+ def _ensure_bank(self) -> None:
145
+ if self._bank is None:
146
+ from env.dataset_loader import get_problem_bank
147
+
148
+ self._bank = get_problem_bank(**self._dataset_kwargs)
149
 
150
  def generate_problem(
151
  self,
 
156
  ) -> dict[str, Any]:
157
  history = history or {}
158
  target_tier = _difficulty_to_tier(difficulty_level)
159
+
160
+ if self._use_dataset:
161
+ self._ensure_bank()
162
+ if problem_id:
163
+ try:
164
+ problem = self._bank.get_by_id(problem_id)
165
+ except KeyError:
166
+ problem = None
167
+ if problem is not None:
168
+ normalized_problem = normalize_problem(problem)
169
+ if validate_problem(normalized_problem):
170
+ return normalized_problem
171
+
172
+ rng = self._rng_for(target_tier, history, problem_id, family_weights or {})
173
+ problem = self._bank.sample(
174
+ self._difficulty_to_label(self._tier_to_scalar(target_tier)),
175
+ rng,
176
+ list(history.get("problem_types", [])),
177
+ )
178
+ if problem is not None:
179
+ normalized_problem = normalize_problem(problem)
180
+ if validate_problem(normalized_problem):
181
+ return normalized_problem
182
+
183
  rng = self._rng_for(target_tier, history, problem_id, family_weights or {})
184
  template = self._choose_template(
185
  target_tier,
 
300
  def _tier_to_scalar(self, tier: int) -> float:
301
  return {1: 0.25, 2: 0.5, 3: 0.75}.get(tier, 0.5)
302
 
303
+ def _difficulty_to_label(self, difficulty: float) -> str:
304
+ if difficulty < 0.35:
305
+ return "easy"
306
+ if difficulty < 0.70:
307
+ return "medium"
308
+ return "hard"
309
+
310
+ def problem_types_for_difficulty(self, difficulty: str) -> list[str]:
311
+ if self._use_dataset:
312
+ self._ensure_bank()
313
+ return self._bank.problem_types_for_difficulty(difficulty)
314
+
315
+ normalized = str(difficulty).strip().lower()
316
+ return sorted(
317
+ {
318
+ template.problem_type
319
+ for template in self.templates
320
+ if DIFFICULTY_LABELS.get(template.difficulty_tier) == normalized
321
+ }
322
+ )
323
+
324
 
325
  def _difficulty_to_tier(difficulty_level: int | float | str) -> int:
326
  if isinstance(difficulty_level, str):
env/test_cases.py CHANGED
@@ -5,7 +5,13 @@ from typing import Any
5
  from env.generator import DIFFICULTY_LABELS, GeneratorAgent, VISIBLE_TEST_COUNT
6
 
7
 
8
- def load_problem_bank() -> list[dict[str, Any]]:
 
 
 
 
 
 
9
  generator = GeneratorAgent()
10
  bank = []
11
  for template in generator.templates:
 
5
  from env.generator import DIFFICULTY_LABELS, GeneratorAgent, VISIBLE_TEST_COUNT
6
 
7
 
8
+ def load_problem_bank(use_dataset: bool = False) -> list[dict[str, Any]]:
9
+ if use_dataset:
10
+ from env.dataset_loader import get_problem_bank
11
+
12
+ bank = get_problem_bank()
13
+ return [bank.get_by_id(problem_id) for problem_id in bank.all_problem_ids()[:200]]
14
+
15
  generator = GeneratorAgent()
16
  bank = []
17
  for template in generator.templates:
pyproject.toml CHANGED
@@ -16,6 +16,7 @@ dependencies = [
16
  "uvicorn>=0.24.0",
17
  "httpx>=0.28.0",
18
  "huggingface_hub>=0.34.0,<0.36",
 
19
  "transformers>=4.51.3,<5.7",
20
  "peft>=0.18.0,<0.20",
21
  ]
 
16
  "uvicorn>=0.24.0",
17
  "httpx>=0.28.0",
18
  "huggingface_hub>=0.34.0,<0.36",
19
+ "datasets>=2.18.0",
20
  "transformers>=4.51.3,<5.7",
21
  "peft>=0.18.0,<0.20",
22
  ]
requirements.txt CHANGED
@@ -4,5 +4,6 @@ fastapi>=0.104.0
4
  uvicorn>=0.24.0
5
  pydantic>=2.0.0
6
  huggingface_hub>=0.34.0,<0.36
 
7
  transformers>=4.51.3,<5.7
8
  peft>=0.18.0,<0.20
 
4
  uvicorn>=0.24.0
5
  pydantic>=2.0.0
6
  huggingface_hub>=0.34.0,<0.36
7
+ datasets>=2.18.0
8
  transformers>=4.51.3,<5.7
9
  peft>=0.18.0,<0.20
scripts/test_dataset_mode.py ADDED
@@ -0,0 +1,93 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ import sys
4
+ from pathlib import Path
5
+
6
+ ROOT = Path(__file__).resolve().parents[1]
7
+ if str(ROOT) not in sys.path:
8
+ sys.path.insert(0, str(ROOT))
9
+
10
+ from env import dataset_loader
11
+ from env.adapt_env import AdaptEnvironment
12
+ from env.generator import GeneratorAgent, validate_problem
13
+ from env.test_cases import load_problem_bank
14
+ from models import AdaptAction
15
+
16
+
17
+ class FakeBank:
18
+ def __init__(self, problem: dict) -> None:
19
+ self.problem = problem
20
+
21
+ def sample(self, difficulty: str, rng, recent_types: list[str]) -> dict:
22
+ del difficulty, rng, recent_types
23
+ return _copy_problem(self.problem)
24
+
25
+ def all_problem_ids(self) -> list[str]:
26
+ return [self.problem["problem_id"]]
27
+
28
+ def get_by_id(self, problem_id: str) -> dict:
29
+ if problem_id != self.problem["problem_id"]:
30
+ raise KeyError(problem_id)
31
+ return _copy_problem(self.problem)
32
+
33
+ def problem_types_for_difficulty(self, difficulty: str) -> list[str]:
34
+ del difficulty
35
+ return [self.problem["problem_type"]]
36
+
37
+
38
+ def _copy_problem(problem: dict) -> dict:
39
+ copied = dict(problem)
40
+ copied["test_cases"] = [dict(test_case) for test_case in problem.get("test_cases", [])]
41
+ copied["visible_problem"] = dict(problem.get("visible_problem", {}))
42
+ examples = copied["visible_problem"].get("examples")
43
+ if isinstance(examples, list):
44
+ copied["visible_problem"]["examples"] = [dict(example) for example in examples]
45
+ return copied
46
+
47
+
48
+ def main() -> None:
49
+ template_problem = GeneratorAgent().generate_problem(1, {}, problem_id="sum_even_numbers")
50
+ dataset_problem = _copy_problem(template_problem)
51
+ dataset_problem["problem_id"] = "cc_stub_sum_even_numbers"
52
+ dataset_problem["generation_mode"] = "dataset"
53
+ dataset_problem["validity_bonus"] = 1.0
54
+
55
+ fake_bank = FakeBank(dataset_problem)
56
+ original_bank = dataset_loader._BANK
57
+ original_config = dataset_loader._BANK_CONFIG
58
+ dataset_loader._BANK = fake_bank
59
+ dataset_loader._BANK_CONFIG = ("deepmind/code_contests", "train", 5000)
60
+
61
+ try:
62
+ loaded_bank = load_problem_bank(use_dataset=True)
63
+ assert loaded_bank
64
+ assert validate_problem(loaded_bank[0])
65
+
66
+ generated = GeneratorAgent(use_dataset=True).generate_problem("easy", {})
67
+ assert generated["problem_id"] == dataset_problem["problem_id"]
68
+ assert generated["generation_mode"] == "dataset"
69
+ assert validate_problem(generated)
70
+
71
+ env = AdaptEnvironment(use_dataset=True)
72
+ observation = env.reset(difficulty="easy")
73
+ assert env.problem["generation_mode"] == "dataset"
74
+ assert observation.problem_type == "sum_even_numbers"
75
+
76
+ result = env.step(
77
+ AdaptAction(
78
+ code=(
79
+ "n=int(input())\n"
80
+ "nums=list(map(int,input().split()))\n"
81
+ "print(sum(x for x in nums if x % 2 == 0))"
82
+ )
83
+ )
84
+ )
85
+ assert result.pass_rate == 1.0
86
+ print("Dataset mode smoke tests passed")
87
+ finally:
88
+ dataset_loader._BANK = original_bank
89
+ dataset_loader._BANK_CONFIG = original_config
90
+
91
+
92
+ if __name__ == "__main__":
93
+ main()
scripts/test_verifier.py CHANGED
@@ -18,6 +18,12 @@ test_cases = [
18
  {"input": "5\n8 8 8 8 8\n", "output": "40", "is_visible": False},
19
  ]
20
 
 
 
 
 
 
 
21
  correct_code = """
22
  n = int(input())
23
  nums = list(map(int, input().split()))
@@ -90,3 +96,19 @@ assert info_optimal["verifier_components"]["hidden_correctness"] == 1.0
90
  assert info_optimal["verifier_components"]["anti_cheat_compliance"] == 1.0
91
  assert reward_safety == 0.0
92
  assert info_safety["execution_status"] == "safety_violation"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
18
  {"input": "5\n8 8 8 8 8\n", "output": "40", "is_visible": False},
19
  ]
20
 
21
+ empirical_test_cases = [
22
+ {"input": "10\n", "output": "45", "is_visible": False},
23
+ {"input": "200\n", "output": "19900", "is_visible": False},
24
+ {"input": "1000\n", "output": "499500", "is_visible": False},
25
+ ]
26
+
27
  correct_code = """
28
  n = int(input())
29
  nums = list(map(int, input().split()))
 
96
  assert info_optimal["verifier_components"]["anti_cheat_compliance"] == 1.0
97
  assert reward_safety == 0.0
98
  assert info_safety["execution_status"] == "safety_violation"
99
+
100
+ linear_code = "n = int(input()); print(sum(range(n)))"
101
+ quadratic_code = "n = int(input()); print(sum(i*j for i in range(n) for j in range(n)))"
102
+ _, linear_result = verify(linear_code, empirical_test_cases)
103
+ _, quadratic_result = verify(quadratic_code, empirical_test_cases)
104
+ assert linear_result["efficiency_score"] >= quadratic_result["efficiency_score"], (
105
+ "Empirical complexity: O(n) should score >= O(n^2)"
106
+ )
107
+
108
+ small_alloc = "n = int(input()); print(n)"
109
+ large_alloc = "n = int(input()); x = [0] * (n * 10000); print(len(x))"
110
+ _, small_result = verify(small_alloc, empirical_test_cases)
111
+ _, large_result = verify(large_alloc, empirical_test_cases)
112
+ assert small_result["efficiency_score"] >= large_result["efficiency_score"], (
113
+ "Empirical complexity: small allocation should score >= large allocation"
114
+ )
server/app.py CHANGED
@@ -59,6 +59,9 @@ class TrainRequest(BaseModel):
59
  evaluation_episodes: Optional[int] = None
60
  baseline_eval: Optional[bool] = None
61
  generator_mode: Optional[str] = None
 
 
 
62
  disable_wandb: Optional[bool] = None
63
  save_merged_model: Optional[bool] = None
64
 
 
59
  evaluation_episodes: Optional[int] = None
60
  baseline_eval: Optional[bool] = None
61
  generator_mode: Optional[str] = None
62
+ use_dataset: bool = False
63
+ dataset_name: str = "deepmind/code_contests"
64
+ dataset_max_problems: int = 5000
65
  disable_wandb: Optional[bool] = None
66
  save_merged_model: Optional[bool] = None
67
 
test.py CHANGED
@@ -1,5 +1,6 @@
1
  from __future__ import annotations
2
 
 
3
  from scripts.test_env import main as run_env_smoke
4
  from scripts.test_space_api import main as run_space_api_smoke
5
  from scripts.test_training_config import main as run_training_config_smoke
@@ -9,6 +10,7 @@ from verifier.verifier import verify
9
 
10
 
11
  def main() -> None:
 
12
  run_env_smoke()
13
  run_space_api_smoke()
14
  run_training_config_smoke()
 
1
  from __future__ import annotations
2
 
3
+ from scripts.test_dataset_mode import main as run_dataset_mode_smoke
4
  from scripts.test_env import main as run_env_smoke
5
  from scripts.test_space_api import main as run_space_api_smoke
6
  from scripts.test_training_config import main as run_training_config_smoke
 
10
 
11
 
12
  def main() -> None:
13
+ run_dataset_mode_smoke()
14
  run_env_smoke()
15
  run_space_api_smoke()
16
  run_training_config_smoke()
training/train_grpo.py CHANGED
@@ -59,6 +59,9 @@ class TrainingConfig:
59
  wandb_run_name: str | None = None
60
  generator_mode: str = "reward_aware"
61
  non_deterministic_generator: bool = False
 
 
 
62
  trace_logging_enabled: bool = True
63
  checkpoint_log_interval_steps: int = 10
64
  save_merged_model: bool = False
@@ -207,6 +210,9 @@ def namespace_to_config(args: argparse.Namespace) -> TrainingConfig:
207
  wandb_run_name=args.wandb_run_name,
208
  generator_mode=args.generator_mode,
209
  non_deterministic_generator=args.non_deterministic_generator,
 
 
 
210
  trace_logging_enabled=args.trace_logging_enabled,
211
  checkpoint_log_interval_steps=args.checkpoint_log_interval_steps,
212
  save_merged_model=getattr(args, "save_merged_model", False),
@@ -258,6 +264,8 @@ class GeneratorController:
258
  mode: str = "heuristic"
259
  deterministic: bool = True
260
  temperature: float = 0.5
 
 
261
  generator: GeneratorAgent = field(init=False)
262
  history: dict[str, Any] = field(
263
  default_factory=lambda: {
@@ -272,10 +280,14 @@ class GeneratorController:
272
  family_productivity: dict[str, float] = field(default_factory=dict)
273
 
274
  def __post_init__(self) -> None:
275
- self.generator = GeneratorAgent(deterministic=self.deterministic)
 
 
 
 
276
  if not self.family_productivity:
277
  self.family_productivity = {
278
- template.problem_type: 0.0 for template in self.generator.templates
279
  }
280
 
281
  @property
@@ -306,11 +318,14 @@ class GeneratorController:
306
  if self.mode != "reward_aware":
307
  return None
308
 
309
- eligible = [
310
- template.problem_type
311
- for template in self.generator.templates
312
- if DIFFICULTY_LABELS[template.difficulty_tier] == difficulty
313
- ]
 
 
 
314
  if not eligible:
315
  return None
316
 
@@ -319,6 +334,14 @@ class GeneratorController:
319
  exp_values = [math.exp(logit - max_logit) for logit in logits]
320
  return {family: value for family, value in zip(eligible, exp_values)}
321
 
 
 
 
 
 
 
 
 
322
  def update(
323
  self,
324
  problem: dict[str, Any],
@@ -695,6 +718,8 @@ def run_policy_evaluation(
695
  tokenizer: Any,
696
  generator_mode: str,
697
  deterministic_generator: bool,
 
 
698
  episodes: int,
699
  logger: TrainingLogger,
700
  phase: str,
@@ -703,6 +728,8 @@ def run_policy_evaluation(
703
  controller = GeneratorController(
704
  mode=generator_mode,
705
  deterministic=deterministic_generator,
 
 
706
  )
707
  schedule = ["easy"] * (episodes // 3 + (1 if episodes % 3 > 0 else 0))
708
  schedule += ["medium"] * (episodes // 3 + (1 if episodes % 3 > 1 else 0))
@@ -1050,6 +1077,11 @@ def run_training(
1050
  controller = GeneratorController(
1051
  mode="reward_aware" if config.generator_mode == "reward_aware" else "heuristic",
1052
  deterministic=not config.non_deterministic_generator,
 
 
 
 
 
1053
  )
1054
  logger = TrainingLogger(
1055
  output_dir=output_dir,
@@ -1110,6 +1142,11 @@ def run_training(
1110
  tokenizer=tokenizer,
1111
  generator_mode=controller.mode,
1112
  deterministic_generator=not config.non_deterministic_generator,
 
 
 
 
 
1113
  episodes=config.evaluation_episodes,
1114
  logger=logger,
1115
  phase="baseline_eval",
@@ -1206,6 +1243,11 @@ def run_training(
1206
  tokenizer=tokenizer,
1207
  generator_mode=controller.mode,
1208
  deterministic_generator=not config.non_deterministic_generator,
 
 
 
 
 
1209
  episodes=config.evaluation_episodes,
1210
  logger=logger,
1211
  phase="trained_eval",
@@ -1290,6 +1332,9 @@ def build_parser() -> argparse.ArgumentParser:
1290
  action="store_true",
1291
  help="Disable deterministic fallback seeding for generator rollouts.",
1292
  )
 
 
 
1293
  return parser
1294
 
1295
 
 
59
  wandb_run_name: str | None = None
60
  generator_mode: str = "reward_aware"
61
  non_deterministic_generator: bool = False
62
+ use_dataset: bool = False
63
+ dataset_name: str = "deepmind/code_contests"
64
+ dataset_max_problems: int = 5000
65
  trace_logging_enabled: bool = True
66
  checkpoint_log_interval_steps: int = 10
67
  save_merged_model: bool = False
 
210
  wandb_run_name=args.wandb_run_name,
211
  generator_mode=args.generator_mode,
212
  non_deterministic_generator=args.non_deterministic_generator,
213
+ use_dataset=args.use_dataset,
214
+ dataset_name=args.dataset_name,
215
+ dataset_max_problems=args.dataset_max_problems,
216
  trace_logging_enabled=args.trace_logging_enabled,
217
  checkpoint_log_interval_steps=args.checkpoint_log_interval_steps,
218
  save_merged_model=getattr(args, "save_merged_model", False),
 
264
  mode: str = "heuristic"
265
  deterministic: bool = True
266
  temperature: float = 0.5
267
+ use_dataset: bool = False
268
+ dataset_kwargs: dict[str, Any] = field(default_factory=dict)
269
  generator: GeneratorAgent = field(init=False)
270
  history: dict[str, Any] = field(
271
  default_factory=lambda: {
 
280
  family_productivity: dict[str, float] = field(default_factory=dict)
281
 
282
  def __post_init__(self) -> None:
283
+ self.generator = GeneratorAgent(
284
+ deterministic=self.deterministic,
285
+ use_dataset=self.use_dataset,
286
+ dataset_kwargs=self.dataset_kwargs,
287
+ )
288
  if not self.family_productivity:
289
  self.family_productivity = {
290
+ family: 0.0 for family in self._known_problem_families()
291
  }
292
 
293
  @property
 
318
  if self.mode != "reward_aware":
319
  return None
320
 
321
+ if self.use_dataset:
322
+ eligible = self.generator.problem_types_for_difficulty(difficulty)
323
+ else:
324
+ eligible = [
325
+ template.problem_type
326
+ for template in self.generator.templates
327
+ if DIFFICULTY_LABELS[template.difficulty_tier] == difficulty
328
+ ]
329
  if not eligible:
330
  return None
331
 
 
334
  exp_values = [math.exp(logit - max_logit) for logit in logits]
335
  return {family: value for family, value in zip(eligible, exp_values)}
336
 
337
+ def _known_problem_families(self) -> list[str]:
338
+ if self.use_dataset:
339
+ families: set[str] = set()
340
+ for difficulty in ("easy", "medium", "hard"):
341
+ families.update(self.generator.problem_types_for_difficulty(difficulty))
342
+ return sorted(families)
343
+ return sorted({template.problem_type for template in self.generator.templates})
344
+
345
  def update(
346
  self,
347
  problem: dict[str, Any],
 
718
  tokenizer: Any,
719
  generator_mode: str,
720
  deterministic_generator: bool,
721
+ use_dataset: bool,
722
+ dataset_kwargs: dict[str, Any],
723
  episodes: int,
724
  logger: TrainingLogger,
725
  phase: str,
 
728
  controller = GeneratorController(
729
  mode=generator_mode,
730
  deterministic=deterministic_generator,
731
+ use_dataset=use_dataset,
732
+ dataset_kwargs=dataset_kwargs,
733
  )
734
  schedule = ["easy"] * (episodes // 3 + (1 if episodes % 3 > 0 else 0))
735
  schedule += ["medium"] * (episodes // 3 + (1 if episodes % 3 > 1 else 0))
 
1077
  controller = GeneratorController(
1078
  mode="reward_aware" if config.generator_mode == "reward_aware" else "heuristic",
1079
  deterministic=not config.non_deterministic_generator,
1080
+ use_dataset=config.use_dataset,
1081
+ dataset_kwargs={
1082
+ "dataset_name": config.dataset_name,
1083
+ "max_problems": config.dataset_max_problems,
1084
+ },
1085
  )
1086
  logger = TrainingLogger(
1087
  output_dir=output_dir,
 
1142
  tokenizer=tokenizer,
1143
  generator_mode=controller.mode,
1144
  deterministic_generator=not config.non_deterministic_generator,
1145
+ use_dataset=config.use_dataset,
1146
+ dataset_kwargs={
1147
+ "dataset_name": config.dataset_name,
1148
+ "max_problems": config.dataset_max_problems,
1149
+ },
1150
  episodes=config.evaluation_episodes,
1151
  logger=logger,
1152
  phase="baseline_eval",
 
1243
  tokenizer=tokenizer,
1244
  generator_mode=controller.mode,
1245
  deterministic_generator=not config.non_deterministic_generator,
1246
+ use_dataset=config.use_dataset,
1247
+ dataset_kwargs={
1248
+ "dataset_name": config.dataset_name,
1249
+ "max_problems": config.dataset_max_problems,
1250
+ },
1251
  episodes=config.evaluation_episodes,
1252
  logger=logger,
1253
  phase="trained_eval",
 
1332
  action="store_true",
1333
  help="Disable deterministic fallback seeding for generator rollouts.",
1334
  )
1335
+ parser.add_argument("--use-dataset", action="store_true")
1336
+ parser.add_argument("--dataset-name", default="deepmind/code_contests")
1337
+ parser.add_argument("--dataset-max-problems", type=int, default=5000)
1338
  return parser
1339
 
1340
 
verifier/complexity.py CHANGED
@@ -1,9 +1,17 @@
1
  from __future__ import annotations
2
 
3
  import ast
 
 
 
4
  from dataclasses import dataclass
5
  from typing import Any
6
 
 
 
 
 
 
7
 
8
  @dataclass
9
  class ComplexitySignals:
@@ -69,7 +77,109 @@ def _call_name(node: ast.AST) -> str:
69
  return ""
70
 
71
 
72
- def analyze_code_complexity(code: str) -> dict[str, Any]:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
73
  try:
74
  tree = ast.parse(code)
75
  except SyntaxError:
@@ -78,7 +188,7 @@ def analyze_code_complexity(code: str) -> dict[str, Any]:
78
  "space_complexity_score": 0.0,
79
  "efficiency_score": 0.0,
80
  "optimization_hints": [],
81
- "complexity_signals": {},
82
  }
83
 
84
  visitor = ComplexityVisitor()
@@ -126,5 +236,70 @@ def analyze_code_complexity(code: str) -> dict[str, Any]:
126
  "generator_expressions": signals.generator_expressions,
127
  "sorting_calls": signals.sorting_calls,
128
  "materialized_builtin_inputs": signals.materialized_builtin_inputs,
 
129
  },
130
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  from __future__ import annotations
2
 
3
  import ast
4
+ import math
5
+ import os
6
+ import re
7
  from dataclasses import dataclass
8
  from typing import Any
9
 
10
+ from env.executor import run_code as execute_submission
11
+
12
+ PROBE_TIMEOUT_SECONDS = 2.0
13
+ METRICS_PATTERN = re.compile(r"ADAPT_METRICS:\s*time_ms=([0-9.]+)\s+peak_kb=([0-9.]+)")
14
+
15
 
16
  @dataclass
17
  class ComplexitySignals:
 
77
  return ""
78
 
79
 
80
+ def _probe_timeout() -> float:
81
+ raw_value = os.getenv("ADAPT_PROBE_TIMEOUT", str(PROBE_TIMEOUT_SECONDS))
82
+ try:
83
+ timeout = float(raw_value)
84
+ except ValueError:
85
+ return PROBE_TIMEOUT_SECONDS
86
+ return timeout if timeout > 0 else PROBE_TIMEOUT_SECONDS
87
+
88
+
89
+ def _build_measurement_harness(code: str) -> str:
90
+ return f"""
91
+ import sys as _adapt_sys
92
+ import time as _adapt_time
93
+ import tracemalloc as _adapt_tracemalloc
94
+
95
+ _adapt_globals = {{"__name__": "__main__"}}
96
+ _adapt_source = {code!r}
97
+ _adapt_tracemalloc.start()
98
+ _adapt_t0 = _adapt_time.perf_counter()
99
+ exec(compile(_adapt_source, "<submission>", "exec"), _adapt_globals, _adapt_globals)
100
+ _adapt_t1 = _adapt_time.perf_counter()
101
+ _adapt_peak_kb = _adapt_tracemalloc.get_traced_memory()[1] / 1024
102
+ _adapt_tracemalloc.stop()
103
+ print(
104
+ f"ADAPT_METRICS: time_ms={{(_adapt_t1 - _adapt_t0) * 1000:.3f}} peak_kb={{_adapt_peak_kb:.1f}}",
105
+ file=_adapt_sys.stderr,
106
+ )
107
+ """
108
+
109
+
110
+ def _parse_harness_output(stderr: str) -> tuple[float, float]:
111
+ match = METRICS_PATTERN.search(str(stderr or ""))
112
+ if match is None:
113
+ return 0.0, 0.0
114
+ try:
115
+ return float(match.group(1)), float(match.group(2))
116
+ except ValueError:
117
+ return 0.0, 0.0
118
+
119
+
120
+ def _fit_scaling_exponent(sizes: list[float], values: list[float]) -> float:
121
+ if len(sizes) < 2 or len(values) < 2:
122
+ return 1.0
123
+ log_n = [math.log(max(size, 1.0)) for size in sizes]
124
+ log_v = [math.log(max(value, 1e-6)) for value in values]
125
+ count = len(log_n)
126
+ mean_n = sum(log_n) / count
127
+ mean_v = sum(log_v) / count
128
+ numerator = sum((log_n[index] - mean_n) * (log_v[index] - mean_v) for index in range(count))
129
+ denominator = sum((log_n[index] - mean_n) ** 2 for index in range(count))
130
+ return numerator / denominator if denominator > 1e-9 else 1.0
131
+
132
+
133
+ def _exponent_to_score(alpha: float) -> float:
134
+ if alpha < 0.1:
135
+ return 1.0
136
+ if alpha < 1.2:
137
+ return 0.85
138
+ if alpha < 1.6:
139
+ return 0.75
140
+ if alpha < 2.3:
141
+ return 0.50
142
+ if alpha < 3.2:
143
+ return 0.20
144
+ return 0.0
145
+
146
+
147
+ def _memory_to_score(peak_kb: float) -> float:
148
+ mb = peak_kb / 1024.0
149
+ if mb < 1:
150
+ return 1.0
151
+ if mb < 10:
152
+ return 0.85
153
+ if mb < 50:
154
+ return 0.65
155
+ if mb < 256:
156
+ return 0.40
157
+ return 0.10
158
+
159
+
160
+ def _hints_from_scores(time_score: float, space_score: float, time_alpha: float) -> list[str]:
161
+ hints: list[str] = []
162
+ if time_alpha >= 2.3:
163
+ hints.append("Reduce quadratic-or-worse work; measured runtime growth looks steep across larger inputs.")
164
+ elif time_score < 0.85:
165
+ hints.append("Consider a more scalable algorithm so runtime grows more gently with input size.")
166
+ if space_score < 0.85:
167
+ hints.append("Reduce peak memory usage by avoiding large intermediate containers when possible.")
168
+ return hints
169
+
170
+
171
+ def _merge_hints(*hint_groups: list[str]) -> list[str]:
172
+ merged: list[str] = []
173
+ seen: set[str] = set()
174
+ for group in hint_groups:
175
+ for hint in group:
176
+ if hint and hint not in seen:
177
+ seen.add(hint)
178
+ merged.append(hint)
179
+ return merged
180
+
181
+
182
+ def _heuristic_fallback(code: str) -> dict[str, Any]:
183
  try:
184
  tree = ast.parse(code)
185
  except SyntaxError:
 
188
  "space_complexity_score": 0.0,
189
  "efficiency_score": 0.0,
190
  "optimization_hints": [],
191
+ "complexity_signals": {"measurement_source": "heuristic"},
192
  }
193
 
194
  visitor = ComplexityVisitor()
 
236
  "generator_expressions": signals.generator_expressions,
237
  "sorting_calls": signals.sorting_calls,
238
  "materialized_builtin_inputs": signals.materialized_builtin_inputs,
239
+ "measurement_source": "heuristic",
240
  },
241
  }
242
+
243
+
244
+ def _empirical_complexity(code: str, probe_inputs: list[str]) -> dict[str, Any]:
245
+ heuristic = _heuristic_fallback(code)
246
+ if len(probe_inputs) < 3:
247
+ return heuristic
248
+
249
+ harness = _build_measurement_harness(code)
250
+ sizes: list[float] = []
251
+ times: list[float] = []
252
+ mem_peaks: list[float] = []
253
+ for probe_input in probe_inputs:
254
+ result = execute_submission(harness, probe_input, timeout_seconds=_probe_timeout())
255
+ if bool(result.get("timed_out")) or int(result.get("exit_code", 0)) != 0:
256
+ return heuristic
257
+ wall_ms, peak_kb = _parse_harness_output(str(result.get("stderr", "")))
258
+ if wall_ms <= 0.0 and peak_kb <= 0.0:
259
+ return heuristic
260
+ sizes.append(float(len(probe_input)))
261
+ times.append(float(wall_ms))
262
+ mem_peaks.append(float(peak_kb))
263
+
264
+ if len(sizes) < 3:
265
+ return heuristic
266
+
267
+ empirical_time_score = _exponent_to_score(_fit_scaling_exponent(sizes, times))
268
+ empirical_space_score = _memory_to_score(max(mem_peaks))
269
+ time_alpha = _fit_scaling_exponent(sizes, times)
270
+ space_alpha = _fit_scaling_exponent(sizes, mem_peaks)
271
+
272
+ time_score = min(empirical_time_score, float(heuristic["time_complexity_score"]))
273
+ space_score = min(empirical_space_score, float(heuristic["space_complexity_score"]))
274
+ efficiency_score = round(min(0.7 * time_score + 0.3 * space_score, float(heuristic["efficiency_score"])), 4)
275
+ optimization_hints = _merge_hints(
276
+ _hints_from_scores(time_score, space_score, time_alpha),
277
+ list(heuristic.get("optimization_hints", [])),
278
+ )
279
+
280
+ complexity_signals = dict(heuristic.get("complexity_signals", {}))
281
+ complexity_signals.update(
282
+ {
283
+ "time_exponent": round(time_alpha, 4),
284
+ "space_exponent": round(space_alpha, 4),
285
+ "peak_memory_kb": round(max(mem_peaks), 1),
286
+ "measurement_source": "empirical",
287
+ }
288
+ )
289
+
290
+ return {
291
+ "time_complexity_score": round(time_score, 4),
292
+ "space_complexity_score": round(space_score, 4),
293
+ "efficiency_score": efficiency_score,
294
+ "optimization_hints": optimization_hints,
295
+ "complexity_signals": complexity_signals,
296
+ }
297
+
298
+
299
+ def analyze_code_complexity(code: str, probe_inputs: list[str] | None = None) -> dict[str, Any]:
300
+ if probe_inputs and len(probe_inputs) >= 3:
301
+ try:
302
+ return _empirical_complexity(code, probe_inputs)
303
+ except Exception:
304
+ pass
305
+ return _heuristic_fallback(code)
verifier/verifier.py CHANGED
@@ -14,9 +14,9 @@ def verify(
14
  step_number: int = 1,
15
  ) -> tuple[float, dict[str, Any]]:
16
  precheck = validate_code(code)
17
- complexity = analyze_code_complexity(code)
18
 
19
  if not precheck["syntax_ok"] or not precheck["safety_ok"]:
 
20
  reward, metrics = compute_pass_rate(
21
  [],
22
  step_number=step_number,
@@ -33,6 +33,9 @@ def verify(
33
  "error": str(precheck["error"]),
34
  }
35
 
 
 
 
36
  results: list[dict[str, Any]] = []
37
  for index, test_case in enumerate(test_cases):
38
  if isinstance(test_case, dict):
@@ -91,6 +94,24 @@ def verify(
91
  }
92
 
93
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
94
  def _build_feedback(metrics: dict[str, Any], *, error: str = "") -> str:
95
  execution_status = str(metrics.get("execution_status", "unknown"))
96
  if execution_status == "syntax_error":
 
14
  step_number: int = 1,
15
  ) -> tuple[float, dict[str, Any]]:
16
  precheck = validate_code(code)
 
17
 
18
  if not precheck["syntax_ok"] or not precheck["safety_ok"]:
19
+ complexity = analyze_code_complexity(code)
20
  reward, metrics = compute_pass_rate(
21
  [],
22
  step_number=step_number,
 
33
  "error": str(precheck["error"]),
34
  }
35
 
36
+ probe_inputs = _build_probe_inputs(test_cases)
37
+ complexity = analyze_code_complexity(code, probe_inputs=probe_inputs)
38
+
39
  results: list[dict[str, Any]] = []
40
  for index, test_case in enumerate(test_cases):
41
  if isinstance(test_case, dict):
 
94
  }
95
 
96
 
97
+ def _build_probe_inputs(test_cases: list[dict[str, Any]] | list[tuple[str, str]]) -> list[str]:
98
+ inputs: list[str] = []
99
+ for test_case in test_cases:
100
+ if isinstance(test_case, dict):
101
+ raw_input = test_case.get("input")
102
+ elif test_case:
103
+ raw_input = test_case[0]
104
+ else:
105
+ raw_input = None
106
+ if isinstance(raw_input, str):
107
+ inputs.append(raw_input)
108
+
109
+ inputs_sorted = sorted(set(inputs), key=len)
110
+ if len({len(item) for item in inputs_sorted}) < 3:
111
+ return []
112
+ return inputs_sorted
113
+
114
+
115
  def _build_feedback(metrics: dict[str, Any], *, error: str = "") -> str:
116
  execution_status = str(metrics.get("execution_status", "unknown"))
117
  if execution_status == "syntax_error":