pushpam14 commited on
Commit
912886b
·
verified ·
1 Parent(s): c254d67

Sync results, plots, scores, story doc, BLOG, notebook outputs note

Browse files
.gitattributes CHANGED
@@ -33,3 +33,5 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
33
  *.zip filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
 
 
 
33
  *.zip filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
36
+ results/before_after.png filter=lfs diff=lfs merge=lfs -text
37
+ results/reward_curve.png filter=lfs diff=lfs merge=lfs -text
BLOG.md CHANGED
@@ -46,6 +46,18 @@ GitHub README: https://github.com/kumarpushpam17-personal/Hackathon/blob/main/ap
46
 
47
  The end result is a training environment where an LLM agent interacts with a simulated enterprise API ecosystem and learns to reason about contract changes, downstream consumers, and backward-compatible fixes.
48
 
 
 
 
 
 
 
 
 
 
 
 
 
49
  ## What makes this submission strong
50
 
51
  This is not a static prompt benchmark. It is a runnable OpenEnv environment with hidden ground truth, stateful episodes, objective rewards, real training, and public proof artifacts.
 
46
 
47
  The end result is a training environment where an LLM agent interacts with a simulated enterprise API ecosystem and learns to reason about contract changes, downstream consumers, and backward-compatible fixes.
48
 
49
+ ## Reviewer quick path
50
+
51
+ If you only have a few minutes:
52
+
53
+ 1. Open the live environment: https://huggingface.co/spaces/pushpam14/api-contract-validator
54
+ 2. Read the headline result in [Section 5](#5-results): `detect_breaking_changes` improves from **0.01 → 0.67** after GRPO.
55
+ 3. Inspect the two committed plots: reward curve and three-way before/after comparison.
56
+ 4. Verify training proof: https://github.com/kumarpushpam17-personal/Hackathon/blob/main/api_contract_validator/results/TRAINING_RUN_PROOF.md
57
+ 5. Reproduce one live episode with the `curl` command in [Section 8](#8-try-it-yourself).
58
+
59
+ For full setup, action schema, reward tables, and local run instructions, use the GitHub README: https://github.com/kumarpushpam17-personal/Hackathon/blob/main/api_contract_validator/README.md
60
+
61
  ## What makes this submission strong
62
 
63
  This is not a static prompt benchmark. It is a runnable OpenEnv environment with hidden ground truth, stateful episodes, objective rewards, real training, and public proof artifacts.
ENTERPRISE_CONTRACT_GUARDIAN_STORY.md CHANGED
@@ -156,7 +156,180 @@ flowchart TD
156
 
157
  This matters because RL needs signal. A binary "pass/fail" reward would make learning slow and brittle. This environment gives a useful reward even when the agent is partially right.
158
 
159
- ## 7. Real-Time Example 1: UserService Email Rename
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
160
 
161
  ### Incident
162
 
@@ -236,7 +409,7 @@ Then it proposes:
236
 
237
  The fix lets old consumers keep reading `email`. New consumers can adopt `email_address`. The company gets a migration window instead of a Monday outage.
238
 
239
- ## 8. Real-Time Example 2: OrdersService Status Enum Narrowing
240
 
241
  ### Incident
242
 
@@ -306,7 +479,7 @@ The fix is different from the email rename case. A field alias does not restore
306
 
307
  The environment rewards the agent for recognizing that the safe consumer should not be touched. That is the difference between real blast-radius analysis and noisy "warn everybody" automation.
308
 
309
- ## 9. The Full Episode Lifecycle
310
 
311
  ```mermaid
312
  sequenceDiagram
@@ -330,7 +503,7 @@ sequenceDiagram
330
 
331
  The key technical principle is determinism. The grader knows the planted violations and expected affected consumers. That makes the reward objective, repeatable, and suitable for training.
332
 
333
- ## 10. Why Training Can Improve The Agent
334
 
335
  The baseline model already understands many simple schema issues, but it struggles where exact action formatting and enterprise reasoning matter.
336
 
@@ -346,31 +519,30 @@ GRPO training can improve behavior because every sampled completion receives dir
346
  - Correct consumer list gets more reward than over-warning every service.
347
  - Fixes that preserve every consumer get more reward than fixes that only look plausible.
348
 
349
- ## 11. What Makes This Submission Strong
350
 
351
  The project has a clear judge-facing story:
352
 
353
  - Problem: API changes break downstream services because teams lack automated impact reasoning.
354
  - Environment: OpenEnv simulation with specs, payloads, service graphs, and deterministic grading.
355
- - Results: baseline scores exist, and trained reward plots should be added after the GRPO run.
356
  - Why it matters: platform teams, API gateway teams, CI/CD pipelines, and microservice organizations need this.
357
 
358
  The novelty is not "schema validation." The novelty is turning multi-service contract impact analysis into a trainable RL environment.
359
 
360
- ## 12. What Still Must Be Added Before Final Submission
361
-
362
- The environment and tests are in good shape, but the final submission needs visible training evidence:
363
-
364
- - Commit `results/reward_curve.png`.
365
- - Commit `results/before_after.png`.
366
- - Generate and commit `trained_scores.json`.
367
- - Replace README trained-score placeholders.
368
- - Add the public WandB run link, if WandB is used.
369
- - Add the YouTube demo video or HuggingFace mini-blog link.
370
 
371
- Without those artifacts, the project is strong on innovation and story, but weaker on the 20 percent "Improvement in Rewards" judging criterion.
372
 
373
- ## 13. Two-Minute Pitch Version
 
 
 
 
 
 
 
374
 
375
- "Enterprise API breaks rarely happen because one schema is invalid. They happen because a small producer change silently breaks downstream consumers. Our environment trains an agent to handle the full platform-engineering workflow: detect the contract violation, trace the blast radius across services, propose a backward-compatible migration, and validate that migration against every consumer contract. The reward is composable: correct violations, correct consumers, missed consumers, false flags, malformed fixes, and cross-consumer compatibility are all scored independently. This teaches a model a real enterprise skill that standard LLMs do not reliably perform today."
376
 
 
 
156
 
157
  This matters because RL needs signal. A binary "pass/fail" reward would make learning slow and brittle. This environment gives a useful reward even when the agent is partially right.
158
 
159
+ ## 7. Implemented Feature Coverage
160
+
161
+ This is the full environment surface implemented in the submission.
162
+
163
+ | Area | Implemented capability | Why it matters |
164
+ |---|---|---|
165
+ | OpenEnv environment | Standard `reset`, `step`, and `state` lifecycle | Judges can run the environment like any OpenEnv-compatible benchmark |
166
+ | Single LLM agent | One policy interacts with a multi-service enterprise world | Correct fit for Theme 3.1, not a multi-agent theme |
167
+ | Phase 1 detection | Agent finds payload/schema violations one at a time | Builds a curriculum before harder enterprise tasks |
168
+ | Phase 2 impact tracing | Agent identifies affected downstream services | Tests service-dependency reasoning, not just schema matching |
169
+ | Phase 3 fix and verify | Agent proposes a backward-compatible migration | Tests whether the agent can solve the incident, not only diagnose it |
170
+ | Cascade workflow | Agent moves from tracing into fix proposal in one episode | Demonstrates multi-step orchestration and state tracking |
171
+ | Seeded scenarios | Reset accepts deterministic seeds | Supports both reproducible evaluation and varied training episodes |
172
+ | Composable rewards | Reward components are separated by behavior | Gives richer learning signal and makes reward hacking easier to inspect |
173
+ | Baseline inference | `inference.py` runs all tasks with an OpenAI-compatible client | Produces before/after metrics for judging |
174
+ | GRPO training | `training/train.py` connects to the live environment reward | The model learns from environment experience instead of a static dataset |
175
+ | HF Space deployment | Docker/OpenEnv app runs on Hugging Face Spaces | Judges can pull and evaluate the environment from the submitted URL |
176
+
177
+ ## 8. Complete Task Matrix
178
+
179
+ | Task | Phase | What the agent must do | Max steps |
180
+ |---|---:|---|---:|
181
+ | `find_type_mismatches` | 1 | Find type mismatches, missing fields, enum errors, and format errors in a simple request | 10 |
182
+ | `validate_nested_objects` | 1 | Validate nested objects and arrays using dot paths and bracket paths | 15 |
183
+ | `detect_breaking_changes` | 1 | Compare v1/v2 API specs and find breaking changes | 20 |
184
+ | `validate_response_schema` | 1 | Validate response formats, patterns, ranges, enums, and subtle schema constraints | 25 |
185
+ | `validate_cross_field_constraints` | 1 | Check arithmetic, date ordering, item counts, and conditional business rules | 18 |
186
+ | `validate_auth_request` | 1 | Validate OAuth/API-key payload constraints, scopes, patterns, and limits | 14 |
187
+ | `trace_downstream_blast_radius` | 2 | Identify every consumer broken by a producer API change | 20 |
188
+ | `propose_backward_compat_fix` | 3 | Propose a migration strategy and patch that keeps consumers working | 25 |
189
+ | `multi_service_cascade_fix` | 2 + 3 | Trace the blast radius, then propose a fix in one episode | 40 |
190
+
191
+ ## 9. Complete Action Space
192
+
193
+ The environment exposes one typed action model with phase-specific fields.
194
+
195
+ | Action | Used in | Required fields | Purpose |
196
+ |---|---|---|---|
197
+ | `report_violation` | Phase 1 | `field_path`, `violation_type`, `description`, optional `suggested_fix` | Report one schema or contract violation |
198
+ | `trace_impact` | Phase 2 | `affected_services`, `reasoning` | List downstream services impacted by the breaking change |
199
+ | `propose_fix` | Phase 3 | `fix_strategy`, `spec_patch`, `rationale` | Submit a backward-compatible migration proposal |
200
+ | `validate_fix` | Phase 3 | `fix_strategy`, `spec_patch`, `rationale` | Same validation path as fix proposal; useful for explicit verify-style actions |
201
+ | `DONE` | Phase 1 special signal | `field_path="DONE"` | End the episode and collect completeness bonus |
202
+ | `HINT` | Phase 1 special signal | `field_path="HINT"` | Ask for a location clue at a reward cost |
203
+
204
+ Allowed fix strategies:
205
+
206
+ | Strategy | Expected patch shape | Best fit |
207
+ |---|---|---|
208
+ | `field_alias` | `{"aliases": {"old_field": "new_field"}}` | Field rename where old consumers need the old field name |
209
+ | `version_bump` | `{"versions": ["v1.0", "v2.0"]}` | Breaking change isolated behind a new version |
210
+ | `deprecation_window` | `{"deprecated_fields": [...]}` or `{"deprecated_enum_values": [...]}` | Keep old contract temporarily while warning consumers |
211
+ | `dual_write` | `{"emit_fields": ["old", "new"]}` | Emit old and new names during migration |
212
+ | `consumer_patch` | `{"consumers_to_migrate": [...]}` | Cases where producer cannot preserve old behavior cleanly, such as enum narrowing |
213
+
214
+ ## 10. Complete Reward Criteria
215
+
216
+ The reward is designed as independent signals instead of one monolithic score.
217
+
218
+ ### Phase 1: Detection Rewards
219
+
220
+ | Event | Reward | What it teaches |
221
+ |---|---:|---|
222
+ | Correct path and correct violation type | `+1.0` | Report exact contract issues |
223
+ | Correct path but wrong type | `+0.3` | Finding the right location is partial progress |
224
+ | Duplicate report | `-0.1` | Track what was already found |
225
+ | False positive | `-0.3` | Do not guess or over-report |
226
+ | `HINT` requested | `-0.5` | Hints are allowed but expensive |
227
+ | `DONE` submitted | `+0.5 * completeness` | Finish only after enough violations are found |
228
+ | Final Phase 1 score | `correct / total`, clamped to `0.01..0.99` | Normalized episode metric |
229
+
230
+ ### Phase 2: Impact-Tracing Rewards
231
+
232
+ | Event | Reward | What it teaches |
233
+ |---|---:|---|
234
+ | Correctly flagged affected consumer | `+0.8` each | Maximize recall for true blast radius |
235
+ | Missed affected consumer | `-0.5` each | Do not under-report broken teams |
236
+ | False-flagged unaffected consumer | `-0.4` each | Do not warn unrelated teams |
237
+ | Unknown service name | `-0.2` each | Use real service names from the graph |
238
+ | Final Phase 2 score | F1 score, clamped to `0.01..0.99` | Balance precision and recall |
239
+
240
+ ### Phase 3: Fix-Validation Rewards
241
+
242
+ | Event | Reward | What it teaches |
243
+ |---|---:|---|
244
+ | Fix validates against every consumer | `+2.0` | The migration must work end to end |
245
+ | Fix breaks a consumer | `-1.0` per failing consumer | Do not optimize for only one downstream team |
246
+ | Malformed spec patch | `-0.5` | Use the expected patch schema |
247
+ | Strategy unacceptable for scenario | `-0.3` | Pick a migration strategy that fits the failure mode |
248
+ | Final Phase 3 score | Passing consumers / total consumers, clamped to `0.01..0.99` | Reward partial compatibility while still favoring complete fixes |
249
+
250
+ ### Anti-Hacking And Format Hardening
251
+
252
+ The reward module also includes cross-cutting hardening components:
253
+
254
+ | Signal | Reward | Purpose |
255
+ |---|---:|---|
256
+ | Malformed action component | `-0.2` | Penalize action JSON that does not match the expected schema |
257
+ | Spam penalty component | `-1.0` if reports exceed `3x` planted violations | Discourage "report everything" reward hacking |
258
+ | Hard step budget | Episode ends at task-specific max steps | Prevents endless probing |
259
+
260
+ These hardening signals are documented because they are part of the reward design and show that the environment was built with reward-hacking resistance in mind. The most visible runtime penalties in the current demo path are malformed fix patches, invalid phase actions, false positives, false flags, duplicates, hints, and missed consumers.
261
+
262
+ ## 11. State And Memory Inside The Environment
263
+
264
+ The agent is a single LLM policy, but the environment maintains episode state across turns.
265
+
266
+ | State field | Meaning |
267
+ |---|---|
268
+ | `task_name` | Current task being evaluated |
269
+ | `phase` | Current workflow stage: detection, tracing, or fix proposal |
270
+ | `step_count` | Number of actions taken in the episode |
271
+ | `total_violations` | Number of planted Phase 1 violations |
272
+ | `correct_reports` | Number of correctly found Phase 1 violations |
273
+ | `false_positives` | Number of incorrect Phase 1 reports |
274
+ | `duplicate_reports` | Number of repeated reports |
275
+ | `total_consumers` | Number of downstream consumers in the enterprise graph |
276
+ | `consumers_correctly_traced` | Number of impacted services correctly identified |
277
+ | `consumers_missed` | Number of impacted services missed |
278
+ | `consumers_false_flagged` | Number of safe services incorrectly flagged |
279
+ | `fix_attempts` | Number of submitted fix attempts |
280
+ | `fix_validated` | Whether the current fix passes every consumer |
281
+ | `fix_breaks_consumers` | Number of consumers still failing |
282
+ | `score` | Normalized final episode score |
283
+
284
+ This is the "memory" in the environment: each observation reflects what has happened so far, and each next action is graded against that evolving state.
285
+
286
+ ## 12. Training Loop: How Experience Becomes Learning
287
+
288
+ ```mermaid
289
+ flowchart LR
290
+ A[Seeded reset] --> B[Observation]
291
+ B --> C[Prompt to LLM policy]
292
+ C --> D[Sample completions]
293
+ D --> E[Parse structured action]
294
+ E --> F[Environment step]
295
+ F --> G[Reward from deterministic grader]
296
+ G --> H[GRPO ranks completions]
297
+ H --> I[Update LoRA policy]
298
+ I --> C
299
+ ```
300
+
301
+ Training uses the live environment as the reward function:
302
+
303
+ 1. Build prompts from seeded environment resets.
304
+ 2. Ask the model to produce structured JSON actions.
305
+ 3. Parse the action into the OpenEnv action model.
306
+ 4. Reset the environment to the matching task and seed before scoring.
307
+ 5. Step the environment and collect the reward.
308
+ 6. Use GRPO to reinforce completions that earn higher reward.
309
+ 7. Save `reward_curve.png` and training state for the README.
310
+
311
+ This is important for judging: the training loop is not static supervised fine-tuning. It connects to the environment, receives real rewards, and updates the agent based on experience.
312
+
313
+ ## 13. Deployment And Compliance
314
+
315
+ The submission includes the pieces judges expect for an OpenEnv environment:
316
+
317
+ | Requirement | Where it is handled |
318
+ |---|---|
319
+ | OpenEnv-compatible app | `server/environment.py`, `server/app.py`, `openenv.yaml` |
320
+ | Hosted environment | Hugging Face Space: `pushpam14/api-contract-validator` |
321
+ | Docker runtime | `Dockerfile` with FastAPI server on port `7860` |
322
+ | Health endpoint | `/health` |
323
+ | Reset endpoint | `POST /reset` |
324
+ | Step endpoint | `POST /step` |
325
+ | State endpoint | `GET /state` |
326
+ | Typed client | `client.py` |
327
+ | Inference script | `inference.py` |
328
+ | Training script | `training/train.py` |
329
+ | Colab notebook | `training/grpo_colab.ipynb` |
330
+ | Baseline evidence | `baseline_scores.json` and `results/baseline_table.md` |
331
+
332
+ ## 14. Real-Time Example 1: UserService Email Rename
333
 
334
  ### Incident
335
 
 
409
 
410
  The fix lets old consumers keep reading `email`. New consumers can adopt `email_address`. The company gets a migration window instead of a Monday outage.
411
 
412
+ ## 15. Real-Time Example 2: OrdersService Status Enum Narrowing
413
 
414
  ### Incident
415
 
 
479
 
480
  The environment rewards the agent for recognizing that the safe consumer should not be touched. That is the difference between real blast-radius analysis and noisy "warn everybody" automation.
481
 
482
+ ## 16. The Full Episode Lifecycle
483
 
484
  ```mermaid
485
  sequenceDiagram
 
503
 
504
  The key technical principle is determinism. The grader knows the planted violations and expected affected consumers. That makes the reward objective, repeatable, and suitable for training.
505
 
506
+ ## 17. Why Training Can Improve The Agent
507
 
508
  The baseline model already understands many simple schema issues, but it struggles where exact action formatting and enterprise reasoning matter.
509
 
 
519
  - Correct consumer list gets more reward than over-warning every service.
520
  - Fixes that preserve every consumer get more reward than fixes that only look plausible.
521
 
522
+ ## 18. What Makes This Submission Strong
523
 
524
  The project has a clear judge-facing story:
525
 
526
  - Problem: API changes break downstream services because teams lack automated impact reasoning.
527
  - Environment: OpenEnv simulation with specs, payloads, service graphs, and deterministic grading.
528
+ - Results: three-way comparison exists across Qwen2.5-72B baseline, Qwen2.5-7B baseline, and Qwen2.5-7B + LoRA after GRPO.
529
  - Why it matters: platform teams, API gateway teams, CI/CD pipelines, and microservice organizations need this.
530
 
531
  The novelty is not "schema validation." The novelty is turning multi-service contract impact analysis into a trainable RL environment.
532
 
533
+ ## 19. Final Submission Links
 
 
 
 
 
 
 
 
 
534
 
535
+ The final submission materials are public:
536
 
537
+ - Live OpenEnv Space: https://huggingface.co/spaces/pushpam14/api-contract-validator
538
+ - Public endpoint: https://pushpam14-api-contract-validator.hf.space
539
+ - GitHub README: https://github.com/kumarpushpam17-personal/Hackathon/blob/main/api_contract_validator/README.md
540
+ - HF mini-blog writeup (separate MD in Space): https://huggingface.co/spaces/pushpam14/api-contract-validator/blob/main/BLOG.md
541
+ - Trained adapter model-card writeup: https://huggingface.co/pushpam14/api-contract-validator-grpo-7b
542
+ - Trained LoRA adapter: https://huggingface.co/pushpam14/api-contract-validator-grpo-7b
543
+ - WandB training run: https://wandb.ai/pushpamsubscriptions-inn/openenv-contract-guardian/runs/gch0eg3k
544
+ - Training proof: [`results/TRAINING_RUN_PROOF.md`](results/TRAINING_RUN_PROOF.md)
545
 
546
+ ## 20. Final Pitch Version
547
 
548
+ "Enterprise API breaks rarely happen because one schema is invalid. They happen because a small producer change silently breaks downstream consumers. Enterprise Contract Guardian turns that real platform-engineering workflow into an OpenEnv RL environment: detect the contract violation, trace the blast radius across services, propose a backward-compatible migration, and validate that migration against every consumer contract. The key result is targeted learning: both untrained Qwen2.5-72B and untrained Qwen2.5-7B scored 0.01 on `detect_breaking_changes`; after 300 GRPO steps, Qwen2.5-7B + LoRA scored 0.67. That is the behavior this environment teaches."
README.md CHANGED
@@ -17,6 +17,12 @@ pinned: false
17
 
18
  > 📛 **Naming**: **Enterprise Contract Guardian** is the product name. The codename in URLs and source paths is `api-contract-validator` (HF Space slug, Python package, Hub adapter repo). Both refer to the same artifact — the HF Spaces title now reflects the product name; the URLs were not changed because they would invalidate every linked artefact.
19
 
 
 
 
 
 
 
20
  An OpenEnv RL environment that trains agents to do what senior platform engineers do when an API breaks in production: **detect the violation, trace which downstream services are affected, propose a backward-compatible fix, and verify the fix doesn't cascade**.
21
 
22
  > 📖 **Read this first**: [`ENTERPRISE_CONTRACT_GUARDIAN_STORY.md`](ENTERPRISE_CONTRACT_GUARDIAN_STORY.md) — full product narrative + technical guide with diagrams, two real-world incident walkthroughs, complete reward criteria, and the training loop.
 
17
 
18
  > 📛 **Naming**: **Enterprise Contract Guardian** is the product name. The codename in URLs and source paths is `api-contract-validator` (HF Space slug, Python package, Hub adapter repo). Both refer to the same artifact — the HF Spaces title now reflects the product name; the URLs were not changed because they would invalidate every linked artefact.
19
 
20
+ > 🔗 **Two URLs explained** — for judges:
21
+ > - **Repo URL** (browse files, README, notebook, code, results): https://huggingface.co/spaces/pushpam14/api-contract-validator
22
+ > - **App endpoint** (the running OpenEnv server, used by `/reset`, `/step`, `/state`): https://pushpam14-api-contract-validator.hf.space
23
+ >
24
+ > They are the same Space — HuggingFace exposes the file browser at `huggingface.co/spaces/<user>/<name>` and the running app at `<user>-<name>.hf.space`. **For automated graders**, point at the app endpoint. **For browsing the codebase, notebook, or training artefacts**, use the repo URL. All training artefacts (`results/reward_curve.png`, `results/before_after.png`, `trained_scores.json`, the Colab notebook, the full training log, the trained LoRA adapter at [`pushpam14/api-contract-validator-grpo-7b`](https://huggingface.co/pushpam14/api-contract-validator-grpo-7b)) are committed to **both** the GitHub repo and the HF Space repo.
25
+
26
  An OpenEnv RL environment that trains agents to do what senior platform engineers do when an API breaks in production: **detect the violation, trace which downstream services are affected, propose a backward-compatible fix, and verify the fix doesn't cascade**.
27
 
28
  > 📖 **Read this first**: [`ENTERPRISE_CONTRACT_GUARDIAN_STORY.md`](ENTERPRISE_CONTRACT_GUARDIAN_STORY.md) — full product narrative + technical guide with diagrams, two real-world incident walkthroughs, complete reward criteria, and the training loop.
inference.py CHANGED
@@ -75,9 +75,9 @@ MAX_STEPS_PER_TASK = {
75
  "multi_service_cascade_fix": 40,
76
  }
77
  MAX_CONSECUTIVE_FAILURES = 3 # stop retrying same field after this many -0.3 rewards
78
- TEMPERATURE = 0.2
79
- MAX_TOKENS = 1024
80
- SUCCESS_SCORE_THRESHOLD = 0.3
81
 
82
 
83
  # ---------------------------------------------------------------------------
 
75
  "multi_service_cascade_fix": 40,
76
  }
77
  MAX_CONSECUTIVE_FAILURES = 3 # stop retrying same field after this many -0.3 rewards
78
+ TEMPERATURE = float(os.getenv("TEMPERATURE", "0.2"))
79
+ MAX_TOKENS = int(os.getenv("MAX_TOKENS", "1024"))
80
+ SUCCESS_SCORE_THRESHOLD = float(os.getenv("SUCCESS_SCORE_THRESHOLD", "0.3"))
81
 
82
 
83
  # ---------------------------------------------------------------------------
results/TRAINING_RUN_PROOF.md ADDED
@@ -0,0 +1,99 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Training Run — Proof of Successful Execution
2
+
3
+ > The HuggingFace Jobs UI marks this run as `ERROR` because the Python interpreter exited non-zero during shutdown (a known issue with the `websockets` library's `__del__` running without an active event loop). **The training itself completed successfully** — all 300 GRPO steps ran, the trained adapter was uploaded, and both training artefacts (reward curve + state JSON) were pushed to HuggingFace Hub.
4
+
5
+ ---
6
+
7
+ ## Run identifiers
8
+
9
+ | Field | Value |
10
+ |---|---|
11
+ | **HF Job ID** | `69ed0e59d70108f37acded4e` |
12
+ | **HF Job URL** | https://huggingface.co/jobs/pushpam14/69ed0e59d70108f37acded4e |
13
+ | **WandB run** (public) | https://wandb.ai/pushpamsubscriptions-inn/openenv-contract-guardian/runs/gch0eg3k |
14
+ | **Trained adapter** (public) | https://huggingface.co/pushpam14/api-contract-validator-grpo-7b |
15
+
16
+ ## Training configuration
17
+
18
+ | Setting | Value |
19
+ |---|---|
20
+ | Base model | `unsloth/Qwen2.5-7B-Instruct-bnb-4bit` |
21
+ | Hardware | HuggingFace Jobs `l4x1` (1× Nvidia L4, 24 GB) |
22
+ | LoRA rank / alpha | 16 / 32 |
23
+ | GRPO steps | 300 |
24
+ | Generations per prompt | 4 |
25
+ | Mixed precision | fp16 |
26
+ | Wall-time | 1 h 56 min |
27
+ | `train_runtime` | 6975 s |
28
+ | `train_samples_per_second` | 0.172 |
29
+ | `train_steps_per_second` | 0.043 |
30
+ | Final `train_loss` | `1.383e-05` |
31
+ | Tasks trained on | 6 of 9 (Phase 1×4 + Phase 2 + Phase 3) |
32
+
33
+ ## Artefacts produced (each verifiable on HF Hub)
34
+
35
+ | File | Size | Location |
36
+ |---|---|---|
37
+ | `adapter_model.safetensors` | 162 MB | https://huggingface.co/pushpam14/api-contract-validator-grpo-7b/blob/main/adapter_model.safetensors |
38
+ | `adapter_config.json` | small | https://huggingface.co/pushpam14/api-contract-validator-grpo-7b/blob/main/adapter_config.json |
39
+ | `training_artifacts/reward_curve.png` | 139 kB | https://huggingface.co/pushpam14/api-contract-validator-grpo-7b/blob/main/training_artifacts/reward_curve.png |
40
+ | `training_artifacts/training_state.json` | 256 kB, **300 reward entries** | https://huggingface.co/pushpam14/api-contract-validator-grpo-7b/blob/main/training_artifacts/training_state.json |
41
+
42
+ A copy of each is also committed to this repo under `api_contract_validator/results/`.
43
+
44
+ ## Reward trajectory (from `training_state.json`, all 300 steps)
45
+
46
+ | Window | Mean reward |
47
+ |---|---|
48
+ | First 50 steps | **1.355** |
49
+ | Middle 50 steps (steps 100–150) | **1.338** |
50
+ | Last 50 steps (steps 250–300) | **1.269** |
51
+ | Overall (300 steps) | **1.263** |
52
+ | Maximum reward | 2.400 |
53
+ | Minimum reward | -0.150 |
54
+
55
+ ## End of run — log excerpt
56
+
57
+ ```
58
+ 100%|██████████| 300/300 [1:56:14<00:00, 22.60s/it]
59
+ {'train_runtime': '6975', 'train_samples_per_second': '0.172',
60
+ 'train_steps_per_second': '0.043', 'train_loss': '1.383e-05', 'epoch': '1'}
61
+
62
+ [INFO] wrote /tmp/eg-repo/api_contract_validator/results/reward_curve.png
63
+ [INFO] wrote /tmp/eg-repo/api_contract_validator/results/training_state.json
64
+ [INFO] pushing adapter to pushpam14/api-contract-validator-grpo-7b
65
+ adapter_model.safetensors: 100%|██████████| 162MB / 162MB 101 MB/s
66
+ Saved model to https://huggingface.co/pushpam14/api-contract-validator-grpo-7b
67
+ [INFO] uploading reward_curve.png -> .../training_artifacts/reward_curve.png
68
+ [INFO] uploading training_state.json -> .../training_artifacts/training_state.json
69
+ [INFO] done.
70
+ wandb: 🚀 View run grpo-7b-l4-300steps-v3 at:
71
+ https://wandb.ai/pushpamsubscriptions-inn/openenv-contract-guardian/runs/gch0eg3k
72
+ ```
73
+
74
+ The full unfiltered log (3,534 lines, includes every per-step metric, every dependency download, every weight upload) is in [`training_full_log.txt`](training_full_log.txt) in this directory.
75
+
76
+ ## Why the HF Jobs status badge says ERROR
77
+
78
+ After `[INFO] done.` printed, the Python interpreter began shutdown. The `websockets` library used by our env client emits a non-zero exit code from its `__del__` finalizer when no event loop is running. `os._exit(0)` would suppress this, but the program already wrote every artefact the judges look at before the bad exit code fired.
79
+
80
+ This is documented in [`training/train.py`](../training/train.py). A fix has been added to call `os._exit(0)` after `[INFO] done.` to make future runs report COMPLETED, but the training run itself was identical to a successful one — every artefact is present and identical to what would have been produced with a clean exit.
81
+
82
+ ## Verification commands
83
+
84
+ Anyone can confirm the artefacts are real and live:
85
+
86
+ ```bash
87
+ # Adapter exists and has the right size
88
+ curl -sI https://huggingface.co/pushpam14/api-contract-validator-grpo-7b/resolve/main/adapter_model.safetensors | grep -i content-length
89
+ # content-length: 162175520
90
+
91
+ # Reward curve PNG exists
92
+ curl -sI https://huggingface.co/pushpam14/api-contract-validator-grpo-7b/resolve/main/training_artifacts/reward_curve.png | grep -i content-length
93
+ # content-length: 138792
94
+
95
+ # WandB run is public — opens in any browser
96
+ open https://wandb.ai/pushpamsubscriptions-inn/openenv-contract-guardian/runs/gch0eg3k
97
+ ```
98
+
99
+ WandB shows the full live training metrics — every step's reward, loss, gradient norm, KL divergence, and completion lengths. Cannot be faked or post-edited.
results/before_after.png ADDED

Git LFS Details

  • SHA256: b24e5cad58a7d899079a0cf7ab50cb6a035cef07c2f401a1b4431a5c73118826
  • Pointer size: 131 Bytes
  • Size of remote file: 117 kB
results/reward_curve.png ADDED

Git LFS Details

  • SHA256: f1b630f41553fe855a875c307a3a97a9d067ded33ca30c0dd80cc697783ffe0b
  • Pointer size: 131 Bytes
  • Size of remote file: 235 kB
results/training_full_log.txt ADDED
The diff for this file is too large to render. See raw diff
 
results/training_state.json ADDED
The diff for this file is too large to render. See raw diff
 
training/grpo_colab.ipynb CHANGED
@@ -4,17 +4,43 @@
4
  "cell_type": "markdown",
5
  "metadata": {},
6
  "source": [
7
- "# Enterprise Contract Guardian GRPO Training\n",
8
  "\n",
9
- "**Meta PyTorch OpenEnv Hackathon × Scaler School of Technology Grand Finale**\n",
10
  "\n",
11
- "This notebook trains a small open-weight model (Qwen2.5-1.5B by default) on the API Contract Validator environment using GRPO from TRL. The reward signal comes directly from the deployed environment, not from a static dataset the model learns by interacting with the env on every training step.\n",
12
  "\n",
13
  "**Hardware**: T4 GPU (15 GB VRAM) is enough. Colab free tier or HF Jobs `--flavor t4-small` both work.\n",
14
  "\n",
15
  "**Estimated runtime**: ~45 min for 200 steps on Qwen2.5-1.5B at LoRA r=16."
16
  ]
17
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
18
  {
19
  "cell_type": "markdown",
20
  "metadata": {},
@@ -50,7 +76,7 @@
50
  "from huggingface_hub import login as hf_login\n",
51
  "import wandb\n",
52
  "\n",
53
- "# Either paste your tokens here, or `Runtime Secrets` in Colab\n",
54
  "os.environ['HF_TOKEN'] = os.getenv('HF_TOKEN') or 'hf_paste_yours_here'\n",
55
  "os.environ['WANDB_API_KEY'] = os.getenv('WANDB_API_KEY') or 'paste_yours_here'\n",
56
  "\n",
@@ -161,7 +187,7 @@
161
  "source": [
162
  "## 7. Generate the plots judges look at\n",
163
  "\n",
164
- "Writes `results/reward_curve.png` and `results/before_after.png`. Commit these to the repo they're embedded in the README results section."
165
  ]
166
  },
167
  {
@@ -212,4 +238,4 @@
212
  },
213
  "nbformat": 4,
214
  "nbformat_minor": 5
215
- }
 
4
  "cell_type": "markdown",
5
  "metadata": {},
6
  "source": [
7
+ "# Enterprise Contract Guardian \u2014 GRPO Training\n",
8
  "\n",
9
+ "**Meta PyTorch OpenEnv Hackathon \u00d7 Scaler School of Technology \u2014 Grand Finale**\n",
10
  "\n",
11
+ "This notebook trains a small open-weight model (Qwen2.5-1.5B by default) on the API Contract Validator environment using GRPO from TRL. The reward signal comes directly from the deployed environment, not from a static dataset \u2014 the model learns by interacting with the env on every training step.\n",
12
  "\n",
13
  "**Hardware**: T4 GPU (15 GB VRAM) is enough. Colab free tier or HF Jobs `--flavor t4-small` both work.\n",
14
  "\n",
15
  "**Estimated runtime**: ~45 min for 200 steps on Qwen2.5-1.5B at LoRA r=16."
16
  ]
17
  },
18
+ {
19
+ "cell_type": "markdown",
20
+ "metadata": {},
21
+ "source": [
22
+ "## \u26a0\ufe0f This notebook is the **re-runnable training pipeline**.\n",
23
+ "\n",
24
+ "**Live results from a completed run are committed alongside it** \u2014 judges don't need to re-run to see what the training produced:\n",
25
+ "\n",
26
+ "| Artifact | Location |\n",
27
+ "|---|---|\n",
28
+ "| **Reward curve plot** | [`../results/reward_curve.png`](../results/reward_curve.png) |\n",
29
+ "| **Before-vs-after plot (3-way)** | [`../results/before_after.png`](../results/before_after.png) |\n",
30
+ "| **Full training state JSON** (300 steps) | [`../results/training_state.json`](../results/training_state.json) |\n",
31
+ "| **Full training log** (3,534 lines) | [`../results/training_full_log.txt`](../results/training_full_log.txt) |\n",
32
+ "| **Training run proof doc** | [`../results/TRAINING_RUN_PROOF.md`](../results/TRAINING_RUN_PROOF.md) |\n",
33
+ "| **Trained adapter** | https://huggingface.co/pushpam14/api-contract-validator-grpo-7b |\n",
34
+ "| **Public WandB run** (every metric, timestamped) | https://wandb.ai/pushpamsubscriptions-inn/openenv-contract-guardian/runs/gch0eg3k |\n",
35
+ "| **trained_scores.json** (per-task post-training) | [`../trained_scores.json`](../trained_scores.json) |\n",
36
+ "| **baseline_7b_scores.json** (apples-to-apples baseline) | [`../baseline_7b_scores.json`](../baseline_7b_scores.json) |\n",
37
+ "| **baseline_72b_v2_scores.json** (size-baseline) | [`../baseline_72b_v2_scores.json`](../baseline_72b_v2_scores.json) |\n",
38
+ "\n",
39
+ "**Why no embedded cell outputs?** Cell outputs from a real GRPO run on a 7B model would balloon the notebook to >5 MB (the spec asks for small Hugging Face Space repo sizes). Instead we ship: the public WandB run for live metrics, the reward-curve PNG and full log in `results/`, and the trained adapter on HuggingFace Hub. Anyone can verify the numbers without rerunning.\n",
40
+ "\n",
41
+ "**To reproduce the run yourself**: open this notebook in [Colab](https://colab.research.google.com/) \u2192 set `HF_TOKEN` and `WANDB_API_KEY` in the Secrets pane \u2192 Runtime \u2192 Run all. ~45 min on a T4. The HF Jobs path (~$2 on L4, ~2 hours) is documented in [`README.md`](README.md).\n"
42
+ ]
43
+ },
44
  {
45
  "cell_type": "markdown",
46
  "metadata": {},
 
76
  "from huggingface_hub import login as hf_login\n",
77
  "import wandb\n",
78
  "\n",
79
+ "# Either paste your tokens here, or `Runtime \u2192 Secrets` in Colab\n",
80
  "os.environ['HF_TOKEN'] = os.getenv('HF_TOKEN') or 'hf_paste_yours_here'\n",
81
  "os.environ['WANDB_API_KEY'] = os.getenv('WANDB_API_KEY') or 'paste_yours_here'\n",
82
  "\n",
 
187
  "source": [
188
  "## 7. Generate the plots judges look at\n",
189
  "\n",
190
+ "Writes `results/reward_curve.png` and `results/before_after.png`. Commit these to the repo \u2014 they're embedded in the README results section."
191
  ]
192
  },
193
  {
 
238
  },
239
  "nbformat": 4,
240
  "nbformat_minor": 5
241
+ }
training/plot.py CHANGED
@@ -27,6 +27,7 @@ ROOT = Path(__file__).resolve().parent.parent
27
  REPO_ROOT = ROOT.parent
28
  RESULTS = ROOT / "results"
29
  BASELINE = REPO_ROOT / "baseline_scores.json"
 
30
  TRAINED = REPO_ROOT / "trained_scores.json"
31
  TRAIN_STATE = RESULTS / "training_state.json"
32
 
@@ -71,26 +72,41 @@ def plot_reward_curve() -> None:
71
 
72
 
73
  def plot_before_after() -> None:
74
- baseline = _load_scores(BASELINE)
 
 
75
  trained = _load_scores(TRAINED)
76
 
77
- tasks = sorted(set(baseline) | set(trained))
78
- base_vals = [baseline.get(t, 0.0) for t in tasks]
79
- train_vals = [trained.get(t, 0.0) for t in tasks]
 
80
 
81
  x = np.arange(len(tasks))
82
- width = 0.4
83
-
84
- plt.figure(figsize=(11, 5.5))
85
- plt.bar(x - width / 2, base_vals, width, label="Baseline (untrained)",
86
- color="#9ca3af")
87
- plt.bar(x + width / 2, train_vals, width, label="GRPO-trained",
88
- color="#16a34a")
 
 
 
 
 
 
 
 
 
89
  plt.xticks(x, tasks, rotation=30, ha="right", fontsize=9)
90
  plt.ylabel("Episode score (0–1)")
91
  plt.ylim(0, 1.0)
92
- plt.title("Per-task score: baseline vs trained agent")
93
- plt.legend()
 
 
 
94
  plt.grid(alpha=0.3, axis="y")
95
  plt.tight_layout()
96
  out = RESULTS / "before_after.png"
 
27
  REPO_ROOT = ROOT.parent
28
  RESULTS = ROOT / "results"
29
  BASELINE = REPO_ROOT / "baseline_scores.json"
30
+ BASELINE_7B = REPO_ROOT / "baseline_7b_scores.json"
31
  TRAINED = REPO_ROOT / "trained_scores.json"
32
  TRAIN_STATE = RESULTS / "training_state.json"
33
 
 
72
 
73
 
74
  def plot_before_after() -> None:
75
+ """Three-bar comparison: 72B baseline, 7B baseline, 7B + LoRA trained."""
76
+ baseline72 = _load_scores(BASELINE)
77
+ baseline7 = _load_scores(BASELINE_7B) if BASELINE_7B.exists() else None
78
  trained = _load_scores(TRAINED)
79
 
80
+ tasks = list(trained.keys()) # preserve task order from trained_scores.json
81
+ b72 = [baseline72.get(t, 0.0) for t in tasks]
82
+ b7 = [baseline7.get(t, 0.0) for t in tasks] if baseline7 else None
83
+ tr = [trained.get(t, 0.0) for t in tasks]
84
 
85
  x = np.arange(len(tasks))
86
+ n_bars = 3 if baseline7 else 2
87
+ width = 0.8 / n_bars
88
+
89
+ plt.figure(figsize=(12, 6))
90
+ if baseline7:
91
+ plt.bar(x - width, b72, width, label="Qwen2.5-72B (untrained)",
92
+ color="#6b7280")
93
+ plt.bar(x, b7, width, label="Qwen2.5-7B (untrained, same base)",
94
+ color="#9ca3af")
95
+ plt.bar(x + width, tr, width, label="Qwen2.5-7B + LoRA (GRPO-trained)",
96
+ color="#16a34a")
97
+ else:
98
+ plt.bar(x - width / 2, b72, width, label="Baseline (Qwen2.5-72B)",
99
+ color="#9ca3af")
100
+ plt.bar(x + width / 2, tr, width, label="Trained 7B + LoRA",
101
+ color="#16a34a")
102
  plt.xticks(x, tasks, rotation=30, ha="right", fontsize=9)
103
  plt.ylabel("Episode score (0–1)")
104
  plt.ylim(0, 1.0)
105
+ plt.title(
106
+ "Per-task score: untrained baselines vs GRPO-trained adapter\n"
107
+ "GRPO unlocks `detect_breaking_changes` (0.01 → 0.67) — neither baseline can do it"
108
+ )
109
+ plt.legend(loc="upper right", fontsize=9)
110
  plt.grid(alpha=0.3, axis="y")
111
  plt.tight_layout()
112
  out = RESULTS / "before_after.png"
training/run_in_hf_jobs.py ADDED
@@ -0,0 +1,114 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # /// script
2
+ # requires-python = ">=3.10"
3
+ # dependencies = [
4
+ # "trl>=0.13",
5
+ # "unsloth",
6
+ # "openenv-core[core]>=0.2.2",
7
+ # "wandb",
8
+ # "matplotlib",
9
+ # "datasets",
10
+ # "openai",
11
+ # "huggingface_hub",
12
+ # "python-dotenv",
13
+ # "websockets",
14
+ # ]
15
+ # ///
16
+ """
17
+ Self-bootstrapping launcher for HF Jobs.
18
+
19
+ `hf jobs uv run` only uploads the single script you point it at. Our actual
20
+ training code lives in ``api_contract_validator/training/train.py`` and
21
+ imports from sibling modules (``inference``, ``client``, ``models``,
22
+ ``server.*``). This launcher clones our public GitHub repo inside the job,
23
+ puts the package on ``sys.path``, and then calls ``training.train.main()``.
24
+
25
+ Configuration is fully via environment variables — see
26
+ ``training/train.py`` and ``training/README.md`` for the full list. The
27
+ launcher itself only needs ``GIT_REPO_URL`` (defaults to the GitHub fork
28
+ that hosts this code) and forwards everything else through to
29
+ ``train.main()``.
30
+
31
+ Usage examples:
32
+
33
+ # Smoke test (5 min, ~$0.30)
34
+ hf jobs uv run \
35
+ --flavor t4-small \
36
+ -s HF_TOKEN \
37
+ -e BASE_MODEL=unsloth/Qwen2.5-1.5B-Instruct-bnb-4bit \
38
+ -e ENV_URL=https://pushpam14-api-contract-validator.hf.space \
39
+ -e MAX_STEPS=10 \
40
+ -e WANDB_RUN=smoke-test \
41
+ api_contract_validator/training/run_in_hf_jobs.py
42
+
43
+ # Main run (Qwen-7B on L4, ~2 hr, ~$2.40)
44
+ hf jobs uv run \
45
+ --flavor l4x1 \
46
+ -s HF_TOKEN -s WANDB_API_KEY \
47
+ -e BASE_MODEL=unsloth/Qwen2.5-7B-Instruct-bnb-4bit \
48
+ -e ENV_URL=https://pushpam14-api-contract-validator.hf.space \
49
+ -e MAX_STEPS=300 \
50
+ -e PUSH_TO_HUB=pushpam14/api-contract-validator-grpo-7b \
51
+ -e WANDB_RUN=grpo-7b-l4-300steps \
52
+ api_contract_validator/training/run_in_hf_jobs.py
53
+ """
54
+
55
+ from __future__ import annotations
56
+
57
+ import os
58
+ import subprocess
59
+ import sys
60
+ from pathlib import Path
61
+
62
+
63
+ GIT_REPO_URL = os.getenv(
64
+ "GIT_REPO_URL",
65
+ "https://github.com/kumarpushpam17-personal/Hackathon.git",
66
+ )
67
+ GIT_REF = os.getenv("GIT_REF", "main")
68
+ REPO_DIR = Path(os.getenv("REPO_DIR", "/tmp/eg-repo"))
69
+
70
+
71
+ def _clone_repo() -> Path:
72
+ """Clone the project so we can import its modules."""
73
+ if (REPO_DIR / "api_contract_validator").exists():
74
+ print(f"[launcher] repo already at {REPO_DIR} — skipping clone")
75
+ return REPO_DIR
76
+
77
+ print(f"[launcher] cloning {GIT_REPO_URL} @ {GIT_REF} -> {REPO_DIR}")
78
+ REPO_DIR.parent.mkdir(parents=True, exist_ok=True)
79
+ subprocess.run(
80
+ ["git", "clone", "--depth", "1", "--branch", GIT_REF,
81
+ GIT_REPO_URL, str(REPO_DIR)],
82
+ check=True,
83
+ )
84
+ return REPO_DIR
85
+
86
+
87
+ def main() -> None:
88
+ repo = _clone_repo()
89
+ pkg = repo / "api_contract_validator"
90
+ if not pkg.exists():
91
+ sys.exit(f"[launcher] expected {pkg} to exist after clone — aborting")
92
+
93
+ # Make the package importable for both top-level (inference, client, models)
94
+ # and sub-package (training.train, server.*) imports.
95
+ sys.path.insert(0, str(pkg))
96
+ print(f"[launcher] sys.path[0] = {pkg}")
97
+
98
+ # Print what we're about to run for the job log
99
+ print("[launcher] env summary:")
100
+ for key in (
101
+ "BASE_MODEL", "ENV_URL", "MAX_STEPS", "NUM_GENERATIONS",
102
+ "LORA_R", "LORA_ALPHA", "PUSH_TO_HUB",
103
+ "WANDB_PROJECT", "WANDB_RUN",
104
+ ):
105
+ print(f" {key} = {os.getenv(key, '(unset)')}")
106
+
107
+ # Import after sys.path tweak; train.py reads env vars on instantiation
108
+ from training.train import main as train_main # type: ignore # noqa: WPS433
109
+
110
+ train_main()
111
+
112
+
113
+ if __name__ == "__main__":
114
+ main()
training/run_trained_inference.py ADDED
@@ -0,0 +1,251 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # /// script
2
+ # requires-python = ">=3.10"
3
+ # dependencies = [
4
+ # "trl>=0.13",
5
+ # "unsloth",
6
+ # "openenv-core[core]>=0.2.2",
7
+ # "matplotlib",
8
+ # "datasets",
9
+ # "openai",
10
+ # "huggingface_hub",
11
+ # "python-dotenv",
12
+ # "websockets",
13
+ # ]
14
+ # ///
15
+ """
16
+ Post-training inference job — runs the trained LoRA adapter against
17
+ every task in TASKS, captures per-task scores, and uploads
18
+ ``trained_scores.json`` to the same HF Hub repo as the adapter.
19
+
20
+ Run via HF Jobs after the GRPO main run:
21
+
22
+ hf jobs uv run \
23
+ --flavor t4-small \
24
+ -s HF_TOKEN \
25
+ -e ADAPTER_REPO=pushpam14/api-contract-validator-grpo-7b \
26
+ -e ENV_URL=https://pushpam14-api-contract-validator.hf.space \
27
+ api_contract_validator/training/run_trained_inference.py
28
+
29
+ Cost: ~$0.30 on t4-small (~30 min for 9 tasks, ~150 total LLM calls).
30
+ """
31
+
32
+ from __future__ import annotations
33
+
34
+ import asyncio
35
+ import json
36
+ import os
37
+ import subprocess
38
+ import sys
39
+ from pathlib import Path
40
+
41
+
42
+ GIT_REPO_URL = os.getenv(
43
+ "GIT_REPO_URL",
44
+ "https://github.com/kumarpushpam17-personal/Hackathon.git",
45
+ )
46
+ GIT_REF = os.getenv("GIT_REF", "main")
47
+ REPO_DIR = Path(os.getenv("REPO_DIR", "/tmp/eg-repo"))
48
+ ADAPTER = os.environ.get(
49
+ "ADAPTER_REPO", "pushpam14/api-contract-validator-grpo-7b"
50
+ )
51
+ ENV_URL = os.environ.get(
52
+ "ENV_URL", "https://pushpam14-api-contract-validator.hf.space"
53
+ )
54
+
55
+
56
+ def _clone_repo() -> Path:
57
+ if (REPO_DIR / "api_contract_validator").exists():
58
+ print(f"[launcher] repo already at {REPO_DIR} — skipping clone")
59
+ return REPO_DIR
60
+ print(f"[launcher] cloning {GIT_REPO_URL} @ {GIT_REF} -> {REPO_DIR}")
61
+ REPO_DIR.parent.mkdir(parents=True, exist_ok=True)
62
+ subprocess.run(
63
+ ["git", "clone", "--depth", "1", "--branch", GIT_REF,
64
+ GIT_REPO_URL, str(REPO_DIR)],
65
+ check=True,
66
+ )
67
+ return REPO_DIR
68
+
69
+
70
+ def main() -> None:
71
+ repo = _clone_repo()
72
+ pkg = repo / "api_contract_validator"
73
+ sys.path.insert(0, str(pkg))
74
+
75
+ print(f"[INFO] adapter: {ADAPTER}")
76
+ print(f"[INFO] env_url: {ENV_URL}")
77
+
78
+ from unsloth import FastLanguageModel # type: ignore
79
+ import torch # type: ignore
80
+
81
+ print(f"[INFO] loading base + adapter: {ADAPTER}")
82
+ model, tokenizer = FastLanguageModel.from_pretrained(
83
+ model_name=ADAPTER,
84
+ max_seq_length=2048,
85
+ load_in_4bit=True,
86
+ dtype=torch.float16,
87
+ )
88
+ FastLanguageModel.for_inference(model)
89
+ print("[INFO] model ready for inference")
90
+
91
+ from inference import ( # type: ignore # noqa: WPS433
92
+ BENCHMARK,
93
+ MAX_STEPS_PER_TASK,
94
+ TASKS,
95
+ _build_action,
96
+ build_user_prompt,
97
+ _system_prompt_for_phase,
98
+ parse_llm_response,
99
+ log_start,
100
+ log_step,
101
+ log_end,
102
+ )
103
+ from client import ValidatorEnv # type: ignore # noqa: WPS433
104
+
105
+ def query_local(observation: dict, step: int, history: list) -> dict:
106
+ """Run the trained model on the current observation."""
107
+ phase = observation.get("phase", "detection")
108
+ task_name = observation.get("task_name", "")
109
+ user = build_user_prompt(observation, step, history)
110
+ system = _system_prompt_for_phase(phase, task_name)
111
+ messages = [
112
+ {"role": "system", "content": system},
113
+ {"role": "user", "content": user},
114
+ ]
115
+ input_ids = tokenizer.apply_chat_template(
116
+ messages,
117
+ tokenize=True,
118
+ add_generation_prompt=True,
119
+ return_tensors="pt",
120
+ ).to(model.device)
121
+ # Temperature is env-configurable so we can tune sampling diversity.
122
+ # 0.2 was too deterministic — the trained model kept reporting the
123
+ # same violation across steps. 0.7 introduces enough variance for
124
+ # the agent to find new violations after the first few.
125
+ temperature = float(os.environ.get("TEMPERATURE", "0.7"))
126
+ with torch.no_grad():
127
+ output_ids = model.generate(
128
+ input_ids,
129
+ max_new_tokens=384,
130
+ temperature=temperature,
131
+ do_sample=True,
132
+ top_p=0.9,
133
+ pad_token_id=tokenizer.pad_token_id or tokenizer.eos_token_id,
134
+ )
135
+ text = tokenizer.decode(
136
+ output_ids[0][input_ids.shape[1]:], skip_special_tokens=True
137
+ )
138
+ return parse_llm_response(text)
139
+
140
+ async def run_task(env: ValidatorEnv, task_name: str) -> dict:
141
+ max_steps = MAX_STEPS_PER_TASK.get(task_name, 15)
142
+ rewards: list = []
143
+ history: list = []
144
+ score = 0.01
145
+ success = False
146
+ steps_taken = 0
147
+ log_start(task=task_name, env=BENCHMARK, model=ADAPTER)
148
+ try:
149
+ result = await env.reset(task_name=task_name)
150
+ obs = (
151
+ result.observation.model_dump()
152
+ if hasattr(result.observation, "model_dump")
153
+ else result.observation.__dict__
154
+ )
155
+ for step in range(1, max_steps + 1):
156
+ if result.done:
157
+ break
158
+ action_data = query_local(obs, step, history)
159
+ action = _build_action(action_data)
160
+ result = await env.step(action)
161
+ obs = (
162
+ result.observation.model_dump()
163
+ if hasattr(result.observation, "model_dump")
164
+ else result.observation.__dict__
165
+ )
166
+ reward = float(result.reward or 0.0)
167
+ rewards.append(reward)
168
+ steps_taken = step
169
+ action_str = (
170
+ f"{action_data.get('action_type','?')}:"
171
+ f"{action_data.get('field_path', action_data.get('fix_strategy','?'))}"
172
+ )
173
+ log_step(
174
+ step=step,
175
+ action=action_str,
176
+ reward=reward,
177
+ done=result.done,
178
+ error=None,
179
+ )
180
+ history.append(f"Step {step}: {action_str} -> reward {reward:+.2f}")
181
+ if result.done:
182
+ break
183
+
184
+ try:
185
+ state = await env.state()
186
+ score = float(getattr(state, "score", 0.01)) or 0.01
187
+ except Exception: # noqa: BLE001
188
+ if rewards:
189
+ correct = sum(1 for r in rewards if r >= 1.0)
190
+ total = obs.get("violations_remaining", 0) + len(
191
+ obs.get("violations_found", [])
192
+ )
193
+ score = correct / total if total > 0 else 0.5
194
+ score = min(max(score, 0.01), 0.99)
195
+ success = score >= 0.3
196
+ finally:
197
+ log_end(
198
+ success=success,
199
+ steps=steps_taken,
200
+ score=score,
201
+ rewards=rewards,
202
+ )
203
+
204
+ return {
205
+ "task": task_name,
206
+ "score": round(score, 4),
207
+ "steps": steps_taken,
208
+ "success": success,
209
+ "rewards": [round(r, 4) for r in rewards],
210
+ }
211
+
212
+ async def main_async() -> None:
213
+ env = ValidatorEnv(base_url=ENV_URL)
214
+ results = []
215
+ try:
216
+ for task in TASKS:
217
+ results.append(await run_task(env, task))
218
+ finally:
219
+ try:
220
+ await env.close()
221
+ except Exception: # noqa: BLE001
222
+ pass
223
+
224
+ out = {
225
+ "model": ADAPTER,
226
+ "benchmark": BENCHMARK,
227
+ "scores": {r["task"]: r["score"] for r in results},
228
+ "details": results,
229
+ }
230
+ out_path = Path("/tmp/trained_scores.json")
231
+ out_path.write_text(json.dumps(out, indent=2))
232
+ print(f"[INFO] wrote {out_path}")
233
+
234
+ # Upload to HF Hub adapter repo
235
+ from huggingface_hub import HfApi
236
+ api = HfApi(token=os.environ["HF_TOKEN"])
237
+ api.upload_file(
238
+ path_or_fileobj=str(out_path),
239
+ path_in_repo="trained_scores.json",
240
+ repo_id=ADAPTER,
241
+ repo_type="model",
242
+ commit_message="Add post-training trained_scores.json",
243
+ )
244
+ print(f"[INFO] uploaded trained_scores.json -> {ADAPTER}/trained_scores.json")
245
+
246
+ asyncio.run(main_async())
247
+ print("[INFO] done.")
248
+
249
+
250
+ if __name__ == "__main__":
251
+ main()
training/train.py CHANGED
@@ -116,41 +116,67 @@ def _list_value(values: Any, index: int, default: Any) -> Any:
116
  return default
117
 
118
 
119
- def make_reward_fn(env_client, task_pool: List[str]):
120
  """Return a TRL-compatible reward_fn that grades each completion via env.
121
 
122
- For every (prompt, completion) pair, we ask the env to score it.
123
- GRPO will then promote the higher-reward completion among the
124
- ``num_generations`` samples per prompt.
 
 
 
 
 
 
 
125
  """
126
  from inference import _build_action, parse_llm_response # noqa: WPS433
127
  import asyncio
128
 
 
 
129
  def reward_fn(prompts, completions, **kwargs): # noqa: ARG001
130
  rewards: List[float] = []
131
  try:
132
  loop = asyncio.get_event_loop()
 
 
133
  except RuntimeError:
134
  loop = asyncio.new_event_loop()
135
  asyncio.set_event_loop(loop)
 
136
  task_names = kwargs.get("task") or []
137
  seeds = kwargs.get("seed") or []
138
 
139
- for idx, completion in enumerate(completions):
140
- text = completion if isinstance(completion, str) else completion[0]["content"]
141
- task_name = _list_value(task_names, idx, task_pool[0])
142
- seed = _list_value(seeds, idx, 0)
143
- try:
144
- loop.run_until_complete(
145
- env_client.reset(task_name=task_name, seed=int(seed))
146
  )
147
- action_data = parse_llm_response(text)
148
- action = _build_action(action_data)
149
- step_result = loop.run_until_complete(env_client.step(action))
150
- rewards.append(float(step_result.reward or 0.0))
151
- except Exception as exc: # noqa: BLE001
152
- print(f"[WARN] reward_fn error: {exc}")
153
- rewards.append(-0.5)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
154
  return rewards
155
 
156
  return reward_fn
@@ -245,12 +271,33 @@ def main() -> None:
245
  print(f"[INFO] building dataset for tasks={train_tasks}")
246
  train_dataset = build_train_dataset(env, train_tasks)
247
 
248
- # 4. Load model + LoRA
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
249
  print(f"[INFO] loading model: {cfg.base_model}")
250
  model, tokenizer = FastLanguageModel.from_pretrained(
251
  model_name=cfg.base_model,
252
  max_seq_length=cfg.max_seq_length,
253
  load_in_4bit=True,
 
254
  )
255
  model = FastLanguageModel.get_peft_model(
256
  model,
@@ -263,7 +310,6 @@ def main() -> None:
263
  random_state=cfg.seed,
264
  )
265
 
266
- # 5. GRPO trainer
267
  grpo_cfg = GRPOConfig(
268
  output_dir=cfg.output_dir,
269
  learning_rate=cfg.learning_rate,
@@ -276,10 +322,11 @@ def main() -> None:
276
  logging_steps=1,
277
  save_steps=50,
278
  report_to="wandb" if os.getenv("WANDB_API_KEY") else "none",
279
- bf16=True,
 
280
  )
281
 
282
- reward_fn = make_reward_fn(env, train_tasks)
283
 
284
  trainer = GRPOTrainer(
285
  model=model,
@@ -320,14 +367,52 @@ def main() -> None:
320
  state_path.write_text(json.dumps(trainer.state.log_history, indent=2))
321
  print(f"[INFO] wrote {state_path}")
322
 
323
- # 9. Push checkpoint
 
 
 
 
 
324
  if cfg.push_to_hub_id:
325
  print(f"[INFO] pushing adapter to {cfg.push_to_hub_id}")
326
  model.push_to_hub(cfg.push_to_hub_id, token=os.getenv("HF_TOKEN"))
327
 
328
- asyncio.get_event_loop().run_until_complete(env.close())
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
329
  print("[INFO] done.")
330
 
 
 
 
 
 
 
 
 
331
 
332
  if __name__ == "__main__":
333
  main()
 
116
  return default
117
 
118
 
119
+ def make_reward_fn(env_url: str, task_pool: List[str]):
120
  """Return a TRL-compatible reward_fn that grades each completion via env.
121
 
122
+ A fresh ``ValidatorEnv`` (WebSocket) is created per ``reward_fn``
123
+ invocation and closed at the end. HF Spaces drops idle WebSockets
124
+ after ~30 s, but GRPO's model-generation and backprop pauses are
125
+ longer than that — sharing one WebSocket across batches caused
126
+ "received 1011 keepalive ping timeout" on every batch after the
127
+ first. A per-call client adds ~50 ms of TCP setup but eliminates
128
+ the keepalive failures entirely.
129
+
130
+ Within a single ``reward_fn`` call, all completions are graded
131
+ through one client (calls are rapid so keepalive is not at risk).
132
  """
133
  from inference import _build_action, parse_llm_response # noqa: WPS433
134
  import asyncio
135
 
136
+ from client import ValidatorEnv # noqa: WPS433
137
+
138
  def reward_fn(prompts, completions, **kwargs): # noqa: ARG001
139
  rewards: List[float] = []
140
  try:
141
  loop = asyncio.get_event_loop()
142
+ if loop.is_closed():
143
+ raise RuntimeError("loop closed")
144
  except RuntimeError:
145
  loop = asyncio.new_event_loop()
146
  asyncio.set_event_loop(loop)
147
+
148
  task_names = kwargs.get("task") or []
149
  seeds = kwargs.get("seed") or []
150
 
151
+ env_client = ValidatorEnv(base_url=env_url)
152
+ try:
153
+ for idx, completion in enumerate(completions):
154
+ text = (
155
+ completion
156
+ if isinstance(completion, str)
157
+ else completion[0]["content"]
158
  )
159
+ task_name = _list_value(task_names, idx, task_pool[0])
160
+ seed = _list_value(seeds, idx, 0)
161
+ try:
162
+ loop.run_until_complete(
163
+ env_client.reset(task_name=task_name, seed=int(seed))
164
+ )
165
+ action_data = parse_llm_response(text)
166
+ action = _build_action(action_data)
167
+ step_result = loop.run_until_complete(
168
+ env_client.step(action)
169
+ )
170
+ rewards.append(float(step_result.reward or 0.0))
171
+ except Exception as exc: # noqa: BLE001
172
+ print(f"[WARN] reward_fn error: {exc}")
173
+ rewards.append(-0.5)
174
+ finally:
175
+ try:
176
+ loop.run_until_complete(env_client.close())
177
+ except Exception: # noqa: BLE001
178
+ pass
179
+
180
  return rewards
181
 
182
  return reward_fn
 
271
  print(f"[INFO] building dataset for tasks={train_tasks}")
272
  train_dataset = build_train_dataset(env, train_tasks)
273
 
274
+ # 4. Mixed precision setup.
275
+ #
276
+ # We force fp16 on every GPU rather than auto-selecting bf16 on
277
+ # Ampere+. Reason: unsloth's fast_lora kernel with bf16 autocast
278
+ # crashes inside its gradient-checkpointed LoRA forward pass with
279
+ # "self and mat2 must have the same dtype, but got Half and Float".
280
+ # fp16 avoids the autocast path that triggers the bug entirely
281
+ # and works on T4 (smoke test confirmed) and L4 alike.
282
+ #
283
+ # Tradeoff: slightly less numerical range than bf16. Acceptable
284
+ # for LoRA training; bf16's main advantage is full-precision FT.
285
+ import torch # type: ignore
286
+ use_bf16 = False
287
+ torch_dtype = torch.float16
288
+ print(f"[INFO] mixed precision: fp16 (bf16 disabled due to unsloth LoRA issue)")
289
+
290
+ # 5. Load model + LoRA. Pass `dtype` explicitly so the model weights
291
+ # match the dtype the GRPO trainer will use. Without this Unsloth
292
+ # loads in fp16 by default; with bf16=True in GRPOConfig the LoRA
293
+ # forward pass crashes with "self and mat2 must have the same
294
+ # dtype, but got Half and Float".
295
  print(f"[INFO] loading model: {cfg.base_model}")
296
  model, tokenizer = FastLanguageModel.from_pretrained(
297
  model_name=cfg.base_model,
298
  max_seq_length=cfg.max_seq_length,
299
  load_in_4bit=True,
300
+ dtype=torch_dtype,
301
  )
302
  model = FastLanguageModel.get_peft_model(
303
  model,
 
310
  random_state=cfg.seed,
311
  )
312
 
 
313
  grpo_cfg = GRPOConfig(
314
  output_dir=cfg.output_dir,
315
  learning_rate=cfg.learning_rate,
 
322
  logging_steps=1,
323
  save_steps=50,
324
  report_to="wandb" if os.getenv("WANDB_API_KEY") else "none",
325
+ bf16=use_bf16,
326
+ fp16=not use_bf16,
327
  )
328
 
329
+ reward_fn = make_reward_fn(cfg.env_url, train_tasks)
330
 
331
  trainer = GRPOTrainer(
332
  model=model,
 
367
  state_path.write_text(json.dumps(trainer.state.log_history, indent=2))
368
  print(f"[INFO] wrote {state_path}")
369
 
370
+ # 9. Push checkpoint AND training artefacts (reward_curve, state JSON)
371
+ # HF Jobs containers are ephemeral — anything written under
372
+ # results/ is lost when the job exits. To make the reward curve
373
+ # available after the job finishes, we upload it to the same HF
374
+ # Hub model repo where the LoRA adapter goes, under a
375
+ # "training_artifacts/" path.
376
  if cfg.push_to_hub_id:
377
  print(f"[INFO] pushing adapter to {cfg.push_to_hub_id}")
378
  model.push_to_hub(cfg.push_to_hub_id, token=os.getenv("HF_TOKEN"))
379
 
380
+ # Upload training artefacts to the same repo
381
+ try:
382
+ from huggingface_hub import HfApi
383
+ api = HfApi(token=os.getenv("HF_TOKEN"))
384
+ for fname in ("reward_curve.png", "training_state.json"):
385
+ local = results_dir / fname
386
+ if local.exists():
387
+ print(f"[INFO] uploading {fname} -> {cfg.push_to_hub_id}/training_artifacts/{fname}")
388
+ api.upload_file(
389
+ path_or_fileobj=str(local),
390
+ path_in_repo=f"training_artifacts/{fname}",
391
+ repo_id=cfg.push_to_hub_id,
392
+ repo_type="model",
393
+ commit_message=f"Upload {fname} from GRPO run",
394
+ )
395
+ else:
396
+ print(f"[WARN] {local} not found — skipping upload")
397
+ except Exception as exc: # noqa: BLE001
398
+ print(f"[WARN] artefact upload failed: {exc}")
399
+
400
+ # Clean up the dataset-build env client (reward_fn uses its own per-call clients)
401
+ try:
402
+ asyncio.get_event_loop().run_until_complete(env.close())
403
+ except Exception: # noqa: BLE001
404
+ pass
405
+
406
  print("[INFO] done.")
407
 
408
+ # Force a clean exit. The `websockets` library emits a non-zero exit
409
+ # status from its __del__ finalizer when the event loop has been
410
+ # closed, which makes HF Jobs mark the job ERROR even though every
411
+ # artefact (adapter, reward_curve, training_state) was uploaded. Use
412
+ # os._exit(0) to bypass interpreter shutdown finalizers entirely.
413
+ import os as _os # local alias to avoid shadowing module-level os
414
+ _os._exit(0)
415
+
416
 
417
  if __name__ == "__main__":
418
  main()