SavK1 commited on
Commit
5bce7ac
·
1 Parent(s): 6c48c5d

writing the GRPO script to train qwen1.7 on A100 GRPO

Browse files
training/__init__.py ADDED
File without changes
training/dataset.py ADDED
@@ -0,0 +1,78 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Generate fixed-seed training dataset for PM-Ops triage task.
2
+
3
+ Each row contains a seed embedded in the prompt string so the rollout
4
+ function can pass it to env.reset(seed=...) for reproducible episodes.
5
+ """
6
+ import json
7
+ import os
8
+ import random
9
+ import sys
10
+
11
+ sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
12
+
13
+ from server.world.org_generator import generate_org_config
14
+ from server.world.scenario_gen import generate_scenario
15
+
16
+ SEED_PREFIX = "SEED:"
17
+
18
+
19
+ def _valid_triage(scenario: dict) -> bool:
20
+ exp = scenario.get("expected", {})
21
+ return bool(exp.get("channel")) and bool(exp.get("team"))
22
+
23
+
24
+ def generate_triage_dataset(n_episodes: int = 150, base_seed: int = 42) -> list[dict]:
25
+ """Return list of dataset rows with embedded seeds."""
26
+ rng = random.Random(base_seed)
27
+ rows = []
28
+
29
+ while len(rows) < n_episodes:
30
+ seed = rng.randint(0, 2**31)
31
+ difficulty = rng.choice(["easy", "medium", "medium", "hard"])
32
+
33
+ org, scenario = None, None
34
+ for attempt in range(10):
35
+ org = generate_org_config(seed + attempt, difficulty)
36
+ scenario = generate_scenario("triage", org, seed + attempt)
37
+ if _valid_triage(scenario):
38
+ break
39
+
40
+ if not _valid_triage(scenario):
41
+ continue
42
+
43
+ rows.append({
44
+ # Seed embedded so rollout can parse it and pass to reset()
45
+ "prompt": f"{SEED_PREFIX}{seed} | {scenario['brief']}",
46
+ "seed": seed,
47
+ "difficulty": difficulty,
48
+ })
49
+
50
+ return rows
51
+
52
+
53
+ def parse_seed_from_prompt(prompt: str) -> int | None:
54
+ """Extract seed embedded by generate_triage_dataset."""
55
+ if not prompt.startswith(SEED_PREFIX):
56
+ return None
57
+ try:
58
+ return int(prompt[len(SEED_PREFIX):].split(" | ")[0])
59
+ except ValueError:
60
+ return None
61
+
62
+
63
+ def save_dataset(rows: list[dict], path: str) -> None:
64
+ with open(path, "w") as f:
65
+ for row in rows:
66
+ f.write(json.dumps(row) + "\n")
67
+ print(f"Saved {len(rows)} episodes → {path}")
68
+
69
+
70
+ def load_dataset(path: str) -> list[dict]:
71
+ with open(path) as f:
72
+ return [json.loads(line) for line in f]
73
+
74
+
75
+ if __name__ == "__main__":
76
+ out = os.path.join(os.path.dirname(__file__), "triage_dataset.jsonl")
77
+ rows = generate_triage_dataset(n_episodes=150)
78
+ save_dataset(rows, out)
training/prompts.py ADDED
@@ -0,0 +1,76 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """System prompt and observation formatting for PM-Ops GRPO training."""
2
+
3
+ SYSTEM_PROMPT = """You are an expert PM Operations agent inside a software organization.
4
+
5
+ ## Your Tools
6
+ - **Ticketing** (Jira-like): create_ticket, update_ticket, assign_ticket, transition_ticket, get_ticket, list_tickets, comment_ticket
7
+ - **Chat** (Slack-like): post_message, read_channel, list_channels, search
8
+ - **Codebase** (GitHub-like): list_commits, get_commit, list_prs
9
+ - **Meta**: read_runbook, finish, noop
10
+
11
+ ## CRITICAL — Read the Runbook First
12
+ Every organization uses different conventions. Your FIRST action MUST be `meta.read_runbook`.
13
+ The runbook tells you the EXACT values to use:
14
+ - `label_taxonomy`: valid ticket labels (e.g. "defect", NOT "bug")
15
+ - `priority_levels`: valid priorities (e.g. "critical", NOT "P1")
16
+ - `team_map`: service → owning team
17
+ - `oncall_channels`: service → channel to notify
18
+
19
+ ## Action Format
20
+ Reason through the problem, then emit exactly ONE JSON code block as your final output:
21
+
22
+ ```json
23
+ {"action_type": "meta.read_runbook", "args": {}}
24
+ ```
25
+
26
+ Full list of valid action_type values:
27
+ meta.read_runbook | meta.finish | meta.noop
28
+ ticketing.create_ticket | ticketing.update_ticket | ticketing.get_ticket
29
+ ticketing.list_tickets | ticketing.assign_ticket | ticketing.comment_ticket
30
+ ticketing.transition_ticket
31
+ codebase.list_commits | codebase.get_commit | codebase.list_prs
32
+ chat.post_message | chat.read_channel | chat.list_channels | chat.search
33
+
34
+ ## Triage Strategy (follow this order)
35
+ 1. `meta.read_runbook` — learn this org's label, priority, team, and channel conventions
36
+ 2. `ticketing.create_ticket` — use EXACT label from label_taxonomy, EXACT priority from priority_levels
37
+ 3. `ticketing.assign_ticket` — assign to the team that owns the affected service (team_map)
38
+ 4. `chat.post_message` — post to the oncall channel for that service (oncall_channels)
39
+ 5. `meta.finish` — end the episode
40
+
41
+ ## Rules
42
+ - NEVER guess label or priority values — use only what the runbook tells you
43
+ - NEVER post to noise channels (#random, #general, #water-cooler, etc.)
44
+ - ONLY post to the oncall channel for the affected service
45
+ - Call `meta.finish` when done — do not exceed steps unnecessarily
46
+ """
47
+
48
+
49
+ def format_observation(obs, step: int, last_action_result: dict | None = None) -> str:
50
+ """Convert a PMOpsObservation into a user-facing message string."""
51
+ if hasattr(obs, "task_brief"):
52
+ task_brief = obs.task_brief
53
+ steps_remaining = obs.steps_remaining
54
+ result = obs.last_action_result
55
+ else:
56
+ task_brief = obs.get("task_brief", "")
57
+ steps_remaining = obs.get("steps_remaining", 40)
58
+ result = obs.get("last_action_result", {})
59
+
60
+ if last_action_result is not None:
61
+ result = last_action_result
62
+
63
+ parts = [f"## Task\n{task_brief}", f"\n**Step {step}** | {steps_remaining} steps remaining"]
64
+
65
+ if result:
66
+ ok = result.get("ok", False)
67
+ if ok:
68
+ data = result.get("data", "")
69
+ data_str = str(data)[:800]
70
+ parts.append(f"\n**Result (success):**\n{data_str}")
71
+ else:
72
+ error = result.get("error", "Unknown error")
73
+ parts.append(f"\n**Result (error):** {error}")
74
+
75
+ parts.append("\nWhat is your next action? Think it through, then output a JSON code block.")
76
+ return "\n".join(parts)
training/rewards.py ADDED
@@ -0,0 +1,66 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Reward functions for PM-Ops GRPO training.
2
+
3
+ Weights are applied inside each function so GRPOTrainer sums them to a
4
+ total reward in [0, 1] range. Weights must sum to 1.0:
5
+
6
+ final_score 0.50 — correctness: label + priority + team + channel
7
+ valid_json 0.20 — format discipline: fraction of valid JSON outputs
8
+ read_runbook 0.15 — process: did agent read runbook before acting?
9
+ efficiency 0.15 — speed: steps saved when task completed correctly
10
+
11
+ To change weights, edit the multipliers below without touching the rollout.
12
+ To switch to Option B weighting (equal weights), set all multipliers to 0.25.
13
+ """
14
+
15
+ WEIGHT_FINAL_SCORE = 0.50
16
+ WEIGHT_VALID_JSON = 0.20
17
+ WEIGHT_READ_RUNBOOK = 0.15
18
+ WEIGHT_EFFICIENCY = 0.15
19
+
20
+
21
+ def _extract(kwargs: dict, key: str, n: int) -> list[float]:
22
+ rewards = kwargs.get(key, [])
23
+ return [float(r) for r in rewards] if rewards else [0.0] * n
24
+
25
+
26
+ def reward_final_score(completions, **kwargs) -> list[float]:
27
+ """Primary correctness signal from the env grader (0–1). Weight: 0.50"""
28
+ raw = _extract(kwargs, "final_score_reward", len(completions))
29
+ return [r * WEIGHT_FINAL_SCORE for r in raw]
30
+
31
+
32
+ def reward_valid_json(completions, **kwargs) -> list[float]:
33
+ """Fraction of steps with parseable JSON output (0–1). Weight: 0.20
34
+
35
+ Anti-staleness: penalises the 'output garbage every step' failure mode
36
+ that naive meta.noop fallback can allow.
37
+ """
38
+ raw = _extract(kwargs, "valid_json_reward", len(completions))
39
+ return [r * WEIGHT_VALID_JSON for r in raw]
40
+
41
+
42
+ def reward_read_runbook(completions, **kwargs) -> list[float]:
43
+ """Binary: did agent call meta.read_runbook before acting? Weight: 0.15
44
+
45
+ Teaches the agent to read org conventions first instead of guessing.
46
+ """
47
+ raw = _extract(kwargs, "read_runbook_reward", len(completions))
48
+ return [r * WEIGHT_READ_RUNBOOK for r in raw]
49
+
50
+
51
+ def reward_efficiency(completions, **kwargs) -> list[float]:
52
+ """Steps saved when task was completed correctly (0–1). Weight: 0.15
53
+
54
+ Only non-zero when final_score >= 0.3, so it doesn't reward fast failure.
55
+ """
56
+ raw = _extract(kwargs, "efficiency_reward", len(completions))
57
+ return [r * WEIGHT_EFFICIENCY for r in raw]
58
+
59
+
60
+ # Convenience list for GRPOTrainer(reward_funcs=...)
61
+ ALL_REWARD_FUNCS = [
62
+ reward_final_score,
63
+ reward_valid_json,
64
+ reward_read_runbook,
65
+ reward_efficiency,
66
+ ]
training/rollout.py ADDED
@@ -0,0 +1,287 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Multi-turn rollout function for PM-Ops GRPO training.
2
+
3
+ History design (no duplicates):
4
+ Each turn record stores the obs that TRIGGERED the completion, not the result.
5
+ build_messages reconstructs:
6
+ [sys] [user:obs_0] [asst:comp_0] [user:obs_1] [asst:comp_1] ... [user:current_obs]
7
+ current_obs is never in history — it becomes the final user message.
8
+
9
+ Other design decisions:
10
+ - Action format B: chain-of-thought reasoning + ```json block
11
+ - Truncation: runbook-pinned sliding window (runbook pair always kept)
12
+ - Fallback cascade: read_runbook (early) → noop (mid) → finish (late)
13
+ - Three-pass JSON extraction: code block → raw JSON → regex
14
+ """
15
+ import json
16
+ import re
17
+ from typing import Any
18
+
19
+ from trl.experimental.openenv import generate_rollout_completions
20
+
21
+ from training.dataset import parse_seed_from_prompt
22
+ from training.prompts import SYSTEM_PROMPT, format_observation
23
+
24
+ MAX_STEPS = 40
25
+ # ~3000 tokens at 4 chars/token; leaves room for completion tokens
26
+ MAX_PROMPT_CHARS = 12_000
27
+ RUNBOOK_RESPONSE_MAX_CHARS = 3_000
28
+ HISTORY_PAIRS = 6 # max (user+asst) pairs kept from non-runbook history
29
+
30
+
31
+ # ---------------------------------------------------------------------------
32
+ # JSON extraction — three-pass, most-specific first
33
+ # ---------------------------------------------------------------------------
34
+
35
+ def extract_json_action(text: str) -> dict | None:
36
+ # Pass 1: last ```json ... ``` block
37
+ matches = re.findall(r"```json\s*(\{.*?\})\s*```", text, re.DOTALL)
38
+ if matches:
39
+ try:
40
+ return json.loads(matches[-1])
41
+ except json.JSONDecodeError:
42
+ pass
43
+
44
+ # Pass 2: entire output is JSON
45
+ stripped = text.strip()
46
+ if stripped.startswith("{"):
47
+ try:
48
+ return json.loads(stripped)
49
+ except json.JSONDecodeError:
50
+ pass
51
+
52
+ # Pass 3: any {...action_type...} pattern — allow one level of nested {} (e.g. "args": {})
53
+ matches = re.findall(
54
+ r'\{(?:[^{}]|\{[^{}]*\})*"action_type"\s*:\s*"[^"]*"(?:[^{}]|\{[^{}]*\})*\}',
55
+ text, re.DOTALL,
56
+ )
57
+ if matches:
58
+ try:
59
+ return json.loads(matches[-1])
60
+ except json.JSONDecodeError:
61
+ pass
62
+
63
+ return None
64
+
65
+
66
+ def step_aware_fallback(step: int) -> dict:
67
+ """Safe fallback that degrades gracefully across the episode."""
68
+ if step <= 1:
69
+ return {"action_type": "meta.read_runbook", "args": {}}
70
+ elif step >= MAX_STEPS - 3:
71
+ return {"action_type": "meta.finish", "args": {}}
72
+ return {"action_type": "meta.noop", "args": {}}
73
+
74
+
75
+ # ---------------------------------------------------------------------------
76
+ # Context builder — runbook-pinned sliding window, no duplicate user turns
77
+ # ---------------------------------------------------------------------------
78
+
79
+ def _chars(messages: list[dict]) -> int:
80
+ return sum(len(m["content"]) for m in messages)
81
+
82
+
83
+ def build_messages(
84
+ turn_history: list[dict],
85
+ current_obs_text: str,
86
+ ) -> list[dict]:
87
+ """Build prompt messages with runbook-pinned sliding window truncation.
88
+
89
+ turn_history entries: {"obs_text": str, "completion": str, "is_runbook": bool}
90
+ obs_text = observation that triggered this completion (user side)
91
+ completion = model output for that step (assistant side)
92
+
93
+ Final conversation shape:
94
+ [sys] [user:obs_0][asst:comp_0] ... [user:obs_k][asst:comp_k] [user:current_obs]
95
+ No entry in turn_history represents current_obs — it's only the final user turn.
96
+ """
97
+ # Task brief is always prepended to current_obs so it stays visible even
98
+ # when old context is truncated.
99
+ messages: list[dict] = [{"role": "system", "content": SYSTEM_PROMPT}]
100
+
101
+ # Separate the runbook exchange from the rest
102
+ runbook_turn: dict | None = None
103
+ general: list[dict] = []
104
+
105
+ for turn in turn_history:
106
+ if turn["is_runbook"] and runbook_turn is None:
107
+ runbook_turn = turn
108
+ else:
109
+ general.append(turn)
110
+
111
+ # Pin runbook exchange (truncate only if absurdly large)
112
+ if runbook_turn is not None:
113
+ rb_comp = runbook_turn["completion"][:RUNBOOK_RESPONSE_MAX_CHARS]
114
+ messages.append({"role": "user", "content": runbook_turn["obs_text"]})
115
+ messages.append({"role": "assistant", "content": rb_comp})
116
+
117
+ # Fill budget with most-recent general turns (newest first, then reverse)
118
+ fixed_chars = _chars(messages) + len(current_obs_text)
119
+ budget = MAX_PROMPT_CHARS - fixed_chars
120
+ window: list[dict] = []
121
+
122
+ for turn in reversed(general[-HISTORY_PAIRS:]):
123
+ pair_chars = len(turn["obs_text"]) + len(turn["completion"])
124
+ if budget - pair_chars < 0:
125
+ break
126
+ window.append(turn)
127
+ budget -= pair_chars
128
+
129
+ for turn in reversed(window):
130
+ messages.append({"role": "user", "content": turn["obs_text"]})
131
+ messages.append({"role": "assistant", "content": turn["completion"]})
132
+
133
+ # Current observation is always the final user turn (never stored in history)
134
+ messages.append({"role": "user", "content": current_obs_text})
135
+ return messages
136
+
137
+
138
+ # ---------------------------------------------------------------------------
139
+ # Observation normaliser — handles both object and dict forms
140
+ # ---------------------------------------------------------------------------
141
+
142
+ def _obs_to_dict(obs: Any) -> dict:
143
+ if isinstance(obs, dict):
144
+ return obs
145
+ fields = ("task_brief", "last_action_result", "step", "steps_remaining",
146
+ "reward", "done", "token_budget_remaining")
147
+ return {f: getattr(obs, f, None) for f in fields}
148
+
149
+
150
+ def _current_obs_text(obs_dict: dict, step: int, task_brief: str) -> str:
151
+ """Format observation, prepending a task reminder so it survives truncation."""
152
+ reminder = f"**Task reminder:** {task_brief[:200]}\n\n"
153
+ return reminder + format_observation(obs_dict, step, obs_dict.get("last_action_result"))
154
+
155
+
156
+ # ---------------------------------------------------------------------------
157
+ # Single-episode rollout
158
+ # ---------------------------------------------------------------------------
159
+
160
+ def rollout_once(
161
+ trainer,
162
+ sync_env,
163
+ tokenizer,
164
+ dataset_prompt: str,
165
+ ) -> dict:
166
+ """Play one full PM-Ops episode. Returns trajectory + reward signals."""
167
+
168
+ seed = parse_seed_from_prompt(dataset_prompt)
169
+ result = sync_env.reset(seed=seed) if seed is not None else sync_env.reset()
170
+
171
+ obs = result.observation if hasattr(result, "observation") else result
172
+ obs_dict = _obs_to_dict(obs)
173
+ task_brief: str = obs_dict.get("task_brief") or dataset_prompt
174
+
175
+ # Flat trajectory buffers (TRL expects flat lists across all steps)
176
+ prompt_ids: list = []
177
+ completion_ids: list = []
178
+ logprobs: list = []
179
+
180
+ # Turn history for context building
181
+ # Each entry: {"obs_text": str, "completion": str, "is_runbook": bool}
182
+ turn_history: list[dict] = []
183
+
184
+ # Reward accumulators
185
+ valid_action_count = 0
186
+ read_runbook_done = False
187
+ final_score = 0.0
188
+ step = 0
189
+ done = False
190
+
191
+ while not done and step < MAX_STEPS:
192
+ # obs_text for THIS step — stored in history BEFORE stepping
193
+ obs_text = _current_obs_text(obs_dict, step, task_brief)
194
+
195
+ messages = build_messages(turn_history, obs_text)
196
+ prompt_text = tokenizer.apply_chat_template(
197
+ messages,
198
+ add_generation_prompt=True,
199
+ tokenize=False,
200
+ enable_thinking=False,
201
+ )
202
+
203
+ rollout_out = generate_rollout_completions(trainer, [prompt_text])[0]
204
+ prompt_ids.extend(rollout_out["prompt_ids"])
205
+ completion_ids.extend(rollout_out["completion_ids"])
206
+ logprobs.extend(rollout_out["logprobs"])
207
+
208
+ completion_text = rollout_out.get("text") or tokenizer.decode(
209
+ rollout_out["completion_ids"], skip_special_tokens=True
210
+ )
211
+
212
+ # Parse action; fall back gracefully on parse failure
213
+ parsed = extract_json_action(completion_text)
214
+ is_valid_json = parsed is not None
215
+
216
+ if not is_valid_json:
217
+ parsed = step_aware_fallback(step)
218
+ else:
219
+ valid_action_count += 1
220
+
221
+ action_type: str = parsed.get("action_type", "meta.noop")
222
+ args: dict = parsed.get("args", {})
223
+
224
+ if action_type == "meta.read_runbook" and not read_runbook_done:
225
+ read_runbook_done = True
226
+
227
+ # Store the (obs_text, completion) pair BEFORE stepping the env
228
+ # is_runbook marks this turn for pinning in future context windows
229
+ turn_history.append({
230
+ "obs_text": obs_text,
231
+ "completion": completion_text,
232
+ "is_runbook": (action_type == "meta.read_runbook" and is_valid_json),
233
+ })
234
+
235
+ # Step the environment — obs_dict now holds the NEXT state
236
+ result = sync_env.step({"action_type": action_type, "args": args})
237
+ new_obs = result.observation if hasattr(result, "observation") else result
238
+ obs_dict = _obs_to_dict(new_obs)
239
+
240
+ done = bool(getattr(result, "done", obs_dict.get("done", False)))
241
+ final_score = float(getattr(result, "reward", obs_dict.get("reward", 0.0)))
242
+ step += 1
243
+
244
+ # Auxiliary reward signals
245
+ valid_json_ratio = valid_action_count / max(step, 1)
246
+ efficiency = max(0.0, 1.0 - step / MAX_STEPS) if final_score >= 0.3 else 0.0
247
+
248
+ return {
249
+ "prompt_ids": prompt_ids,
250
+ "completion_ids": completion_ids,
251
+ "logprobs": logprobs,
252
+ "final_score_reward": final_score,
253
+ "valid_json_reward": valid_json_ratio,
254
+ "read_runbook_reward": 1.0 if read_runbook_done else 0.0,
255
+ "efficiency_reward": efficiency,
256
+ }
257
+
258
+
259
+ # ---------------------------------------------------------------------------
260
+ # GRPOTrainer-compatible rollout function (factory)
261
+ # ---------------------------------------------------------------------------
262
+
263
+ def make_rollout_func(sync_env, tokenizer):
264
+ """Bind env + tokenizer; return the function GRPOTrainer calls each batch."""
265
+
266
+ def rollout_func(prompts: list[str], trainer=None) -> dict:
267
+ out: dict[str, list] = {
268
+ "prompt_ids": [],
269
+ "completion_ids": [],
270
+ "logprobs": [],
271
+ "final_score_reward": [],
272
+ "valid_json_reward": [],
273
+ "read_runbook_reward": [],
274
+ "efficiency_reward": [],
275
+ }
276
+ for prompt_text in prompts:
277
+ episode = rollout_once(
278
+ trainer=trainer,
279
+ sync_env=sync_env,
280
+ tokenizer=tokenizer,
281
+ dataset_prompt=prompt_text,
282
+ )
283
+ for k in out:
284
+ out[k].append(episode[k])
285
+ return out
286
+
287
+ return rollout_func
training/smoke_test.py ADDED
@@ -0,0 +1,246 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Smoke test — runs locally without TRL/vLLM.
2
+
3
+ Tests in order:
4
+ 1. Dataset generation
5
+ 2. JSON extraction (edge cases)
6
+ 3. build_messages structure (no duplicates, correct alternation)
7
+ 4. Server startup + env connection
8
+ 5. One full heuristic episode (env API end-to-end)
9
+
10
+ Run from repo root: python training/smoke_test.py
11
+ """
12
+ import json
13
+ import os
14
+ import subprocess
15
+ import sys
16
+ import time
17
+
18
+ import requests
19
+
20
+ sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
21
+
22
+ PASS = "\033[92m[PASS]\033[0m"
23
+ FAIL = "\033[91m[FAIL]\033[0m"
24
+ INFO = "\033[94m[INFO]\033[0m"
25
+
26
+
27
+ def check(label: str, cond: bool, detail: str = "") -> bool:
28
+ if cond:
29
+ print(f"{PASS} {label}")
30
+ else:
31
+ print(f"{FAIL} {label}" + (f" — {detail}" if detail else ""))
32
+ return cond
33
+
34
+
35
+ # ---------------------------------------------------------------------------
36
+ # 1. Dataset
37
+ # ---------------------------------------------------------------------------
38
+ print("\n── 1. Dataset ──────────────────────────────────────────────────────")
39
+ from training.dataset import generate_triage_dataset, parse_seed_from_prompt
40
+
41
+ rows = generate_triage_dataset(n_episodes=5, base_seed=99)
42
+ check("generates 5 rows", len(rows) == 5)
43
+ check("each row has prompt+seed+difficulty", all(
44
+ "prompt" in r and "seed" in r and "difficulty" in r for r in rows
45
+ ))
46
+ seed_back = parse_seed_from_prompt(rows[0]["prompt"])
47
+ check("seed round-trips through prompt string", seed_back == rows[0]["seed"],
48
+ f"got {seed_back}, expected {rows[0]['seed']}")
49
+ print(f" sample prompt: {rows[0]['prompt'][:100]}...")
50
+
51
+
52
+ # ---------------------------------------------------------------------------
53
+ # 2. JSON extraction
54
+ # ---------------------------------------------------------------------------
55
+ print("\n── 2. JSON extraction ──────────────────────────────────────────────")
56
+ from training.rollout import extract_json_action, step_aware_fallback
57
+
58
+ cases = [
59
+ # (description, input_text, expected_action_type)
60
+ ("code block", 'Thinking...\n```json\n{"action_type": "meta.read_runbook", "args": {}}\n```', "meta.read_runbook"),
61
+ ("raw JSON", '{"action_type": "meta.finish", "args": {}}', "meta.finish"),
62
+ ("regex hit", 'I will do this: {"action_type": "meta.noop", "args": {}} done.', "meta.noop"),
63
+ ("no JSON", "I cannot decide.", None),
64
+ ("bad block", '```json\n{broken\n```', None),
65
+ ]
66
+ for desc, text, expected in cases:
67
+ result = extract_json_action(text)
68
+ got = result.get("action_type") if result else None
69
+ check(f"extract: {desc}", got == expected, f"got {got!r}, expected {expected!r}")
70
+
71
+ fb = step_aware_fallback(0)
72
+ check("fallback step 0 → read_runbook", fb["action_type"] == "meta.read_runbook")
73
+ fb = step_aware_fallback(20)
74
+ check("fallback step 20 → noop", fb["action_type"] == "meta.noop")
75
+ fb = step_aware_fallback(38)
76
+ check("fallback step 38 → finish", fb["action_type"] == "meta.finish")
77
+
78
+
79
+ # ---------------------------------------------------------------------------
80
+ # 3. build_messages structure
81
+ # ---------------------------------------------------------------------------
82
+ print("\n── 3. build_messages ───────────────────────────────────────────────")
83
+ from training.rollout import build_messages
84
+
85
+ # Empty history (step 0)
86
+ msgs = build_messages([], "First observation")
87
+ check("step 0: starts with system", msgs[0]["role"] == "system")
88
+ check("step 0: ends with user", msgs[-1]["role"] == "user")
89
+ check("step 0: final user is current obs", msgs[-1]["content"] == "First observation")
90
+
91
+ # Simulate 3 turns
92
+ history = [
93
+ {"obs_text": "obs_0", "completion": "comp_0", "is_runbook": True},
94
+ {"obs_text": "obs_1", "completion": "comp_1", "is_runbook": False},
95
+ {"obs_text": "obs_2", "completion": "comp_2", "is_runbook": False},
96
+ ]
97
+ msgs = build_messages(history, "current_obs")
98
+
99
+ # Validate alternation: after system, must be user/asst/user/asst/.../user
100
+ roles = [m["role"] for m in msgs]
101
+ check("roles start with system", roles[0] == "system")
102
+ check("roles end with user", roles[-1] == "user")
103
+ pairs_ok = all(
104
+ roles[i] == "user" and roles[i+1] == "assistant"
105
+ for i in range(1, len(roles) - 2, 2)
106
+ )
107
+ check("strict user/asst alternation throughout", pairs_ok, str(roles))
108
+
109
+ # Verify current_obs appears exactly once as the last message
110
+ final_user_content = msgs[-1]["content"]
111
+ check("current_obs is last message content", final_user_content == "current_obs")
112
+ all_contents = [m["content"] for m in msgs]
113
+ check("current_obs not duplicated", all_contents.count("current_obs") == 1)
114
+
115
+ # Verify runbook is pinned (obs_0/comp_0 should appear even with many turns)
116
+ all_content_str = " ".join(all_contents)
117
+ check("runbook obs pinned", "obs_0" in all_content_str)
118
+ check("runbook completion pinned", "comp_0" in all_content_str)
119
+
120
+
121
+ # ---------------------------------------------------------------------------
122
+ # 4. Server startup
123
+ # ---------------------------------------------------------------------------
124
+ print("\n── 4. Server startup ───────────────────────────────────────────────")
125
+ REPO_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
126
+ ENV_URL = "http://localhost:8765" # non-standard port to avoid conflicts
127
+
128
+ proc = subprocess.Popen(
129
+ [sys.executable, "-m", "uvicorn", "server.app:app",
130
+ "--host", "0.0.0.0", "--port", "8765"],
131
+ cwd=REPO_DIR,
132
+ stdout=subprocess.DEVNULL,
133
+ stderr=subprocess.DEVNULL,
134
+ )
135
+
136
+ ready = False
137
+ for _ in range(20):
138
+ try:
139
+ r = requests.get(f"{ENV_URL}/", timeout=1)
140
+ if r.status_code == 200:
141
+ ready = True
142
+ break
143
+ except Exception:
144
+ pass
145
+ time.sleep(1)
146
+
147
+ check("server started on :8765", ready)
148
+ if not ready:
149
+ proc.terminate()
150
+ print(f"{FAIL} Cannot continue without server.")
151
+ sys.exit(1)
152
+
153
+
154
+ # ---------------------------------------------------------------------------
155
+ # 5. Full heuristic episode (env API end-to-end)
156
+ # ---------------------------------------------------------------------------
157
+ print("\n── 5. End-to-end episode ───────────────────────────────────────────")
158
+ from openenv.core import GenericEnvClient
159
+
160
+ client = GenericEnvClient(base_url=ENV_URL).sync()
161
+ client.connect()
162
+
163
+ try:
164
+ # reset with explicit seed (tests seed passthrough)
165
+ res = client.reset(seed=rows[0]["seed"])
166
+ obs = res.observation if hasattr(res, "observation") else res
167
+
168
+ def _get(o, k):
169
+ return getattr(o, k, None) if not isinstance(o, dict) else o.get(k)
170
+
171
+ task_brief = _get(obs, "task_brief")
172
+ check("reset returns task_brief", bool(task_brief), repr(task_brief)[:80])
173
+
174
+ # Step 1: read runbook
175
+ res = client.step({"action_type": "meta.read_runbook", "args": {}})
176
+ obs = res.observation if hasattr(res, "observation") else res
177
+ last = _get(obs, "last_action_result") or {}
178
+ check("read_runbook ok=True", last.get("ok") is True)
179
+ check("runbook contains org_config", "org_config" in str(last.get("data", "")))
180
+
181
+ org = last.get("data", {}).get("org_config", {}) if isinstance(last.get("data"), dict) else {}
182
+ labels = list(org.get("label_taxonomy", {}).values())
183
+ priorities = org.get("priority_levels", [])
184
+ teams = list(org.get("team_map", {}).values())
185
+ channels = list(org.get("oncall_channels", {}).values())
186
+
187
+ check("org has labels", len(labels) > 0)
188
+ check("org has priorities", len(priorities) > 0)
189
+
190
+ # Step 2: create ticket with correct label/priority
191
+ res = client.step({
192
+ "action_type": "ticketing.create_ticket",
193
+ "args": {
194
+ "summary": f"Triage: {task_brief[:60]}",
195
+ "description": task_brief,
196
+ "label": labels[0],
197
+ "priority": priorities[1] if len(priorities) > 1 else priorities[0],
198
+ "assignee": teams[0] if teams else "backend",
199
+ },
200
+ })
201
+ obs = res.observation if hasattr(res, "observation") else res
202
+ last = _get(obs, "last_action_result") or {}
203
+ check("create_ticket ok=True", last.get("ok") is True, str(last.get("error")))
204
+ ticket_id = (last.get("data") or {}).get("id") if isinstance(last.get("data"), dict) else None
205
+ check("ticket_id returned", bool(ticket_id), repr(ticket_id))
206
+
207
+ # Step 3: assign ticket
208
+ if ticket_id and teams:
209
+ res = client.step({
210
+ "action_type": "ticketing.assign_ticket",
211
+ "args": {"ticket_id": ticket_id, "team": teams[0]},
212
+ })
213
+ obs = res.observation if hasattr(res, "observation") else res
214
+ last = _get(obs, "last_action_result") or {}
215
+ check("assign_ticket ok=True", last.get("ok") is True, str(last.get("error")))
216
+
217
+ # Step 4: post to oncall channel
218
+ if channels:
219
+ res = client.step({
220
+ "action_type": "chat.post_message",
221
+ "args": {"channel": channels[0], "text": f"Triage smoke test: {task_brief[:80]}"},
222
+ })
223
+ obs = res.observation if hasattr(res, "observation") else res
224
+ last = _get(obs, "last_action_result") or {}
225
+ check("post_message ok=True", last.get("ok") is True, str(last.get("error")))
226
+
227
+ # Step 5: finish + check reward
228
+ res = client.step({"action_type": "meta.finish", "args": {}})
229
+ done = getattr(res, "done", None)
230
+ reward = getattr(res, "reward", 0.0)
231
+ check("episode done after finish", done is True, f"done={done}")
232
+ check("reward is float in [0,1]", isinstance(reward, float) and 0.0 <= reward <= 1.0,
233
+ f"reward={reward}")
234
+ print(f" episode reward: {reward:.3f}")
235
+
236
+ finally:
237
+ client.close()
238
+
239
+ proc.terminate()
240
+
241
+ # ---------------------------------------------------------------------------
242
+ # Summary
243
+ # ---------------------------------------------------------------------------
244
+ print("\n────────────────────────────────────────────────────────────────────")
245
+ print("Smoke test complete. If all checks passed, training pipeline is ready.")
246
+ print("Next: push repo to HF and run training/train.ipynb on A100 GPU Space.")
training/train.ipynb ADDED
@@ -0,0 +1,639 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "cells": [
3
+ {
4
+ "cell_type": "markdown",
5
+ "metadata": {},
6
+ "source": [
7
+ "# PM-Ops: Train a Project Management Agent with GRPO\n",
8
+ "\n",
9
+ "Fine-tune **Qwen3-1.7B** to handle PM operations (triage, incident routing, release notes, dep updates)\n",
10
+ "using GRPO via TRL and the PM-Ops OpenEnv environment.\n",
11
+ "\n",
12
+ "**GPU:** A100 40GB (HF Space or Colab Pro) \n",
13
+ "**Time:** ~90 min (150 triage episodes, 1 epoch) \n",
14
+ "**Environment:** PM-Ops server runs locally on localhost:8000 — no network latency during training.\n",
15
+ "\n",
16
+ "### Architecture\n",
17
+ "```\n",
18
+ "[HF GPU Space / Colab A100]\n",
19
+ " ├── GRPOTrainer + Qwen3-1.7B ← uses HF credits / Colab GPU\n",
20
+ " └── PM-Ops FastAPI (localhost:8000) ← started as subprocess, <1ms latency\n",
21
+ "```"
22
+ ]
23
+ },
24
+ {
25
+ "cell_type": "markdown",
26
+ "metadata": {},
27
+ "source": [
28
+ "## 0. Install Dependencies"
29
+ ]
30
+ },
31
+ {
32
+ "cell_type": "code",
33
+ "execution_count": null,
34
+ "metadata": {},
35
+ "outputs": [],
36
+ "source": [
37
+ "!pip install -Uq \"trl>=0.17.0\" openenv-core transformers datasets accelerate vllm trackio\n",
38
+ "print('Dependencies installed.')"
39
+ ]
40
+ },
41
+ {
42
+ "cell_type": "markdown",
43
+ "metadata": {},
44
+ "source": [
45
+ "## 1. Clone PM-Ops Repo"
46
+ ]
47
+ },
48
+ {
49
+ "cell_type": "code",
50
+ "execution_count": null,
51
+ "metadata": {},
52
+ "outputs": [],
53
+ "source": [
54
+ "import os, sys\n",
55
+ "\n",
56
+ "REPO_URL = 'https://huggingface.co/spaces/adityaguntur/pm-ops' # update if repo moved\n",
57
+ "REPO_DIR = '/content/pm_ops'\n",
58
+ "\n",
59
+ "if not os.path.exists(REPO_DIR):\n",
60
+ " !git clone --depth=1 -q {REPO_URL} {REPO_DIR}\n",
61
+ " print(f'Cloned → {REPO_DIR}')\n",
62
+ "else:\n",
63
+ " print(f'Already exists: {REPO_DIR}')\n",
64
+ "\n",
65
+ "# Add repo root to Python path so imports work\n",
66
+ "for p in [REPO_DIR, os.path.join(REPO_DIR, 'training')]:\n",
67
+ " if p not in sys.path:\n",
68
+ " sys.path.insert(0, p)\n",
69
+ "\n",
70
+ "os.chdir(REPO_DIR)\n",
71
+ "print(f'Working directory: {os.getcwd()}')"
72
+ ]
73
+ },
74
+ {
75
+ "cell_type": "markdown",
76
+ "metadata": {},
77
+ "source": [
78
+ "## 2. HuggingFace Login"
79
+ ]
80
+ },
81
+ {
82
+ "cell_type": "code",
83
+ "execution_count": null,
84
+ "metadata": {},
85
+ "outputs": [],
86
+ "source": [
87
+ "from huggingface_hub import notebook_login\n",
88
+ "notebook_login()"
89
+ ]
90
+ },
91
+ {
92
+ "cell_type": "markdown",
93
+ "metadata": {},
94
+ "source": [
95
+ "## 3. Start PM-Ops Server Locally\n",
96
+ "\n",
97
+ "We run the environment server **on localhost** inside the same machine.\n",
98
+ "This eliminates network latency — each of the 40 steps per episode costs <1ms vs ~200ms over HF Spaces."
99
+ ]
100
+ },
101
+ {
102
+ "cell_type": "code",
103
+ "execution_count": null,
104
+ "metadata": {},
105
+ "outputs": [],
106
+ "source": [
107
+ "import subprocess, time, requests\n",
108
+ "\n",
109
+ "server_proc = subprocess.Popen(\n",
110
+ " [sys.executable, '-m', 'uvicorn', 'server.app:app',\n",
111
+ " '--host', '0.0.0.0', '--port', '8000'],\n",
112
+ " cwd=REPO_DIR,\n",
113
+ " stdout=subprocess.DEVNULL,\n",
114
+ " stderr=subprocess.DEVNULL,\n",
115
+ ")\n",
116
+ "\n",
117
+ "ENV_URL = 'http://localhost:8000'\n",
118
+ "\n",
119
+ "# Wait up to 30 s for the server to be ready\n",
120
+ "for i in range(30):\n",
121
+ " try:\n",
122
+ " r = requests.get(f'{ENV_URL}/', timeout=2)\n",
123
+ " if r.status_code == 200:\n",
124
+ " print(f'PM-Ops server ready at {ENV_URL} (pid={server_proc.pid})')\n",
125
+ " break\n",
126
+ " except Exception:\n",
127
+ " pass\n",
128
+ " time.sleep(1)\n",
129
+ "else:\n",
130
+ " raise RuntimeError('Server did not start in 30 s — check uvicorn logs.')"
131
+ ]
132
+ },
133
+ {
134
+ "cell_type": "markdown",
135
+ "metadata": {},
136
+ "source": [
137
+ "## 4. Verify Environment Connection"
138
+ ]
139
+ },
140
+ {
141
+ "cell_type": "code",
142
+ "execution_count": null,
143
+ "metadata": {},
144
+ "outputs": [],
145
+ "source": [
146
+ "from openenv.core import GenericEnvClient\n",
147
+ "\n",
148
+ "# Quick sanity check: reset → step → done\n",
149
+ "_check = GenericEnvClient(base_url=ENV_URL).sync()\n",
150
+ "with _check as env_check:\n",
151
+ " res = env_check.reset()\n",
152
+ " obs = res.observation if hasattr(res, 'observation') else res\n",
153
+ " brief = getattr(obs, 'task_brief', obs.get('task_brief', '')) if not hasattr(obs, 'task_brief') else obs.task_brief\n",
154
+ " print(f'Task brief: {brief[:120]}...')\n",
155
+ " res2 = env_check.step({'action_type': 'meta.read_runbook', 'args': {}})\n",
156
+ " print('Step OK — environment is working.')"
157
+ ]
158
+ },
159
+ {
160
+ "cell_type": "markdown",
161
+ "metadata": {},
162
+ "source": [
163
+ "## 5. Load Model and Tokenizer"
164
+ ]
165
+ },
166
+ {
167
+ "cell_type": "code",
168
+ "execution_count": null,
169
+ "metadata": {},
170
+ "outputs": [],
171
+ "source": [
172
+ "from transformers import AutoTokenizer\n",
173
+ "\n",
174
+ "MODEL_NAME = 'Qwen/Qwen3-1.7B'\n",
175
+ "tokenizer = AutoTokenizer.from_pretrained(MODEL_NAME)\n",
176
+ "tokenizer.pad_token = tokenizer.eos_token\n",
177
+ "print(f'Tokenizer ready: {MODEL_NAME}')"
178
+ ]
179
+ },
180
+ {
181
+ "cell_type": "markdown",
182
+ "metadata": {},
183
+ "source": [
184
+ "## 6. Generate Training Dataset\n",
185
+ "\n",
186
+ "150 fixed-seed triage episodes. Each seed is embedded in the prompt string so the rollout\n",
187
+ "function can reproduce the exact same org config via `env.reset(seed=...)`."
188
+ ]
189
+ },
190
+ {
191
+ "cell_type": "code",
192
+ "execution_count": null,
193
+ "metadata": {},
194
+ "outputs": [],
195
+ "source": [
196
+ "from datasets import Dataset\n",
197
+ "from training.dataset import generate_triage_dataset\n",
198
+ "\n",
199
+ "N_EPISODES = 150 # increase to 500 for full training run\n",
200
+ "\n",
201
+ "rows = generate_triage_dataset(n_episodes=N_EPISODES, base_seed=42)\n",
202
+ "dataset = Dataset.from_list([{'prompt': r['prompt']} for r in rows])\n",
203
+ "print(f'Dataset: {len(dataset)} triage episodes')\n",
204
+ "print(f'Sample prompt: {dataset[0][\"prompt\"][:120]}...')"
205
+ ]
206
+ },
207
+ {
208
+ "cell_type": "markdown",
209
+ "metadata": {},
210
+ "source": [
211
+ "## 7. Create Persistent Environment Client\n",
212
+ "\n",
213
+ "One WebSocket connection is reused across all rollouts — no reconnect overhead per episode."
214
+ ]
215
+ },
216
+ {
217
+ "cell_type": "code",
218
+ "execution_count": null,
219
+ "metadata": {},
220
+ "outputs": [],
221
+ "source": [
222
+ "from openenv.core import GenericEnvClient\n",
223
+ "\n",
224
+ "sync_env = GenericEnvClient(base_url=ENV_URL).sync()\n",
225
+ "sync_env.connect()\n",
226
+ "print('Persistent training connection established.')"
227
+ ]
228
+ },
229
+ {
230
+ "cell_type": "markdown",
231
+ "metadata": {},
232
+ "source": [
233
+ "## 8. Build Rollout Function\n",
234
+ "\n",
235
+ "Key features:\n",
236
+ "- **Option B action format**: chain-of-thought reasoning + `\\`\\`\\`json` block\n",
237
+ "- **Runbook-pinned truncation**: runbook response always kept in context, sliding window for the rest\n",
238
+ "- **Step-aware fallback**: `read_runbook` (early) → `noop` (mid) → `finish` (late) when JSON parse fails\n",
239
+ "- **Three-pass JSON extraction**: code block → raw JSON → regex pattern"
240
+ ]
241
+ },
242
+ {
243
+ "cell_type": "code",
244
+ "execution_count": null,
245
+ "metadata": {},
246
+ "outputs": [],
247
+ "source": [
248
+ "from training.rollout import make_rollout_func\n",
249
+ "\n",
250
+ "rollout_func = make_rollout_func(sync_env=sync_env, tokenizer=tokenizer)\n",
251
+ "print('Rollout function ready.')"
252
+ ]
253
+ },
254
+ {
255
+ "cell_type": "markdown",
256
+ "metadata": {},
257
+ "source": [
258
+ "## 9. Define Reward Functions\n",
259
+ "\n",
260
+ "| Function | Weight | What it measures |\n",
261
+ "|---|---|---|\n",
262
+ "| `reward_final_score` | 0.50 | Correctness: label + priority + team + channel |\n",
263
+ "| `reward_valid_json` | 0.20 | Format: fraction of steps with parseable JSON |\n",
264
+ "| `reward_read_runbook` | 0.15 | Process: read runbook before acting? |\n",
265
+ "| `reward_efficiency` | 0.15 | Speed: steps saved when correctly done |"
266
+ ]
267
+ },
268
+ {
269
+ "cell_type": "code",
270
+ "execution_count": null,
271
+ "metadata": {},
272
+ "outputs": [],
273
+ "source": [
274
+ "from training.rewards import ALL_REWARD_FUNCS\n",
275
+ "print(f'Reward functions: {[f.__name__ for f in ALL_REWARD_FUNCS]}')"
276
+ ]
277
+ },
278
+ {
279
+ "cell_type": "markdown",
280
+ "metadata": {},
281
+ "source": [
282
+ "## 10. Configure GRPO Training"
283
+ ]
284
+ },
285
+ {
286
+ "cell_type": "code",
287
+ "execution_count": null,
288
+ "metadata": {},
289
+ "outputs": [],
290
+ "source": [
291
+ "from trl import GRPOConfig\n",
292
+ "\n",
293
+ "OUTPUT_DIR = 'pm-ops-grpo-Qwen3-1.7B-triage'\n",
294
+ "\n",
295
+ "grpo_config = GRPOConfig(\n",
296
+ " # --- training ---\n",
297
+ " num_train_epochs=1,\n",
298
+ " learning_rate=5e-6,\n",
299
+ " gradient_accumulation_steps=64,\n",
300
+ " per_device_train_batch_size=1,\n",
301
+ " warmup_steps=10,\n",
302
+ " num_generations=2, # 2 trajectories per episode for relative ranking\n",
303
+ "\n",
304
+ " # --- sequence lengths (longer than Wordle due to JSON + reasoning) ---\n",
305
+ " max_completion_length=512, # room for reasoning + JSON action block\n",
306
+ " max_prompt_length=4096, # accumulated multi-turn context\n",
307
+ "\n",
308
+ " # --- vLLM colocate (generation + training share A100) ---\n",
309
+ " use_vllm=True,\n",
310
+ " vllm_mode='colocate',\n",
311
+ " vllm_gpu_memory_utilization=0.3, # ~12 GB for KV cache on A100 40GB\n",
312
+ "\n",
313
+ " # --- output + logging ---\n",
314
+ " output_dir=OUTPUT_DIR,\n",
315
+ " report_to='trackio',\n",
316
+ " trackio_space_id=OUTPUT_DIR,\n",
317
+ " logging_steps=1,\n",
318
+ " save_steps=25,\n",
319
+ " gradient_checkpointing=True,\n",
320
+ " gradient_checkpointing_kwargs={'use_reentrant': False},\n",
321
+ " push_to_hub=True,\n",
322
+ ")\n",
323
+ "\n",
324
+ "print(f'Output dir: {OUTPUT_DIR}')\n",
325
+ "print(f'Effective batch: {grpo_config.per_device_train_batch_size * grpo_config.gradient_accumulation_steps}')"
326
+ ]
327
+ },
328
+ {
329
+ "cell_type": "markdown",
330
+ "metadata": {},
331
+ "source": [
332
+ "## 11. Create Trainer"
333
+ ]
334
+ },
335
+ {
336
+ "cell_type": "code",
337
+ "execution_count": null,
338
+ "metadata": {},
339
+ "outputs": [],
340
+ "source": [
341
+ "from trl import GRPOTrainer\n",
342
+ "\n",
343
+ "trainer = GRPOTrainer(\n",
344
+ " model=MODEL_NAME,\n",
345
+ " processing_class=tokenizer,\n",
346
+ " reward_funcs=ALL_REWARD_FUNCS,\n",
347
+ " train_dataset=dataset,\n",
348
+ " args=grpo_config,\n",
349
+ " rollout_func=rollout_func,\n",
350
+ ")\n",
351
+ "print('GRPOTrainer ready.')"
352
+ ]
353
+ },
354
+ {
355
+ "cell_type": "markdown",
356
+ "metadata": {},
357
+ "source": [
358
+ "## 12. GPU Check"
359
+ ]
360
+ },
361
+ {
362
+ "cell_type": "code",
363
+ "execution_count": null,
364
+ "metadata": {},
365
+ "outputs": [],
366
+ "source": [
367
+ "import torch\n",
368
+ "gpu = torch.cuda.get_device_properties(0)\n",
369
+ "reserved_before = round(torch.cuda.max_memory_reserved() / 1024**3, 2)\n",
370
+ "total_gb = round(gpu.total_memory / 1024**3, 2)\n",
371
+ "print(f'GPU: {gpu.name} — {total_gb} GB total, {reserved_before} GB reserved')\n",
372
+ "assert total_gb >= 38, f'Need A100 40GB, got {total_gb} GB — switch runtime!'"
373
+ ]
374
+ },
375
+ {
376
+ "cell_type": "markdown",
377
+ "metadata": {},
378
+ "source": [
379
+ "## 13. Train (~90 min on A100)"
380
+ ]
381
+ },
382
+ {
383
+ "cell_type": "code",
384
+ "execution_count": null,
385
+ "metadata": {},
386
+ "outputs": [],
387
+ "source": [
388
+ "trainer_stats = trainer.train()"
389
+ ]
390
+ },
391
+ {
392
+ "cell_type": "code",
393
+ "execution_count": null,
394
+ "metadata": {},
395
+ "outputs": [],
396
+ "source": [
397
+ "used_gb = round(torch.cuda.max_memory_reserved() / 1024**3, 2)\n",
398
+ "train_mins = round(trainer_stats.metrics.get('train_runtime', 0) / 60, 1)\n",
399
+ "print(f'Training time : {train_mins} min')\n",
400
+ "print(f'Peak GPU usage: {used_gb} GB / {total_gb} GB ({round(used_gb/total_gb*100,1)}%)')"
401
+ ]
402
+ },
403
+ {
404
+ "cell_type": "markdown",
405
+ "metadata": {},
406
+ "source": [
407
+ "## 14. Save and Push"
408
+ ]
409
+ },
410
+ {
411
+ "cell_type": "code",
412
+ "execution_count": null,
413
+ "metadata": {},
414
+ "outputs": [],
415
+ "source": [
416
+ "sync_env.close() # close persistent connection before saving\n",
417
+ "trainer.save_model(OUTPUT_DIR)\n",
418
+ "trainer.push_to_hub()\n",
419
+ "print(f'Model saved to {OUTPUT_DIR} and pushed to HF Hub.')"
420
+ ]
421
+ },
422
+ {
423
+ "cell_type": "markdown",
424
+ "metadata": {},
425
+ "source": [
426
+ "## 15. Evaluate: Baseline vs Trained\n",
427
+ "\n",
428
+ "Run both the heuristic baseline and the trained model on 10 fresh triage episodes.\n",
429
+ "Uses the remote HF Space for eval so no local server needed."
430
+ ]
431
+ },
432
+ {
433
+ "cell_type": "code",
434
+ "execution_count": null,
435
+ "metadata": {},
436
+ "outputs": [],
437
+ "source": [
438
+ "import json\n",
439
+ "from transformers import AutoModelForCausalLM\n",
440
+ "from openenv.core import GenericEnvClient\n",
441
+ "from training.rollout import extract_json_action, step_aware_fallback, build_messages, _obs_to_dict\n",
442
+ "from training.prompts import SYSTEM_PROMPT, format_observation\n",
443
+ "from inference import baseline_agent\n",
444
+ "\n",
445
+ "EVAL_URL = 'https://adityaguntur-pm-ops.hf.space' # remote Space for eval\n",
446
+ "N_EVAL = 10\n",
447
+ "\n",
448
+ "fine_tuned_model = AutoModelForCausalLM.from_pretrained(\n",
449
+ " OUTPUT_DIR, torch_dtype='auto', device_map='auto'\n",
450
+ ")\n",
451
+ "\n",
452
+ "\n",
453
+ "def eval_trained(sync_env, model, tokenizer, n=N_EVAL):\n",
454
+ " scores = []\n",
455
+ " for i in range(n):\n",
456
+ " result = sync_env.reset()\n",
457
+ " obs = result.observation if hasattr(result, 'observation') else result\n",
458
+ " obs_dict = _obs_to_dict(obs)\n",
459
+ " task_brief = obs_dict.get('task_brief', '')\n",
460
+ " history = []\n",
461
+ " done = False\n",
462
+ " step = 0\n",
463
+ " score = 0.0\n",
464
+ "\n",
465
+ " while not done and step < 40:\n",
466
+ " obs_text = format_observation(obs_dict, step, obs_dict.get('last_action_result'))\n",
467
+ " messages = build_messages(task_brief, history, obs_text)\n",
468
+ " prompt_text = tokenizer.apply_chat_template(\n",
469
+ " messages, add_generation_prompt=True,\n",
470
+ " tokenize=False, enable_thinking=False,\n",
471
+ " )\n",
472
+ " inputs = tokenizer([prompt_text], return_tensors='pt').to(model.device)\n",
473
+ " out_ids = model.generate(**inputs, max_new_tokens=512)\n",
474
+ " completion = tokenizer.decode(\n",
475
+ " out_ids[0][len(inputs.input_ids[0]):], skip_special_tokens=True\n",
476
+ " )\n",
477
+ " parsed = extract_json_action(completion) or step_aware_fallback(step)\n",
478
+ " result = sync_env.step({'action_type': parsed['action_type'], 'args': parsed.get('args', {})})\n",
479
+ " obs = result.observation if hasattr(result, 'observation') else result\n",
480
+ " obs_dict = _obs_to_dict(obs)\n",
481
+ " done = bool(getattr(result, 'done', obs_dict.get('done', False)))\n",
482
+ " score = float(getattr(result, 'reward', obs_dict.get('reward', 0.0)))\n",
483
+ " history.append({'role': 'assistant', 'content': completion, 'is_runbook': False})\n",
484
+ " step += 1\n",
485
+ "\n",
486
+ " scores.append(score)\n",
487
+ " print(f' Eval episode {i+1}/{n}: score={score:.2f}')\n",
488
+ " return scores\n",
489
+ "\n",
490
+ "\n",
491
+ "def eval_baseline(sync_env, n=N_EVAL):\n",
492
+ " scores = []\n",
493
+ " for i in range(n):\n",
494
+ " result = sync_env.reset()\n",
495
+ " obs = result.observation if hasattr(result, 'observation') else result\n",
496
+ " obs_dict = _obs_to_dict(obs)\n",
497
+ " org_config = {}\n",
498
+ " done = False\n",
499
+ " step = 0\n",
500
+ " score = 0.0\n",
501
+ " while not done and step < 40:\n",
502
+ " action_type, args = baseline_agent(obs_dict, org_config)\n",
503
+ " result = sync_env.step({'action_type': action_type, 'args': args})\n",
504
+ " obs = result.observation if hasattr(result, 'observation') else result\n",
505
+ " obs_dict = _obs_to_dict(obs)\n",
506
+ " done = bool(getattr(result, 'done', obs_dict.get('done', False)))\n",
507
+ " score = float(getattr(result, 'reward', obs_dict.get('reward', 0.0)))\n",
508
+ " step += 1\n",
509
+ " scores.append(score)\n",
510
+ " print(f' Baseline episode {i+1}/{n}: score={score:.2f}')\n",
511
+ " return scores\n",
512
+ "\n",
513
+ "\n",
514
+ "eval_client = GenericEnvClient(base_url=EVAL_URL).sync()\n",
515
+ "\n",
516
+ "with eval_client as env_eval:\n",
517
+ " print('--- Baseline agent ---')\n",
518
+ " baseline_scores = eval_baseline(env_eval)\n",
519
+ "\n",
520
+ "with GenericEnvClient(base_url=EVAL_URL).sync() as env_eval:\n",
521
+ " print('--- Trained agent ---')\n",
522
+ " trained_scores = eval_trained(env_eval, fine_tuned_model, tokenizer)\n",
523
+ "\n",
524
+ "print(f'\\nBaseline avg: {sum(baseline_scores)/len(baseline_scores):.3f}')\n",
525
+ "print(f'Trained avg: {sum(trained_scores)/len(trained_scores):.3f}')"
526
+ ]
527
+ },
528
+ {
529
+ "cell_type": "markdown",
530
+ "metadata": {},
531
+ "source": [
532
+ "## 16. Plot Results"
533
+ ]
534
+ },
535
+ {
536
+ "cell_type": "code",
537
+ "execution_count": null,
538
+ "metadata": {},
539
+ "outputs": [],
540
+ "source": [
541
+ "import matplotlib.pyplot as plt\n",
542
+ "import numpy as np\n",
543
+ "\n",
544
+ "fig, axes = plt.subplots(1, 2, figsize=(12, 4))\n",
545
+ "\n",
546
+ "# Left: episode-by-episode comparison\n",
547
+ "ax = axes[0]\n",
548
+ "x = np.arange(N_EVAL)\n",
549
+ "w = 0.35\n",
550
+ "ax.bar(x - w/2, baseline_scores, w, label='Baseline', color='steelblue', alpha=0.8)\n",
551
+ "ax.bar(x + w/2, trained_scores, w, label='Trained (GRPO)', color='coral', alpha=0.8)\n",
552
+ "ax.axhline(sum(baseline_scores)/N_EVAL, color='steelblue', linestyle='--', alpha=0.6)\n",
553
+ "ax.axhline(sum(trained_scores)/N_EVAL, color='coral', linestyle='--', alpha=0.6)\n",
554
+ "ax.set_xlabel('Eval episode')\n",
555
+ "ax.set_ylabel('Episode reward (0–1)')\n",
556
+ "ax.set_title('Baseline vs Trained — per-episode reward')\n",
557
+ "ax.set_xticks(x)\n",
558
+ "ax.set_ylim(0, 1.05)\n",
559
+ "ax.legend()\n",
560
+ "\n",
561
+ "# Right: average summary\n",
562
+ "ax2 = axes[1]\n",
563
+ "avgs = [sum(baseline_scores)/N_EVAL, sum(trained_scores)/N_EVAL]\n",
564
+ "bars = ax2.bar(['Baseline', 'Trained (GRPO)'], avgs,\n",
565
+ " color=['steelblue', 'coral'], alpha=0.85, width=0.5)\n",
566
+ "for bar, val in zip(bars, avgs):\n",
567
+ " ax2.text(bar.get_x() + bar.get_width()/2, val + 0.01, f'{val:.3f}',\n",
568
+ " ha='center', fontsize=12, fontweight='bold')\n",
569
+ "ax2.set_ylabel('Average reward (0–1)')\n",
570
+ "ax2.set_title(f'Average over {N_EVAL} triage episodes')\n",
571
+ "ax2.set_ylim(0, 1.05)\n",
572
+ "\n",
573
+ "plt.tight_layout()\n",
574
+ "plt.savefig('eval_results.png', dpi=150, bbox_inches='tight')\n",
575
+ "plt.show()\n",
576
+ "print('Saved: eval_results.png')"
577
+ ]
578
+ },
579
+ {
580
+ "cell_type": "markdown",
581
+ "metadata": {},
582
+ "source": [
583
+ "## 17. Teardown"
584
+ ]
585
+ },
586
+ {
587
+ "cell_type": "code",
588
+ "execution_count": null,
589
+ "metadata": {},
590
+ "outputs": [],
591
+ "source": [
592
+ "server_proc.terminate()\n",
593
+ "print('Local PM-Ops server stopped.')"
594
+ ]
595
+ },
596
+ {
597
+ "cell_type": "markdown",
598
+ "metadata": {},
599
+ "source": [
600
+ "---\n",
601
+ "## Summary\n",
602
+ "\n",
603
+ "What happened:\n",
604
+ "1. Started PM-Ops server locally (0ms latency for 40-step episodes)\n",
605
+ "2. Generated 150 fixed-seed triage episodes (reproducible)\n",
606
+ "3. Trained Qwen3-1.7B with GRPO using 4 reward signals\n",
607
+ "4. Compared baseline heuristic vs trained agent on 10 fresh episodes\n",
608
+ "\n",
609
+ "### Reward signal design\n",
610
+ "- `reward_final_score × 0.50` — correctness (label + priority + team + channel)\n",
611
+ "- `reward_valid_json × 0.20` — format discipline (anti-staleness)\n",
612
+ "- `reward_read_runbook × 0.15` — process (read before acting)\n",
613
+ "- `reward_efficiency × 0.15` — speed (fewer steps when correct)\n",
614
+ "\n",
615
+ "### What to try next\n",
616
+ "- Scale to 500 episodes across all 4 task types (triage + incident_routing + release_notes + dep_update)\n",
617
+ "- Larger model: Qwen3-4B\n",
618
+ "- Tune reward weights in `training/rewards.py`\n",
619
+ "- Switch `WEIGHT_*` constants to equal 0.25 each to test Option B (equal weights)\n",
620
+ "- Longer training: `num_train_epochs=3`"
621
+ ]
622
+ }
623
+ ],
624
+ "metadata": {
625
+ "kernelspec": {
626
+ "display_name": "Python 3",
627
+ "language": "python",
628
+ "name": "python3"
629
+ },
630
+ "language_info": {
631
+ "name": "python",
632
+ "version": "3.11.0"
633
+ },
634
+ "accelerator": "GPU",
635
+ "gpuClass": "premium"
636
+ },
637
+ "nbformat": 4,
638
+ "nbformat_minor": 4
639
+ }
training/triage_dataset.jsonl ADDED
@@ -0,0 +1,150 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {"prompt": "SEED:478163327 | Support escalation: payments is intermittently failing for EU users. Triage and route per org process.", "seed": 478163327, "difficulty": "easy"}
2
+ {"prompt": "SEED:1181241943 | A user reported that the notifications is returning 500 errors. The issue started 20 minutes ago. Triage this bug: file a ticket with the correct label and priority, assign it to the right team, and notify the correct oncall channel.", "seed": 1181241943, "difficulty": "medium"}
3
+ {"prompt": "SEED:958682846 | Monitoring alert: elevated error rate on api-gateway. File a bug ticket and page the right team.", "seed": 958682846, "difficulty": "medium"}
4
+ {"prompt": "SEED:440213415 | A user reported that the inventory is returning 500 errors. The issue started 20 minutes ago. Triage this bug: file a ticket with the correct label and priority, assign it to the right team, and notify the correct oncall channel.", "seed": 440213415, "difficulty": "easy"}
5
+ {"prompt": "SEED:1812140441 | A user reported that the payments is returning 500 errors. The issue started 20 minutes ago. Triage this bug: file a ticket with the correct label and priority, assign it to the right team, and notify the correct oncall channel.", "seed": 1812140441, "difficulty": "easy"}
6
+ {"prompt": "SEED:127978094 | A user reported that the auth is returning 500 errors. The issue started 20 minutes ago. Triage this bug: file a ticket with the correct label and priority, assign it to the right team, and notify the correct oncall channel.", "seed": 127978094, "difficulty": "easy"}
7
+ {"prompt": "SEED:939042955 | Monitoring alert: elevated error rate on payments. File a bug ticket and page the right team.", "seed": 939042955, "difficulty": "medium"}
8
+ {"prompt": "SEED:113971123 | Monitoring alert: elevated error rate on inventory. File a bug ticket and page the right team.", "seed": 113971123, "difficulty": "medium"}
9
+ {"prompt": "SEED:1801823908 | Support escalation: api-gateway is intermittently failing for EU users. Triage and route per org process.", "seed": 1801823908, "difficulty": "medium"}
10
+ {"prompt": "SEED:1929338154 | A user reported that the checkout is returning 500 errors. The issue started 20 minutes ago. Triage this bug: file a ticket with the correct label and priority, assign it to the right team, and notify the correct oncall channel.", "seed": 1929338154, "difficulty": "medium"}
11
+ {"prompt": "SEED:27911967 | Monitoring alert: elevated error rate on api-gateway. File a bug ticket and page the right team.", "seed": 27911967, "difficulty": "medium"}
12
+ {"prompt": "SEED:1815115025 | Monitoring alert: elevated error rate on payments. File a bug ticket and page the right team.", "seed": 1815115025, "difficulty": "medium"}
13
+ {"prompt": "SEED:1193448329 | Support escalation: checkout is intermittently failing for EU users. Triage and route per org process.", "seed": 1193448329, "difficulty": "medium"}
14
+ {"prompt": "SEED:924765563 | Monitoring alert: elevated error rate on search. File a bug ticket and page the right team.", "seed": 924765563, "difficulty": "medium"}
15
+ {"prompt": "SEED:438989805 | Support escalation: notifications is intermittently failing for EU users. Triage and route per org process.", "seed": 438989805, "difficulty": "easy"}
16
+ {"prompt": "SEED:1631775357 | Monitoring alert: elevated error rate on auth. File a bug ticket and page the right team.", "seed": 1631775357, "difficulty": "easy"}
17
+ {"prompt": "SEED:1541804686 | Support escalation: api-gateway is intermittently failing for EU users. Triage and route per org process.", "seed": 1541804686, "difficulty": "medium"}
18
+ {"prompt": "SEED:1136108454 | Support escalation: payments is intermittently failing for EU users. Triage and route per org process.", "seed": 1136108454, "difficulty": "easy"}
19
+ {"prompt": "SEED:1973214822 | Support escalation: payments is intermittently failing for EU users. Triage and route per org process.", "seed": 1973214822, "difficulty": "easy"}
20
+ {"prompt": "SEED:1625792787 | A user reported that the checkout is returning 500 errors. The issue started 20 minutes ago. Triage this bug: file a ticket with the correct label and priority, assign it to the right team, and notify the correct oncall channel.", "seed": 1625792787, "difficulty": "easy"}
21
+ {"prompt": "SEED:1259191105 | Monitoring alert: elevated error rate on checkout. File a bug ticket and page the right team.", "seed": 1259191105, "difficulty": "medium"}
22
+ {"prompt": "SEED:825873196 | A user reported that the api-gateway is returning 500 errors. The issue started 20 minutes ago. Triage this bug: file a ticket with the correct label and priority, assign it to the right team, and notify the correct oncall channel.", "seed": 825873196, "difficulty": "easy"}
23
+ {"prompt": "SEED:196814233 | Support escalation: api-gateway is intermittently failing for EU users. Triage and route per org process.", "seed": 196814233, "difficulty": "medium"}
24
+ {"prompt": "SEED:1242911821 | Monitoring alert: elevated error rate on payments. File a bug ticket and page the right team.", "seed": 1242911821, "difficulty": "easy"}
25
+ {"prompt": "SEED:999829240 | Monitoring alert: elevated error rate on checkout. File a bug ticket and page the right team.", "seed": 999829240, "difficulty": "easy"}
26
+ {"prompt": "SEED:1632629719 | Monitoring alert: elevated error rate on notifications. File a bug ticket and page the right team.", "seed": 1632629719, "difficulty": "medium"}
27
+ {"prompt": "SEED:1947382419 | Support escalation: inventory is intermittently failing for EU users. Triage and route per org process.", "seed": 1947382419, "difficulty": "medium"}
28
+ {"prompt": "SEED:698594025 | Monitoring alert: elevated error rate on inventory. File a bug ticket and page the right team.", "seed": 698594025, "difficulty": "medium"}
29
+ {"prompt": "SEED:1525876051 | A user reported that the checkout is returning 500 errors. The issue started 20 minutes ago. Triage this bug: file a ticket with the correct label and priority, assign it to the right team, and notify the correct oncall channel.", "seed": 1525876051, "difficulty": "medium"}
30
+ {"prompt": "SEED:1146660997 | Monitoring alert: elevated error rate on auth. File a bug ticket and page the right team.", "seed": 1146660997, "difficulty": "easy"}
31
+ {"prompt": "SEED:735034881 | Support escalation: inventory is intermittently failing for EU users. Triage and route per org process.", "seed": 735034881, "difficulty": "medium"}
32
+ {"prompt": "SEED:701808367 | Monitoring alert: elevated error rate on auth. File a bug ticket and page the right team.", "seed": 701808367, "difficulty": "hard"}
33
+ {"prompt": "SEED:1629748727 | A user reported that the api-gateway is returning 500 errors. The issue started 20 minutes ago. Triage this bug: file a ticket with the correct label and priority, assign it to the right team, and notify the correct oncall channel.", "seed": 1629748727, "difficulty": "medium"}
34
+ {"prompt": "SEED:943239974 | Support escalation: search is intermittently failing for EU users. Triage and route per org process.", "seed": 943239974, "difficulty": "medium"}
35
+ {"prompt": "SEED:240251661 | A user reported that the payments is returning 500 errors. The issue started 20 minutes ago. Triage this bug: file a ticket with the correct label and priority, assign it to the right team, and notify the correct oncall channel.", "seed": 240251661, "difficulty": "medium"}
36
+ {"prompt": "SEED:137869475 | Support escalation: api-gateway is intermittently failing for EU users. Triage and route per org process.", "seed": 137869475, "difficulty": "medium"}
37
+ {"prompt": "SEED:1722989659 | Monitoring alert: elevated error rate on search. File a bug ticket and page the right team.", "seed": 1722989659, "difficulty": "medium"}
38
+ {"prompt": "SEED:284277889 | Monitoring alert: elevated error rate on payments. File a bug ticket and page the right team.", "seed": 284277889, "difficulty": "medium"}
39
+ {"prompt": "SEED:1351531223 | Monitoring alert: elevated error rate on payments. File a bug ticket and page the right team.", "seed": 1351531223, "difficulty": "medium"}
40
+ {"prompt": "SEED:2144181937 | Monitoring alert: elevated error rate on checkout. File a bug ticket and page the right team.", "seed": 2144181937, "difficulty": "hard"}
41
+ {"prompt": "SEED:1970753705 | Monitoring alert: elevated error rate on auth. File a bug ticket and page the right team.", "seed": 1970753705, "difficulty": "medium"}
42
+ {"prompt": "SEED:1137651678 | Support escalation: payments is intermittently failing for EU users. Triage and route per org process.", "seed": 1137651678, "difficulty": "medium"}
43
+ {"prompt": "SEED:1059257080 | Monitoring alert: elevated error rate on payments. File a bug ticket and page the right team.", "seed": 1059257080, "difficulty": "medium"}
44
+ {"prompt": "SEED:1840109255 | Support escalation: auth is intermittently failing for EU users. Triage and route per org process.", "seed": 1840109255, "difficulty": "hard"}
45
+ {"prompt": "SEED:1554762903 | Monitoring alert: elevated error rate on payments. File a bug ticket and page the right team.", "seed": 1554762903, "difficulty": "medium"}
46
+ {"prompt": "SEED:594130308 | Support escalation: checkout is intermittently failing for EU users. Triage and route per org process.", "seed": 594130308, "difficulty": "hard"}
47
+ {"prompt": "SEED:390452952 | Support escalation: inventory is intermittently failing for EU users. Triage and route per org process.", "seed": 390452952, "difficulty": "easy"}
48
+ {"prompt": "SEED:470939445 | A user reported that the payments is returning 500 errors. The issue started 20 minutes ago. Triage this bug: file a ticket with the correct label and priority, assign it to the right team, and notify the correct oncall channel.", "seed": 470939445, "difficulty": "medium"}
49
+ {"prompt": "SEED:687117441 | Support escalation: notifications is intermittently failing for EU users. Triage and route per org process.", "seed": 687117441, "difficulty": "hard"}
50
+ {"prompt": "SEED:272849424 | A user reported that the search is returning 500 errors. The issue started 20 minutes ago. Triage this bug: file a ticket with the correct label and priority, assign it to the right team, and notify the correct oncall channel.", "seed": 272849424, "difficulty": "hard"}
51
+ {"prompt": "SEED:1639042338 | Support escalation: checkout is intermittently failing for EU users. Triage and route per org process.", "seed": 1639042338, "difficulty": "hard"}
52
+ {"prompt": "SEED:1079815404 | Support escalation: search is intermittently failing for EU users. Triage and route per org process.", "seed": 1079815404, "difficulty": "easy"}
53
+ {"prompt": "SEED:491995979 | Monitoring alert: elevated error rate on auth. File a bug ticket and page the right team.", "seed": 491995979, "difficulty": "medium"}
54
+ {"prompt": "SEED:1461042543 | A user reported that the inventory is returning 500 errors. The issue started 20 minutes ago. Triage this bug: file a ticket with the correct label and priority, assign it to the right team, and notify the correct oncall channel.", "seed": 1461042543, "difficulty": "easy"}
55
+ {"prompt": "SEED:1260573448 | Support escalation: auth is intermittently failing for EU users. Triage and route per org process.", "seed": 1260573448, "difficulty": "hard"}
56
+ {"prompt": "SEED:679282378 | Monitoring alert: elevated error rate on search. File a bug ticket and page the right team.", "seed": 679282378, "difficulty": "hard"}
57
+ {"prompt": "SEED:13938521 | A user reported that the auth is returning 500 errors. The issue started 20 minutes ago. Triage this bug: file a ticket with the correct label and priority, assign it to the right team, and notify the correct oncall channel.", "seed": 13938521, "difficulty": "medium"}
58
+ {"prompt": "SEED:767303988 | Support escalation: api-gateway is intermittently failing for EU users. Triage and route per org process.", "seed": 767303988, "difficulty": "easy"}
59
+ {"prompt": "SEED:1281810600 | A user reported that the payments is returning 500 errors. The issue started 20 minutes ago. Triage this bug: file a ticket with the correct label and priority, assign it to the right team, and notify the correct oncall channel.", "seed": 1281810600, "difficulty": "medium"}
60
+ {"prompt": "SEED:656439677 | Monitoring alert: elevated error rate on checkout. File a bug ticket and page the right team.", "seed": 656439677, "difficulty": "medium"}
61
+ {"prompt": "SEED:693847829 | Monitoring alert: elevated error rate on auth. File a bug ticket and page the right team.", "seed": 693847829, "difficulty": "easy"}
62
+ {"prompt": "SEED:1392239675 | A user reported that the notifications is returning 500 errors. The issue started 20 minutes ago. Triage this bug: file a ticket with the correct label and priority, assign it to the right team, and notify the correct oncall channel.", "seed": 1392239675, "difficulty": "hard"}
63
+ {"prompt": "SEED:83651970 | A user reported that the search is returning 500 errors. The issue started 20 minutes ago. Triage this bug: file a ticket with the correct label and priority, assign it to the right team, and notify the correct oncall channel.", "seed": 83651970, "difficulty": "easy"}
64
+ {"prompt": "SEED:1558990517 | Support escalation: checkout is intermittently failing for EU users. Triage and route per org process.", "seed": 1558990517, "difficulty": "medium"}
65
+ {"prompt": "SEED:1028439863 | Monitoring alert: elevated error rate on payments. File a bug ticket and page the right team.", "seed": 1028439863, "difficulty": "easy"}
66
+ {"prompt": "SEED:1034535593 | Monitoring alert: elevated error rate on notifications. File a bug ticket and page the right team.", "seed": 1034535593, "difficulty": "easy"}
67
+ {"prompt": "SEED:367878761 | Monitoring alert: elevated error rate on auth. File a bug ticket and page the right team.", "seed": 367878761, "difficulty": "hard"}
68
+ {"prompt": "SEED:297265480 | A user reported that the notifications is returning 500 errors. The issue started 20 minutes ago. Triage this bug: file a ticket with the correct label and priority, assign it to the right team, and notify the correct oncall channel.", "seed": 297265480, "difficulty": "medium"}
69
+ {"prompt": "SEED:551437149 | Monitoring alert: elevated error rate on notifications. File a bug ticket and page the right team.", "seed": 551437149, "difficulty": "hard"}
70
+ {"prompt": "SEED:709214891 | A user reported that the checkout is returning 500 errors. The issue started 20 minutes ago. Triage this bug: file a ticket with the correct label and priority, assign it to the right team, and notify the correct oncall channel.", "seed": 709214891, "difficulty": "medium"}
71
+ {"prompt": "SEED:1817363615 | Monitoring alert: elevated error rate on payments. File a bug ticket and page the right team.", "seed": 1817363615, "difficulty": "medium"}
72
+ {"prompt": "SEED:863937247 | A user reported that the inventory is returning 500 errors. The issue started 20 minutes ago. Triage this bug: file a ticket with the correct label and priority, assign it to the right team, and notify the correct oncall channel.", "seed": 863937247, "difficulty": "medium"}
73
+ {"prompt": "SEED:1713658874 | Support escalation: auth is intermittently failing for EU users. Triage and route per org process.", "seed": 1713658874, "difficulty": "medium"}
74
+ {"prompt": "SEED:1881625505 | A user reported that the notifications is returning 500 errors. The issue started 20 minutes ago. Triage this bug: file a ticket with the correct label and priority, assign it to the right team, and notify the correct oncall channel.", "seed": 1881625505, "difficulty": "hard"}
75
+ {"prompt": "SEED:519709079 | Monitoring alert: elevated error rate on notifications. File a bug ticket and page the right team.", "seed": 519709079, "difficulty": "medium"}
76
+ {"prompt": "SEED:965067727 | Monitoring alert: elevated error rate on inventory. File a bug ticket and page the right team.", "seed": 965067727, "difficulty": "easy"}
77
+ {"prompt": "SEED:1452066459 | A user reported that the notifications is returning 500 errors. The issue started 20 minutes ago. Triage this bug: file a ticket with the correct label and priority, assign it to the right team, and notify the correct oncall channel.", "seed": 1452066459, "difficulty": "easy"}
78
+ {"prompt": "SEED:988335251 | A user reported that the auth is returning 500 errors. The issue started 20 minutes ago. Triage this bug: file a ticket with the correct label and priority, assign it to the right team, and notify the correct oncall channel.", "seed": 988335251, "difficulty": "medium"}
79
+ {"prompt": "SEED:30884438 | Monitoring alert: elevated error rate on auth. File a bug ticket and page the right team.", "seed": 30884438, "difficulty": "easy"}
80
+ {"prompt": "SEED:252860896 | Support escalation: api-gateway is intermittently failing for EU users. Triage and route per org process.", "seed": 252860896, "difficulty": "medium"}
81
+ {"prompt": "SEED:289482182 | A user reported that the auth is returning 500 errors. The issue started 20 minutes ago. Triage this bug: file a ticket with the correct label and priority, assign it to the right team, and notify the correct oncall channel.", "seed": 289482182, "difficulty": "easy"}
82
+ {"prompt": "SEED:1419179580 | Support escalation: payments is intermittently failing for EU users. Triage and route per org process.", "seed": 1419179580, "difficulty": "easy"}
83
+ {"prompt": "SEED:1022222125 | Support escalation: checkout is intermittently failing for EU users. Triage and route per org process.", "seed": 1022222125, "difficulty": "medium"}
84
+ {"prompt": "SEED:2084839399 | Monitoring alert: elevated error rate on auth. File a bug ticket and page the right team.", "seed": 2084839399, "difficulty": "medium"}
85
+ {"prompt": "SEED:568275055 | Support escalation: notifications is intermittently failing for EU users. Triage and route per org process.", "seed": 568275055, "difficulty": "hard"}
86
+ {"prompt": "SEED:1043665192 | Monitoring alert: elevated error rate on inventory. File a bug ticket and page the right team.", "seed": 1043665192, "difficulty": "hard"}
87
+ {"prompt": "SEED:1748309234 | A user reported that the search is returning 500 errors. The issue started 20 minutes ago. Triage this bug: file a ticket with the correct label and priority, assign it to the right team, and notify the correct oncall channel.", "seed": 1748309234, "difficulty": "medium"}
88
+ {"prompt": "SEED:405126228 | A user reported that the inventory is returning 500 errors. The issue started 20 minutes ago. Triage this bug: file a ticket with the correct label and priority, assign it to the right team, and notify the correct oncall channel.", "seed": 405126228, "difficulty": "easy"}
89
+ {"prompt": "SEED:1851350739 | Monitoring alert: elevated error rate on checkout. File a bug ticket and page the right team.", "seed": 1851350739, "difficulty": "medium"}
90
+ {"prompt": "SEED:1819256337 | A user reported that the payments is returning 500 errors. The issue started 20 minutes ago. Triage this bug: file a ticket with the correct label and priority, assign it to the right team, and notify the correct oncall channel.", "seed": 1819256337, "difficulty": "hard"}
91
+ {"prompt": "SEED:2005855667 | Support escalation: inventory is intermittently failing for EU users. Triage and route per org process.", "seed": 2005855667, "difficulty": "easy"}
92
+ {"prompt": "SEED:422701550 | A user reported that the auth is returning 500 errors. The issue started 20 minutes ago. Triage this bug: file a ticket with the correct label and priority, assign it to the right team, and notify the correct oncall channel.", "seed": 422701550, "difficulty": "easy"}
93
+ {"prompt": "SEED:1729245242 | A user reported that the auth is returning 500 errors. The issue started 20 minutes ago. Triage this bug: file a ticket with the correct label and priority, assign it to the right team, and notify the correct oncall channel.", "seed": 1729245242, "difficulty": "medium"}
94
+ {"prompt": "SEED:469306919 | Monitoring alert: elevated error rate on inventory. File a bug ticket and page the right team.", "seed": 469306919, "difficulty": "medium"}
95
+ {"prompt": "SEED:822873088 | Monitoring alert: elevated error rate on inventory. File a bug ticket and page the right team.", "seed": 822873088, "difficulty": "medium"}
96
+ {"prompt": "SEED:1926780541 | Support escalation: checkout is intermittently failing for EU users. Triage and route per org process.", "seed": 1926780541, "difficulty": "medium"}
97
+ {"prompt": "SEED:1811967841 | A user reported that the api-gateway is returning 500 errors. The issue started 20 minutes ago. Triage this bug: file a ticket with the correct label and priority, assign it to the right team, and notify the correct oncall channel.", "seed": 1811967841, "difficulty": "medium"}
98
+ {"prompt": "SEED:1196342297 | Support escalation: payments is intermittently failing for EU users. Triage and route per org process.", "seed": 1196342297, "difficulty": "hard"}
99
+ {"prompt": "SEED:1072910527 | Monitoring alert: elevated error rate on api-gateway. File a bug ticket and page the right team.", "seed": 1072910527, "difficulty": "easy"}
100
+ {"prompt": "SEED:1903232052 | Support escalation: inventory is intermittently failing for EU users. Triage and route per org process.", "seed": 1903232052, "difficulty": "easy"}
101
+ {"prompt": "SEED:217275224 | A user reported that the inventory is returning 500 errors. The issue started 20 minutes ago. Triage this bug: file a ticket with the correct label and priority, assign it to the right team, and notify the correct oncall channel.", "seed": 217275224, "difficulty": "easy"}
102
+ {"prompt": "SEED:400560567 | Monitoring alert: elevated error rate on checkout. File a bug ticket and page the right team.", "seed": 400560567, "difficulty": "medium"}
103
+ {"prompt": "SEED:714300770 | A user reported that the notifications is returning 500 errors. The issue started 20 minutes ago. Triage this bug: file a ticket with the correct label and priority, assign it to the right team, and notify the correct oncall channel.", "seed": 714300770, "difficulty": "hard"}
104
+ {"prompt": "SEED:2085812759 | Support escalation: notifications is intermittently failing for EU users. Triage and route per org process.", "seed": 2085812759, "difficulty": "hard"}
105
+ {"prompt": "SEED:918037633 | Monitoring alert: elevated error rate on payments. File a bug ticket and page the right team.", "seed": 918037633, "difficulty": "hard"}
106
+ {"prompt": "SEED:251837136 | A user reported that the payments is returning 500 errors. The issue started 20 minutes ago. Triage this bug: file a ticket with the correct label and priority, assign it to the right team, and notify the correct oncall channel.", "seed": 251837136, "difficulty": "medium"}
107
+ {"prompt": "SEED:1627677155 | Support escalation: api-gateway is intermittently failing for EU users. Triage and route per org process.", "seed": 1627677155, "difficulty": "easy"}
108
+ {"prompt": "SEED:1676848676 | Monitoring alert: elevated error rate on inventory. File a bug ticket and page the right team.", "seed": 1676848676, "difficulty": "medium"}
109
+ {"prompt": "SEED:1954246074 | A user reported that the api-gateway is returning 500 errors. The issue started 20 minutes ago. Triage this bug: file a ticket with the correct label and priority, assign it to the right team, and notify the correct oncall channel.", "seed": 1954246074, "difficulty": "medium"}
110
+ {"prompt": "SEED:1816803306 | Monitoring alert: elevated error rate on search. File a bug ticket and page the right team.", "seed": 1816803306, "difficulty": "hard"}
111
+ {"prompt": "SEED:664847319 | A user reported that the auth is returning 500 errors. The issue started 20 minutes ago. Triage this bug: file a ticket with the correct label and priority, assign it to the right team, and notify the correct oncall channel.", "seed": 664847319, "difficulty": "medium"}
112
+ {"prompt": "SEED:1274350418 | Support escalation: search is intermittently failing for EU users. Triage and route per org process.", "seed": 1274350418, "difficulty": "medium"}
113
+ {"prompt": "SEED:251183830 | Support escalation: api-gateway is intermittently failing for EU users. Triage and route per org process.", "seed": 251183830, "difficulty": "easy"}
114
+ {"prompt": "SEED:1346922426 | Monitoring alert: elevated error rate on checkout. File a bug ticket and page the right team.", "seed": 1346922426, "difficulty": "easy"}
115
+ {"prompt": "SEED:215359682 | Monitoring alert: elevated error rate on api-gateway. File a bug ticket and page the right team.", "seed": 215359682, "difficulty": "hard"}
116
+ {"prompt": "SEED:676168421 | Support escalation: notifications is intermittently failing for EU users. Triage and route per org process.", "seed": 676168421, "difficulty": "easy"}
117
+ {"prompt": "SEED:344076115 | A user reported that the notifications is returning 500 errors. The issue started 20 minutes ago. Triage this bug: file a ticket with the correct label and priority, assign it to the right team, and notify the correct oncall channel.", "seed": 344076115, "difficulty": "medium"}
118
+ {"prompt": "SEED:294296873 | Support escalation: payments is intermittently failing for EU users. Triage and route per org process.", "seed": 294296873, "difficulty": "easy"}
119
+ {"prompt": "SEED:1010193046 | Monitoring alert: elevated error rate on api-gateway. File a bug ticket and page the right team.", "seed": 1010193046, "difficulty": "hard"}
120
+ {"prompt": "SEED:514909066 | A user reported that the search is returning 500 errors. The issue started 20 minutes ago. Triage this bug: file a ticket with the correct label and priority, assign it to the right team, and notify the correct oncall channel.", "seed": 514909066, "difficulty": "medium"}
121
+ {"prompt": "SEED:170689150 | Support escalation: notifications is intermittently failing for EU users. Triage and route per org process.", "seed": 170689150, "difficulty": "easy"}
122
+ {"prompt": "SEED:1800557283 | Monitoring alert: elevated error rate on payments. File a bug ticket and page the right team.", "seed": 1800557283, "difficulty": "medium"}
123
+ {"prompt": "SEED:1119980130 | A user reported that the auth is returning 500 errors. The issue started 20 minutes ago. Triage this bug: file a ticket with the correct label and priority, assign it to the right team, and notify the correct oncall channel.", "seed": 1119980130, "difficulty": "medium"}
124
+ {"prompt": "SEED:1349409434 | A user reported that the checkout is returning 500 errors. The issue started 20 minutes ago. Triage this bug: file a ticket with the correct label and priority, assign it to the right team, and notify the correct oncall channel.", "seed": 1349409434, "difficulty": "medium"}
125
+ {"prompt": "SEED:1140805649 | Support escalation: checkout is intermittently failing for EU users. Triage and route per org process.", "seed": 1140805649, "difficulty": "hard"}
126
+ {"prompt": "SEED:562117925 | A user reported that the payments is returning 500 errors. The issue started 20 minutes ago. Triage this bug: file a ticket with the correct label and priority, assign it to the right team, and notify the correct oncall channel.", "seed": 562117925, "difficulty": "medium"}
127
+ {"prompt": "SEED:1963764597 | A user reported that the api-gateway is returning 500 errors. The issue started 20 minutes ago. Triage this bug: file a ticket with the correct label and priority, assign it to the right team, and notify the correct oncall channel.", "seed": 1963764597, "difficulty": "medium"}
128
+ {"prompt": "SEED:311570307 | Monitoring alert: elevated error rate on inventory. File a bug ticket and page the right team.", "seed": 311570307, "difficulty": "easy"}
129
+ {"prompt": "SEED:1968321319 | A user reported that the payments is returning 500 errors. The issue started 20 minutes ago. Triage this bug: file a ticket with the correct label and priority, assign it to the right team, and notify the correct oncall channel.", "seed": 1968321319, "difficulty": "easy"}
130
+ {"prompt": "SEED:314652384 | A user reported that the checkout is returning 500 errors. The issue started 20 minutes ago. Triage this bug: file a ticket with the correct label and priority, assign it to the right team, and notify the correct oncall channel.", "seed": 314652384, "difficulty": "medium"}
131
+ {"prompt": "SEED:1139027119 | Monitoring alert: elevated error rate on search. File a bug ticket and page the right team.", "seed": 1139027119, "difficulty": "medium"}
132
+ {"prompt": "SEED:1498981529 | A user reported that the payments is returning 500 errors. The issue started 20 minutes ago. Triage this bug: file a ticket with the correct label and priority, assign it to the right team, and notify the correct oncall channel.", "seed": 1498981529, "difficulty": "easy"}
133
+ {"prompt": "SEED:1049193572 | A user reported that the payments is returning 500 errors. The issue started 20 minutes ago. Triage this bug: file a ticket with the correct label and priority, assign it to the right team, and notify the correct oncall channel.", "seed": 1049193572, "difficulty": "medium"}
134
+ {"prompt": "SEED:1224011538 | Support escalation: notifications is intermittently failing for EU users. Triage and route per org process.", "seed": 1224011538, "difficulty": "medium"}
135
+ {"prompt": "SEED:1881988384 | A user reported that the payments is returning 500 errors. The issue started 20 minutes ago. Triage this bug: file a ticket with the correct label and priority, assign it to the right team, and notify the correct oncall channel.", "seed": 1881988384, "difficulty": "medium"}
136
+ {"prompt": "SEED:33599984 | Support escalation: search is intermittently failing for EU users. Triage and route per org process.", "seed": 33599984, "difficulty": "medium"}
137
+ {"prompt": "SEED:444900634 | A user reported that the checkout is returning 500 errors. The issue started 20 minutes ago. Triage this bug: file a ticket with the correct label and priority, assign it to the right team, and notify the correct oncall channel.", "seed": 444900634, "difficulty": "medium"}
138
+ {"prompt": "SEED:1135872495 | A user reported that the api-gateway is returning 500 errors. The issue started 20 minutes ago. Triage this bug: file a ticket with the correct label and priority, assign it to the right team, and notify the correct oncall channel.", "seed": 1135872495, "difficulty": "easy"}
139
+ {"prompt": "SEED:459716009 | A user reported that the checkout is returning 500 errors. The issue started 20 minutes ago. Triage this bug: file a ticket with the correct label and priority, assign it to the right team, and notify the correct oncall channel.", "seed": 459716009, "difficulty": "medium"}
140
+ {"prompt": "SEED:1169726681 | Monitoring alert: elevated error rate on search. File a bug ticket and page the right team.", "seed": 1169726681, "difficulty": "medium"}
141
+ {"prompt": "SEED:904647469 | Support escalation: inventory is intermittently failing for EU users. Triage and route per org process.", "seed": 904647469, "difficulty": "medium"}
142
+ {"prompt": "SEED:874443787 | Monitoring alert: elevated error rate on payments. File a bug ticket and page the right team.", "seed": 874443787, "difficulty": "medium"}
143
+ {"prompt": "SEED:2098228320 | Monitoring alert: elevated error rate on inventory. File a bug ticket and page the right team.", "seed": 2098228320, "difficulty": "medium"}
144
+ {"prompt": "SEED:218179599 | A user reported that the inventory is returning 500 errors. The issue started 20 minutes ago. Triage this bug: file a ticket with the correct label and priority, assign it to the right team, and notify the correct oncall channel.", "seed": 218179599, "difficulty": "easy"}
145
+ {"prompt": "SEED:1819244083 | Monitoring alert: elevated error rate on api-gateway. File a bug ticket and page the right team.", "seed": 1819244083, "difficulty": "medium"}
146
+ {"prompt": "SEED:189351213 | Support escalation: inventory is intermittently failing for EU users. Triage and route per org process.", "seed": 189351213, "difficulty": "easy"}
147
+ {"prompt": "SEED:1432614422 | Monitoring alert: elevated error rate on search. File a bug ticket and page the right team.", "seed": 1432614422, "difficulty": "medium"}
148
+ {"prompt": "SEED:1125089309 | Monitoring alert: elevated error rate on payments. File a bug ticket and page the right team.", "seed": 1125089309, "difficulty": "medium"}
149
+ {"prompt": "SEED:1897669065 | Support escalation: inventory is intermittently failing for EU users. Triage and route per org process.", "seed": 1897669065, "difficulty": "hard"}
150
+ {"prompt": "SEED:41531046 | A user reported that the inventory is returning 500 errors. The issue started 20 minutes ago. Triage this bug: file a ticket with the correct label and priority, assign it to the right team, and notify the correct oncall channel.", "seed": 41531046, "difficulty": "easy"}