SavK1 Claude Sonnet 4.6 commited on
Commit
2ee348e
Β·
1 Parent(s): 16705aa

docs: rewrite README with full system documentation

Browse files

Covers: core RL insight (procedural org drift), all 4 tasks, scoring
breakdown, action space, org world generation, SFT→GRPO pipeline,
repo layout, quick start, and key design decisions (why each fix was
needed). Keeps HF Space frontmatter.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

Files changed (1) hide show
  1. README.md +233 -28
README.md CHANGED
@@ -11,44 +11,249 @@ tags:
11
  - reinforcement-learning
12
  ---
13
 
14
- # PM-Ops: Procedural Project-Management RL Environment
15
 
16
- A multi-app simulated environment (Ticketing, Codebase, Chat) featuring **procedural
17
- organizational drift**, designed for reinforcement learning training.
18
 
19
- ## What makes it RL-shaped
 
 
20
 
21
- Every episode samples a fresh OrgConfig -- different label names, priority schemes,
22
- team-to-service mappings, and oncall channel names. Frontier LLMs fail because they
23
- rely on memorized conventions; a trained agent learns to read the runbook first.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
24
 
25
  ## Tasks
26
 
27
- | Task | Brief | Max Steps |
28
- |------|-------|-----------|
29
- | triage | Bug report -- correct label, priority, team, channel | 25 |
30
- | incident_routing | Alert -- identify owner via commits, page oncall | 25 |
31
- | release_notes | Compile + post release notes in org style | 40 |
32
- | dep_update | Coordinate dependency update across all owning teams | 40 |
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
33
 
34
- ## Action space
35
 
36
- The agent calls actions like:
37
 
38
- meta.read_runbook -- learn this org's conventions
39
- ticketing.create_ticket -- file a ticket with label + priority
40
- ticketing.assign_ticket -- assign to correct team
41
- chat.post_message -- notify the right oncall channel
42
- meta.finish -- end episode, trigger scoring
43
 
44
- ## Reward
45
 
46
- Delayed to episode end. Deterministic Python verifier -- no LLM-as-judge.
47
- Partial credit for partial correctness.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
48
 
49
- ## Sponsor alignment
50
 
51
- - AI Labs: direct PM-in-the-loop positioning
52
- - Patronus: schema drift -- env tests whether agents track convention changes
53
- - Scale AI: enterprise workflow simulation
54
- - Meta RFC 004: delayed trajectory reward is the native reward mode
 
 
 
11
  - reinforcement-learning
12
  ---
13
 
14
+ # PM-Ops: Procedural PM Operations RL Environment
15
 
16
+ A simulated software-org environment β€” Ticketing, Chat, Codebase β€” designed to train LLM agents that learn *organizational conventions* rather than memorize fixed answers.
 
17
 
18
+ **Hosted env:** `https://huggingface.co/spaces/TheCrustaceans/Pm-ops`
19
+ **Trained checkpoint:** `Saurav1/pm-ops-grpo-Qwen3-1.7B-triage-v4`
20
+ **Stack:** Qwen3-1.7B Β· Unsloth Β· TRL 0.22.2 Β· SFT β†’ GRPO
21
 
22
+ ---
23
+
24
+ ## The Core Idea
25
+
26
+ Every episode samples a fresh `OrgConfig` β€” different label names, priority schemes, team-to-service mappings, and oncall channels. An agent that hardcodes `label="bug"` or `priority="P1"` will fail most episodes. A trained agent learns to **read the runbook first**, then use exactly what it says.
27
+
28
+ ```
29
+ Episode A Episode B
30
+ ────────────────────────────── ──────────────────────────────
31
+ labels: issue, perf, docs labels: defect, vuln, guide
32
+ priority: critical/high/low priority: P0/P1/P2/P3
33
+ team: payments β†’ data team: payments β†’ backend
34
+ channel: #data-alerts channel: #backend-oncall
35
+ ```
36
+
37
+ The same hardcoded action scores 1.0 in Episode A and 0.0 in Episode B. This is the learning signal.
38
+
39
+ ---
40
 
41
  ## Tasks
42
 
43
+ | Task | What the agent must do | Max Steps |
44
+ |---|---|---|
45
+ | **triage** | File a bug ticket with the correct org label + priority, assign to the owning team, page the oncall channel | 25 |
46
+ | **incident_routing** | Identify the commit that broke a service, page the right oncall channel, optionally ticket it | 25 |
47
+ | **release_notes** | Compile resolved tickets into release notes in the org's style (terse / verbose / bulleted) and post them | 40 |
48
+ | **dep_update** | Coordinate a dependency update across all teams that own affected services | 40 |
49
+
50
+ ---
51
+
52
+ ## Scoring
53
+
54
+ Reward is **delayed** to episode end via a deterministic Python verifier β€” no LLM-as-judge.
55
+
56
+ ### Triage (example)
57
+ ```
58
+ +0.25 ticket created
59
+ +0.20 correct label (must match label_taxonomy)
60
+ +0.20 correct priority (must match priority_levels)
61
+ +0.20 assigned to correct team (via team_map)
62
+ +0.10 message posted to correct oncall channel
63
+ +0.05 message body β‰₯ 20 chars
64
+ ─0.10 per duplicate ticket
65
+ ─0.05 per message posted to a noise channel
66
+ ```
67
+
68
+ ### GRPO Training Reward (runbook-compliance)
69
+
70
+ During RL training we add a **dense shaped reward** on top of the env score:
71
+
72
+ | Signal | Reward |
73
+ |---|---|
74
+ | Called `meta.read_runbook` | +0.10 |
75
+ | Ticket label in `label_taxonomy` | +0.20 / βˆ’0.10 |
76
+ | Ticket priority in `priority_levels` | +0.15 / βˆ’0.10 |
77
+ | Team in `team_map` | +0.20 / βˆ’0.10 |
78
+ | Posted to `oncall_channels` | +0.25 / βˆ’0.10 per wrong channel |
79
+ | Env grader score | Γ—0.10 bonus |
80
+
81
+ Reward naturally varies per seed because every org has different valid values β€” the model can't memorize answers.
82
+
83
+ ---
84
+
85
+ ## Action Space
86
+
87
+ The agent emits one JSON block per step:
88
+
89
+ ```json
90
+ {"action_type": "ticketing.create_ticket", "args": {"summary": "...", "label": "issue", "priority": "high"}}
91
+ ```
92
+
93
+ | Namespace | Actions |
94
+ |---|---|
95
+ | `meta` | `read_runbook` Β· `finish` Β· `noop` |
96
+ | `ticketing` | `create_ticket` Β· `update_ticket` Β· `get_ticket` Β· `list_tickets` Β· `assign_ticket` Β· `comment_ticket` Β· `transition_ticket` |
97
+ | `chat` | `post_message` Β· `read_channel` Β· `list_channels` Β· `search` |
98
+ | `codebase` | `list_commits` Β· `get_commit` Β· `list_prs` |
99
+
100
+ The optimal triage sequence takes **5 steps**:
101
+ ```
102
+ meta.read_runbook β†’ ticketing.create_ticket β†’ ticketing.assign_ticket β†’ chat.post_message β†’ meta.finish
103
+ ```
104
+
105
+ ---
106
+
107
+ ## Procedural World Generation
108
+
109
+ Each episode is fully seeded β€” same seed always produces the same org + scenario.
110
+
111
+ ```python
112
+ # org_generator.py
113
+ rng = random.Random(seed)
114
+ difficulty = rng.choice(["easy", "medium", "medium", "hard"])
115
+ label_taxonomy = {k: rng.choice(variants[k]) for k in sampled_keys}
116
+ priority_levels = rng.choice([["P0","P1","P2","P3"], ["critical","high","medium","low"], ...])
117
+ team_map = {service: team for service, team in zip(services, teams)}
118
+ oncall_channels = {svc: f"#{team_map[svc]}{rng.choice(suffixes)}" for svc in services}
119
+ ```
120
+
121
+ **Difficulty** controls the number of services (2 / 3 / 5), labels (3 / 4 / 5), noise channels (1 / 2 / 3), and required ticket fields.
122
+
123
+ ---
124
+
125
+ ## Training Pipeline
126
 
127
+ ### Phase 1 β€” SFT Warmup (~15 min on A100)
128
 
129
+ Run the deterministic `baseline_agent` for N episodes and collect every `(observation, action)` pair as a supervised example. Without this, the model outputs freeform text β†’ all GRPO advantages are zero.
130
 
131
+ ```
132
+ baseline_agent: read_runbook β†’ create_ticket β†’ assign_ticket β†’ list_channels β†’ post_message β†’ finish
133
+ SFT target: prompt + ```json\n{"action_type": ..., "args": {...}}\n``` + EOS
134
+ ```
 
135
 
136
+ After 2 epochs, the model reliably emits `\`\`\`json ... \`\`\`` blocks.
137
 
138
+ ### Phase 2 β€” GRPO (~90 min on A100)
139
+
140
+ GRPO learns by comparing rewards across N generations of the same prompt. Each generation gets a different env seed (`gen_slot` offset), so they explore distinct org configs and produce varied rewards β€” giving GRPO a real gradient signal.
141
+
142
+ ```
143
+ prompt (same seed) β†’ gen_slot=0 β†’ org_A β†’ reward=0.45
144
+ β†’ gen_slot=1 β†’ org_B β†’ reward=0.80
145
+ β†’ gen_slot=2 β†’ org_C β†’ reward=0.10
146
+ advantage = reward - mean(group) β†’ non-zero gradient
147
+ ```
148
+
149
+ **Critical: the dataset is pre-filtered.** `dataset.py` simulates the env's RNG to only include seeds where `env.reset(seed)` will run a *triage* task β€” eliminating the 75% of seeds that would produce release_notes or dep_update episodes and always score 0.
150
+
151
+ ---
152
+
153
+ ## Repository Layout
154
+
155
+ ```
156
+ β”œβ”€β”€ server/
157
+ β”‚ β”œβ”€β”€ pm_ops_environment.py # OpenEnv Environment β€” reset/step/grade
158
+ β”‚ β”œβ”€β”€ apps/
159
+ β”‚ β”‚ β”œβ”€β”€ ticketing.py # Jira-like app (create, assign, transition tickets)
160
+ β”‚ β”‚ β”œβ”€β”€ chat.py # Slack-like app (post, read, search channels)
161
+ β”‚ β”‚ └── codebase.py # GitHub-like app (commits, PRs)
162
+ β”‚ β”œβ”€β”€ tasks/
163
+ β”‚ β”‚ β”œβ”€β”€ triage_task.py # Grader: label + priority + team + channel
164
+ β”‚ β”‚ β”œβ”€β”€ incident_routing_task.py
165
+ β”‚ β”‚ β”œβ”€β”€ release_notes_task.py
166
+ β”‚ β”‚ └── dep_update_task.py
167
+ β”‚ └── world/
168
+ β”‚ β”œβ”€β”€ org_generator.py # Procedural OrgConfig from seed
169
+ β”‚ └── scenario_gen.py # Task brief + expected solution from seed
170
+ β”‚
171
+ β”œβ”€β”€ training/
172
+ β”‚ β”œβ”€β”€ train_v4.ipynb # ← run this (current clean notebook)
173
+ β”‚ β”œβ”€β”€ dataset.py # Pre-filtered triage dataset (seed-matched to env)
174
+ β”‚ β”œβ”€β”€ rollout.py # Multi-turn episode runner + JSON extractor
175
+ β”‚ β”œβ”€β”€ rewards.py # compute_rollout_reward (runbook-compliance)
176
+ β”‚ β”œβ”€β”€ prompts.py # System prompt + observation formatter
177
+ β”‚ └── pm_ops_trainer.py # PMOpsGRPOTrainer β€” reward cache injection
178
+ β”‚
179
+ β”œβ”€β”€ inference.py # Deterministic baseline_agent (heuristic)
180
+ β”œβ”€β”€ models.py # Pydantic types: PMOpsAction, PMOpsObservation
181
+ β”œβ”€β”€ client.py # OpenEnv client helpers
182
+ └── Dockerfile # HF Space deployment
183
+ ```
184
+
185
+ ---
186
+
187
+ ## Quick Start
188
+
189
+ ### Run the env locally
190
+
191
+ ```bash
192
+ pip install -e .
193
+ uvicorn server.app:app --host 0.0.0.0 --port 8000
194
+ ```
195
+
196
+ ### Run the baseline agent
197
+
198
+ ```bash
199
+ python inference.py
200
+ # [END] success=True steps=6 score=0.75 ...
201
+ ```
202
+
203
+ ### Connect a custom agent
204
+
205
+ ```python
206
+ from openenv.core import GenericEnvClient
207
+
208
+ with GenericEnvClient(base_url="http://localhost:8000").sync() as env:
209
+ result = env.reset(seed=42)
210
+ obs = result.observation
211
+ result = env.step({"action_type": "meta.read_runbook", "args": {}})
212
+ # ... agent loop ...
213
+ result = env.step({"action_type": "meta.finish", "args": {}})
214
+ print(f"score = {result.reward:.3f}")
215
+ ```
216
+
217
+ ### Train (cloud β€” A100 or T4)
218
+
219
+ Open `training/train_v4.ipynb` on your cloud runtime, run top to bottom. The notebook is self-contained: installs deps, clones the HF Space, runs SFT then GRPO, saves merged 16-bit weights to HF Hub.
220
+
221
+ Key hyperparameters auto-adapt to GPU:
222
+
223
+ | | A100 (40 GB) | T4 (16 GB) |
224
+ |---|---|---|
225
+ | `NUM_GEN` | 6 | 2 |
226
+ | `GRAD_ACCUM` | 32 | 8 |
227
+ | `N_SFT_EPISODES` | 120 | 50 |
228
+ | `N_GRPO_EPISODES` | 150 | 30 |
229
+ | `MAX_COMP_LEN` | 192 | 192 |
230
+
231
+ ---
232
+
233
+ ## Key Design Decisions
234
+
235
+ **Why SFT before GRPO?**
236
+ GRPO computes `advantage = reward οΏ½οΏ½ mean(group)`. If the model outputs garbage on every step, all rewards are ~0 and all advantages are 0 β†’ zero gradient. SFT teaches the JSON format first so GRPO has something to improve.
237
+
238
+ **Why runbook-compliance reward instead of env score?**
239
+ The env grader requires a precise 5-step sequence with *exact* field values (label, priority, team, channel). Early in training the model rarely completes the full sequence, so env score is 0 for nearly every episode β€” zero variance, zero GRPO signal. The compliance reward fires on individual correct actions and creates variance even from partial completions.
240
+
241
+ **Why filter training seeds to triage-only?**
242
+ `PMOpsEnvironment.reset(seed)` draws task type uniformly from 4 options. Without filtering, 75% of training prompts describe a triage task but the env runs release_notes or dep_update β†’ guaranteed env score = 0 regardless of what the model does. The dataset pre-simulates the env's RNG to only include seeds where `task_type == "triage"`.
243
+
244
+ **Why `gen_slot` offsets?**
245
+ GRPO needs reward *variance across generations of the same prompt*. Without the offset, all N generations of the same prompt reset the env with the same seed β†’ same org config β†’ same correct answers β†’ potentially same reward β†’ zero advantage. `gen_slot=k` adds `k` to the seed so generation k explores a slightly different org.
246
+
247
+ **Why `PMOpsGRPOTrainer`?**
248
+ TRL's GRPOTrainer builds `reward_kwargs` from dataset columns only. Our multi-turn rollout reward (computed inside `rollout_func`) never reaches `_calculate_rewards` through the normal kwargs path. `PMOpsGRPOTrainer` wraps `rollout_func` to cache rewards and injects them directly as a tensor, bypassing TRL's broken plumbing.
249
+
250
+ ---
251
 
252
+ ## Sponsor Alignment
253
 
254
+ | Sponsor | Relevance |
255
+ |---|---|
256
+ | **Meta RFC 004** | Delayed trajectory reward is the native reward mode |
257
+ | **Patronus** | Schema drift β€” tests whether agents track per-org convention changes |
258
+ | **Scale AI** | Enterprise workflow simulation with procedurally generated organizations |
259
+ | **AI Labs** | Direct PM-in-the-loop positioning for agentic systems |