Spaces:
Sleeping
Sleeping
writing the GRPO script to train qwen1.7 on A100 GRPO
Browse files- training/__init__.py +0 -0
- training/dataset.py +78 -0
- training/prompts.py +76 -0
- training/rewards.py +66 -0
- training/rollout.py +287 -0
- training/smoke_test.py +246 -0
- training/train.ipynb +639 -0
- training/triage_dataset.jsonl +150 -0
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"}
|