Spaces:
Sleeping
Sleeping
Sync results, plots, scores, story doc, BLOG, notebook outputs note
Browse files- .gitattributes +2 -0
- BLOG.md +12 -0
- ENTERPRISE_CONTRACT_GUARDIAN_STORY.md +191 -19
- README.md +6 -0
- inference.py +3 -3
- results/TRAINING_RUN_PROOF.md +99 -0
- results/before_after.png +3 -0
- results/reward_curve.png +3 -0
- results/training_full_log.txt +0 -0
- results/training_state.json +0 -0
- training/grpo_colab.ipynb +32 -6
- training/plot.py +29 -13
- training/run_in_hf_jobs.py +114 -0
- training/run_trained_inference.py +251 -0
- training/train.py +109 -24
.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.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
##
|
| 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 |
-
##
|
| 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 |
-
##
|
| 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 |
-
##
|
| 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:
|
| 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 |
-
##
|
| 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 |
-
|
| 372 |
|
| 373 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 374 |
|
| 375 |
-
|
| 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
|
results/reward_curve.png
ADDED
|
Git LFS Details
|
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
|
| 8 |
"\n",
|
| 9 |
-
"**Meta PyTorch OpenEnv Hackathon
|
| 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
|
| 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
|
| 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
|
| 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
|
|
|
|
|
|
|
| 75 |
trained = _load_scores(TRAINED)
|
| 76 |
|
| 77 |
-
tasks =
|
| 78 |
-
|
| 79 |
-
|
|
|
|
| 80 |
|
| 81 |
x = np.arange(len(tasks))
|
| 82 |
-
|
| 83 |
-
|
| 84 |
-
|
| 85 |
-
plt.
|
| 86 |
-
|
| 87 |
-
|
| 88 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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(
|
| 93 |
-
|
|
|
|
|
|
|
|
|
|
| 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(
|
| 120 |
"""Return a TRL-compatible reward_fn that grades each completion via env.
|
| 121 |
|
| 122 |
-
|
| 123 |
-
|
| 124 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
| 140 |
-
|
| 141 |
-
|
| 142 |
-
|
| 143 |
-
|
| 144 |
-
|
| 145 |
-
|
| 146 |
)
|
| 147 |
-
|
| 148 |
-
|
| 149 |
-
|
| 150 |
-
|
| 151 |
-
|
| 152 |
-
|
| 153 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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=
|
|
|
|
| 280 |
)
|
| 281 |
|
| 282 |
-
reward_fn = make_reward_fn(
|
| 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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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()
|