Aditya Guntur commited on
Commit
1f213fe
·
1 Parent(s): 48d089d

base iteration

Browse files
.dockerignore ADDED
@@ -0,0 +1,10 @@
 
 
 
 
 
 
 
 
 
 
 
1
+ .venv/
2
+ venv/
3
+ __pycache__/
4
+ *.pyc
5
+ *.pyo
6
+ .git/
7
+ .gitignore
8
+ *.md
9
+ .env
10
+ pdf_pages/
.gitignore ADDED
@@ -0,0 +1,14 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ .venv/
2
+ venv/
3
+ __pycache__/
4
+ *.pyc
5
+ *.pyo
6
+ *.pyd
7
+ .env
8
+ .claude/
9
+ *.log
10
+ pdf_pages/
11
+ *.egg-info/
12
+ dist/
13
+ build/
14
+ .pytest_cache/
Dockerfile ADDED
@@ -0,0 +1,11 @@
 
 
 
 
 
 
 
 
 
 
 
 
1
+ FROM ghcr.io/meta-pytorch/openenv-base:latest
2
+
3
+ WORKDIR /app
4
+ COPY . .
5
+
6
+ RUN uv sync --frozen
7
+
8
+ EXPOSE 8000
9
+
10
+ CMD ["uv", "run", "uvicorn", "server.app:app", \
11
+ "--host", "0.0.0.0", "--port", "8000"]
README.md CHANGED
Binary files a/README.md and b/README.md differ
 
__init__.py ADDED
File without changes
client.py ADDED
@@ -0,0 +1,14 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """OpenEnv client wrapper for PM-Ops environment."""
2
+ import os
3
+ from openenv.core.client import EnvClient
4
+ from pm_ops.models import PMOpsAction, PMOpsObservation
5
+
6
+
7
+ class PMOpsEnv(EnvClient):
8
+ action_class = PMOpsAction
9
+ observation_class = PMOpsObservation
10
+
11
+ def __init__(self, base_url: str = None, token: str = None):
12
+ url = base_url or os.getenv("API_BASE_URL", "https://adityaguntur-pm-ops.hf.space")
13
+ tok = token or os.getenv("HF_TOKEN")
14
+ super().__init__(base_url=url, token=tok)
inference.py ADDED
@@ -0,0 +1,122 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Baseline inference script for PM-Ops environment."""
2
+ import json
3
+ import os
4
+ import requests
5
+
6
+ API_BASE_URL = os.getenv("API_BASE_URL", "https://adityaguntur-pm-ops.hf.space")
7
+ MODEL_NAME = os.getenv("MODEL_NAME", "claude-opus-4-5")
8
+ HF_TOKEN = os.getenv("HF_TOKEN")
9
+ LOCAL_IMAGE_NAME = os.getenv("LOCAL_IMAGE_NAME")
10
+
11
+ MAX_STEPS = 40
12
+ TASK_IDS = ["triage", "incident_routing", "release_notes", "dep_update"]
13
+
14
+
15
+ def call_action(session, base_url, action_type, args=None):
16
+ payload = {"action": {"action_type": action_type, "args": args or {}}}
17
+ resp = session.post(f"{base_url}/step", json=payload)
18
+ resp.raise_for_status()
19
+ return resp.json()
20
+
21
+
22
+ def baseline_agent(obs, session, base_url):
23
+ """Simple heuristic baseline: read runbook, then create ticket + notify."""
24
+ step = obs.get("step", 0)
25
+ task_brief = obs.get("task_brief", "")
26
+
27
+ if step == 0:
28
+ return "meta.read_runbook", {}
29
+
30
+ last = obs.get("last_action_result", {})
31
+ data = last.get("data", {})
32
+
33
+ if step == 1 and isinstance(data, dict) and "org_config" in data:
34
+ org = data["org_config"]
35
+ label_taxonomy = org.get("label_taxonomy", {})
36
+ priority_levels = org.get("priority_levels", ["P1"])
37
+ label = list(label_taxonomy.values())[0] if label_taxonomy else "bug"
38
+ priority = priority_levels[1] if len(priority_levels) > 1 else priority_levels[0]
39
+ return "ticketing.create_ticket", {
40
+ "summary": f"Issue from task: {task_brief[:80]}",
41
+ "description": task_brief,
42
+ "label": label,
43
+ "priority": priority,
44
+ }
45
+
46
+ if step == 2 and isinstance(data, dict) and "id" in data:
47
+ ticket_id = data["id"]
48
+ return "ticketing.assign_ticket", {"ticket_id": ticket_id, "team": "backend"}
49
+
50
+ if step == 3:
51
+ last_result = obs.get("last_action_result", {})
52
+ org_data = {}
53
+ return "chat.list_channels", {}
54
+
55
+ if step == 4:
56
+ channels = last.get("data", [])
57
+ if channels:
58
+ channel = channels[0]
59
+ return "chat.post_message", {
60
+ "channel": channel,
61
+ "text": f"Incident notification: {task_brief[:120]}",
62
+ }
63
+
64
+ return "meta.finish", {}
65
+
66
+
67
+ def run_episode(task_id, session, base_url):
68
+ resp = session.post(f"{base_url}/reset", json={})
69
+ resp.raise_for_status()
70
+ obs = resp.json()
71
+
72
+ rewards = []
73
+ n = 0
74
+ done = False
75
+ success = False
76
+ score = 0.0
77
+
78
+ print(f"[START] task={task_id} env=pm_ops model={MODEL_NAME}", flush=True)
79
+
80
+ while not done and n < MAX_STEPS:
81
+ action_type, args = baseline_agent(obs, session, base_url)
82
+ action_str = json.dumps({"action_type": action_type, "args": args})
83
+
84
+ try:
85
+ result = call_action(session, base_url, action_type, args)
86
+ r = float(result.get("reward", 0.0))
87
+ done = bool(result.get("done", False))
88
+ err = result.get("last_action_result", {}).get("error", "none")
89
+ obs = result
90
+ except Exception as e:
91
+ r = 0.0
92
+ done = True
93
+ err = str(e)
94
+
95
+ rewards.append(r)
96
+ n += 1
97
+ print(f"[STEP] step={n} action={action_type} reward={r:.2f} done={done} error={err}", flush=True)
98
+
99
+ if done:
100
+ score = r
101
+ success = r > 0.5
102
+
103
+ rewards_str = ",".join(f"{x:.2f}" for x in rewards)
104
+ print(f"[END] success={success} steps={n} score={score:.2f} rewards={rewards_str}", flush=True)
105
+ return score
106
+
107
+
108
+ def main():
109
+ session = requests.Session()
110
+ if HF_TOKEN:
111
+ session.headers["Authorization"] = f"Bearer {HF_TOKEN}"
112
+
113
+ total_score = 0.0
114
+ for task_id in TASK_IDS:
115
+ score = run_episode(task_id, session, API_BASE_URL)
116
+ total_score += score
117
+
118
+ print(f"[SUMMARY] avg_score={total_score / len(TASK_IDS):.3f}", flush=True)
119
+
120
+
121
+ if __name__ == "__main__":
122
+ main()
models.py ADDED
@@ -0,0 +1,49 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from typing import Any, Dict, List, Literal, Optional
2
+ from pydantic import Field
3
+ from openenv.core.env_server.types import Action, Observation, State
4
+
5
+
6
+ class PMOpsAction(Action):
7
+ action_type: Literal[
8
+ "ticketing.create_ticket",
9
+ "ticketing.update_ticket",
10
+ "ticketing.get_ticket",
11
+ "ticketing.list_tickets",
12
+ "ticketing.assign_ticket",
13
+ "ticketing.comment_ticket",
14
+ "ticketing.transition_ticket",
15
+ "codebase.list_commits",
16
+ "codebase.get_commit",
17
+ "codebase.list_prs",
18
+ "chat.post_message",
19
+ "chat.read_channel",
20
+ "chat.list_channels",
21
+ "chat.search",
22
+ "meta.read_runbook",
23
+ "meta.finish",
24
+ "meta.noop",
25
+ ] = Field(default="meta.noop")
26
+ args: Dict[str, Any] = Field(default_factory=dict)
27
+
28
+
29
+ class PMOpsObservation(Observation):
30
+ step: int = Field(default=0)
31
+ max_steps: int = Field(default=40)
32
+ task_brief: str = Field(default="")
33
+ last_action_result: Dict[str, Any] = Field(default_factory=dict)
34
+ app_state_deltas: Dict[str, List[Any]] = Field(
35
+ default_factory=lambda: {"ticketing": [], "chat": [], "codebase": []}
36
+ )
37
+ steps_remaining: int = Field(default=40)
38
+ token_budget_remaining: int = Field(default=12000)
39
+
40
+
41
+ class PMOpsState(State):
42
+ org_config: Dict[str, Any] = Field(default_factory=dict)
43
+ task_config: Dict[str, Any] = Field(default_factory=dict)
44
+ ticketing: Dict[str, Any] = Field(default_factory=dict)
45
+ chat: Dict[str, Any] = Field(default_factory=dict)
46
+ codebase: Dict[str, Any] = Field(default_factory=dict)
47
+ step_count: int = Field(default=0)
48
+ finished: bool = Field(default=False)
49
+ episode_id: str = Field(default="")
openenv.yaml ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ spec_version: 1
2
+ name: pm_ops
3
+ type: space
4
+ runtime: fastapi
5
+ app: server.app:app
6
+ port: 8000
pyproject.toml ADDED
@@ -0,0 +1,25 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ [build-system]
2
+ requires = ["setuptools>=68", "wheel"]
3
+ build-backend = "setuptools.backends.legacy:build"
4
+
5
+ [project]
6
+ name = "pm_ops"
7
+ version = "0.1.0"
8
+ requires-python = ">=3.11"
9
+ dependencies = [
10
+ "openenv-core>=0.2.2",
11
+ "fastapi>=0.110.0",
12
+ "uvicorn[standard]>=0.29.0",
13
+ "pydantic>=2.0.0",
14
+ ]
15
+
16
+ [tool.setuptools]
17
+ packages = ["pm_ops", "pm_ops.server", "pm_ops.server.apps", "pm_ops.server.world", "pm_ops.server.tasks", "pm_ops.server.verifiers"]
18
+
19
+ [tool.setuptools.package-dir]
20
+ "pm_ops" = "."
21
+ "pm_ops.server" = "server"
22
+ "pm_ops.server.apps" = "server/apps"
23
+ "pm_ops.server.world" = "server/world"
24
+ "pm_ops.server.tasks" = "server/tasks"
25
+ "pm_ops.server.verifiers" = "server/verifiers"
server/__init__.py ADDED
File without changes
server/app.py ADDED
@@ -0,0 +1,30 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from openenv.core.env_server.http_server import create_app
2
+ from pm_ops.models import PMOpsAction, PMOpsObservation
3
+ from pm_ops.server.pm_ops_environment import PMOpsEnvironment
4
+
5
+
6
+ def create_environment():
7
+ return PMOpsEnvironment()
8
+
9
+
10
+ app = create_app(
11
+ create_environment,
12
+ PMOpsAction,
13
+ PMOpsObservation,
14
+ env_name="pm_ops",
15
+ max_concurrent_envs=8,
16
+ )
17
+
18
+
19
+ @app.get("/")
20
+ def root():
21
+ return {"name": "pm_ops", "status": "running"}
22
+
23
+
24
+ def main():
25
+ import uvicorn
26
+ uvicorn.run(app, host="0.0.0.0", port=8000)
27
+
28
+
29
+ if __name__ == "__main__":
30
+ main()
server/apps/__init__.py ADDED
File without changes
server/apps/chat.py ADDED
@@ -0,0 +1,48 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import copy
2
+ from typing import Any, Dict, List
3
+
4
+
5
+ class ChatApp:
6
+ def __init__(self, channels: List[str], noise_channels: List[str]):
7
+ all_channels = list(dict.fromkeys(channels + noise_channels))
8
+ self.channels = all_channels
9
+ self.messages: Dict[str, List[Dict]] = {ch: [] for ch in all_channels}
10
+ self._episode_messages: List[Dict] = []
11
+
12
+ def snapshot(self) -> Dict[str, Any]:
13
+ return {
14
+ "channels": self.channels,
15
+ "messages_posted_this_episode": copy.deepcopy(self._episode_messages),
16
+ }
17
+
18
+ def post_message(self, args: Dict[str, Any]) -> Dict[str, Any]:
19
+ channel = args.get("channel", "")
20
+ text = args.get("text", "")
21
+ if not channel or not text:
22
+ return {"ok": False, "error": "Both 'channel' and 'text' are required"}
23
+ if channel not in self.channels:
24
+ return {"ok": False, "error": f"Channel '{channel}' not found. Use chat.list_channels to see available channels."}
25
+ msg = {"channel": channel, "text": text}
26
+ self.messages[channel].append(msg)
27
+ self._episode_messages.append(msg)
28
+ return {"ok": True, "data": {"channel": channel, "delivered": True}, "side_effects": [f"message posted to {channel}"]}
29
+
30
+ def read_channel(self, args: Dict[str, Any]) -> Dict[str, Any]:
31
+ channel = args.get("channel", "")
32
+ limit = min(args.get("limit", 20), 50)
33
+ if channel not in self.channels:
34
+ return {"ok": False, "error": f"Channel '{channel}' not found"}
35
+ msgs = self.messages[channel][-limit:]
36
+ return {"ok": True, "data": {"channel": channel, "messages": copy.deepcopy(msgs)}}
37
+
38
+ def list_channels(self, args: Dict[str, Any]) -> Dict[str, Any]:
39
+ return {"ok": True, "data": self.channels}
40
+
41
+ def search(self, args: Dict[str, Any]) -> Dict[str, Any]:
42
+ query = args.get("query", "").lower()
43
+ results = []
44
+ for ch, msgs in self.messages.items():
45
+ for m in msgs:
46
+ if query in m["text"].lower():
47
+ results.append({"channel": ch, "text": m["text"]})
48
+ return {"ok": True, "data": results[:20]}
server/apps/codebase.py ADDED
@@ -0,0 +1,60 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import copy
2
+ import random
3
+ from typing import Any, Dict, List
4
+
5
+ _COMMIT_POOL = [
6
+ {"hash": "a1b2c3d", "message": "fix: null pointer in auth middleware", "author": "alice", "files": ["auth/middleware.py"], "service": "auth"},
7
+ {"hash": "e4f5g6h", "message": "feat: rate limiting for API gateway", "author": "bob", "files": ["gateway/ratelimit.py"], "service": "api-gateway"},
8
+ {"hash": "i7j8k9l", "message": "refactor: database connection pool", "author": "carol", "files": ["db/pool.py"], "service": "auth"},
9
+ {"hash": "m1n2o3p", "message": "fix: SQL injection in user search", "author": "dave", "files": ["search/query.py"], "service": "search"},
10
+ {"hash": "q4r5s6t", "message": "perf: Redis session caching", "author": "eve", "files": ["payments/session.py"], "service": "payments"},
11
+ {"hash": "u7v8w9x", "message": "feat: dark mode dashboard", "author": "frank", "files": ["frontend/theme.js"], "service": "notifications"},
12
+ {"hash": "y1z2a3b", "message": "fix: XSS in comment renderer", "author": "alice", "files": ["frontend/comments.jsx"], "service": "notifications"},
13
+ {"hash": "c4d5e6f", "message": "docs: API authentication guide", "author": "bob", "files": ["docs/auth.md"], "service": "auth"},
14
+ {"hash": "g7h8i9j", "message": "feat: CSV data export endpoint", "author": "carol", "files": ["api/export.py"], "service": "api-gateway"},
15
+ {"hash": "k1l2m3n", "message": "fix: websocket memory leak", "author": "dave", "files": ["payments/ws.py"], "service": "payments"},
16
+ {"hash": "p2q3r4s", "message": "fix: checkout timeout on slow networks", "author": "eve", "files": ["checkout/flow.py"], "service": "checkout"},
17
+ {"hash": "t5u6v7w", "message": "perf: inventory cache warm-up", "author": "frank", "files": ["inventory/cache.py"], "service": "inventory"},
18
+ ]
19
+
20
+ _PR_POOL = [
21
+ {"id": "PR-101", "title": "Fix auth rate limiting bug", "author": "alice", "status": "merged", "service": "auth"},
22
+ {"id": "PR-102", "title": "Add payment retry logic", "author": "bob", "status": "open", "service": "payments"},
23
+ {"id": "PR-103", "title": "Search index optimization", "author": "carol", "status": "merged", "service": "search"},
24
+ {"id": "PR-104", "title": "Gateway health check endpoint", "author": "dave", "status": "open", "service": "api-gateway"},
25
+ {"id": "PR-105", "title": "Checkout flow improvements", "author": "eve", "status": "merged", "service": "checkout"},
26
+ ]
27
+
28
+
29
+ class CodebaseApp:
30
+ def __init__(self, seed: int, services: List[str]):
31
+ rng = random.Random(seed + 1337)
32
+ relevant = [c for c in _COMMIT_POOL if c["service"] in services]
33
+ other = [c for c in _COMMIT_POOL if c["service"] not in services]
34
+ pool = relevant + rng.sample(other, min(3, len(other)))
35
+ self.commits = rng.sample(pool, min(8, len(pool)))
36
+ self.prs = [p for p in _PR_POOL if p["service"] in services]
37
+
38
+ def list_commits(self, args: Dict[str, Any]) -> Dict[str, Any]:
39
+ query = args.get("query", "").lower()
40
+ service = args.get("service")
41
+ limit = min(args.get("limit", 10), 20)
42
+ offset = args.get("offset", 0)
43
+ results = self.commits
44
+ if query:
45
+ results = [c for c in results if query in c["message"].lower() or query in c["hash"]]
46
+ if service:
47
+ results = [c for c in results if c.get("service") == service]
48
+ return {"ok": True, "data": results[offset: offset + limit], "total": len(results)}
49
+
50
+ def get_commit(self, args: Dict[str, Any]) -> Dict[str, Any]:
51
+ h = args.get("hash", "")
52
+ for c in self.commits:
53
+ if c["hash"].startswith(h):
54
+ return {"ok": True, "data": copy.deepcopy(c)}
55
+ return {"ok": False, "error": f"Commit '{h}' not found"}
56
+
57
+ def list_prs(self, args: Dict[str, Any]) -> Dict[str, Any]:
58
+ status = args.get("status")
59
+ results = self.prs if not status else [p for p in self.prs if p["status"] == status]
60
+ return {"ok": True, "data": copy.deepcopy(results)}
server/apps/ticketing.py ADDED
@@ -0,0 +1,107 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import copy
2
+ from typing import Any, Dict, List, Optional
3
+
4
+
5
+ class TicketingApp:
6
+ def __init__(self, org_config: Dict[str, Any]):
7
+ self.org = org_config
8
+ self.tickets: Dict[str, Dict] = {}
9
+ self.counter = 0
10
+ self._episode_created: List[str] = []
11
+
12
+ def snapshot(self) -> Dict[str, Any]:
13
+ return copy.deepcopy({
14
+ "tickets": list(self.tickets.values()),
15
+ "episode_created_ids": self._episode_created,
16
+ })
17
+
18
+ def create_ticket(self, args: Dict[str, Any]) -> Dict[str, Any]:
19
+ required = self.org.get("required_ticket_fields", ["summary"])
20
+ missing = [f for f in required if not args.get(f)]
21
+ if missing:
22
+ return {"ok": False, "error": f"Missing required fields: {missing}. Required: {required}"}
23
+
24
+ self.counter += 1
25
+ ticket_id = f"T-{self.counter:04d}"
26
+ ticket = {
27
+ "id": ticket_id,
28
+ "summary": args.get("summary", ""),
29
+ "description": args.get("description", ""),
30
+ "label": args.get("label"),
31
+ "priority": args.get("priority"),
32
+ "status": "open",
33
+ "assigned_team": None,
34
+ "linked_pr": args.get("linked_pr"),
35
+ "comments": [],
36
+ "created_this_episode": True,
37
+ }
38
+
39
+ valid_labels = list(self.org["label_taxonomy"].values())
40
+ if ticket["label"] and ticket["label"] not in valid_labels:
41
+ return {"ok": False, "error": f"Invalid label '{ticket['label']}'. Valid labels: {valid_labels}"}
42
+
43
+ valid_priorities = self.org["priority_levels"]
44
+ if ticket["priority"] and ticket["priority"] not in valid_priorities:
45
+ return {"ok": False, "error": f"Invalid priority '{ticket['priority']}'. Valid: {valid_priorities}"}
46
+
47
+ self.tickets[ticket_id] = ticket
48
+ self._episode_created.append(ticket_id)
49
+ return {"ok": True, "data": copy.deepcopy(ticket), "side_effects": [f"ticket {ticket_id} created"]}
50
+
51
+ def update_ticket(self, args: Dict[str, Any]) -> Dict[str, Any]:
52
+ tid = args.get("ticket_id")
53
+ if not tid or tid not in self.tickets:
54
+ return {"ok": False, "error": f"Ticket '{tid}' not found"}
55
+ t = self.tickets[tid]
56
+ for k in ("summary", "description", "label", "priority", "linked_pr"):
57
+ if k in args:
58
+ t[k] = args[k]
59
+ return {"ok": True, "data": copy.deepcopy(t), "side_effects": [f"ticket {tid} updated"]}
60
+
61
+ def get_ticket(self, args: Dict[str, Any]) -> Dict[str, Any]:
62
+ tid = args.get("ticket_id")
63
+ t = self.tickets.get(tid)
64
+ if not t:
65
+ return {"ok": False, "error": f"Ticket '{tid}' not found"}
66
+ return {"ok": True, "data": copy.deepcopy(t)}
67
+
68
+ def list_tickets(self, args: Dict[str, Any]) -> Dict[str, Any]:
69
+ tickets = list(self.tickets.values())
70
+ if args.get("status"):
71
+ tickets = [t for t in tickets if t.get("status") == args["status"]]
72
+ if args.get("label"):
73
+ tickets = [t for t in tickets if t.get("label") == args["label"]]
74
+ limit = min(args.get("limit", 20), 50)
75
+ offset = args.get("offset", 0)
76
+ page = tickets[offset: offset + limit]
77
+ return {"ok": True, "data": page, "total": len(tickets)}
78
+
79
+ def assign_ticket(self, args: Dict[str, Any]) -> Dict[str, Any]:
80
+ tid = args.get("ticket_id")
81
+ team = args.get("team")
82
+ if not tid or tid not in self.tickets:
83
+ return {"ok": False, "error": f"Ticket '{tid}' not found"}
84
+ valid_teams = list(self.org["team_map"].values())
85
+ if team not in valid_teams:
86
+ return {"ok": False, "error": f"Team '{team}' not found. Valid teams: {valid_teams}"}
87
+ self.tickets[tid]["assigned_team"] = team
88
+ return {"ok": True, "data": {"ticket_id": tid, "assigned_team": team}, "side_effects": [f"ticket {tid} assigned to {team}"]}
89
+
90
+ def comment_ticket(self, args: Dict[str, Any]) -> Dict[str, Any]:
91
+ tid = args.get("ticket_id")
92
+ text = args.get("text", "")
93
+ if not tid or tid not in self.tickets:
94
+ return {"ok": False, "error": f"Ticket '{tid}' not found"}
95
+ self.tickets[tid]["comments"].append({"text": text})
96
+ return {"ok": True, "data": {"comment": "added"}, "side_effects": [f"comment added to {tid}"]}
97
+
98
+ def transition_ticket(self, args: Dict[str, Any]) -> Dict[str, Any]:
99
+ tid = args.get("ticket_id")
100
+ status = args.get("status", "closed")
101
+ valid = ["open", "in_progress", "review", "closed", "wont_fix"]
102
+ if status not in valid:
103
+ return {"ok": False, "error": f"Invalid status. Valid: {valid}"}
104
+ if not tid or tid not in self.tickets:
105
+ return {"ok": False, "error": f"Ticket '{tid}' not found"}
106
+ self.tickets[tid]["status"] = status
107
+ return {"ok": True, "data": {"ticket_id": tid, "status": status}, "side_effects": [f"ticket {tid} transitioned to {status}"]}
server/pm_ops_environment.py ADDED
@@ -0,0 +1,202 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import copy
2
+ import random
3
+ import uuid
4
+ from typing import Any, Dict, Optional
5
+
6
+ from pm_ops.models import PMOpsAction, PMOpsObservation
7
+ from pm_ops.server.apps.ticketing import TicketingApp
8
+ from pm_ops.server.apps.codebase import CodebaseApp
9
+ from pm_ops.server.apps.chat import ChatApp
10
+ from pm_ops.server.world.org_generator import generate_org_config
11
+ from pm_ops.server.world.scenario_gen import generate_scenario
12
+ from pm_ops.server.tasks.triage_task import TriageTask
13
+ from pm_ops.server.tasks.incident_routing_task import IncidentRoutingTask
14
+ from pm_ops.server.tasks.release_notes_task import ReleaseNotesTask
15
+ from pm_ops.server.tasks.dep_update_task import DepUpdateTask
16
+
17
+ MAX_STEPS = 40
18
+ _TASK_TYPES = ["triage", "incident_routing", "release_notes", "dep_update"]
19
+ _DIFFICULTY_POOL = ["easy", "medium", "medium", "hard"]
20
+ _TASK_GRADERS = {
21
+ "triage": TriageTask(),
22
+ "incident_routing": IncidentRoutingTask(),
23
+ "release_notes": ReleaseNotesTask(),
24
+ "dep_update": DepUpdateTask(),
25
+ }
26
+
27
+
28
+ def _oracle_check(scenario: Dict[str, Any]) -> bool:
29
+ expected = scenario.get("expected", {})
30
+ if not expected:
31
+ return False
32
+ channel = expected.get("channel") or expected.get("channels")
33
+ team = expected.get("team") or expected.get("teams_to_notify")
34
+ return bool(channel) and (bool(team) or scenario["type"] == "release_notes")
35
+
36
+
37
+ class PMOpsEnvironment:
38
+ def __init__(self):
39
+ self._ticketing: Optional[TicketingApp] = None
40
+ self._codebase: Optional[CodebaseApp] = None
41
+ self._chat: Optional[ChatApp] = None
42
+ self._org_config: Optional[Dict[str, Any]] = None
43
+ self._scenario: Optional[Dict[str, Any]] = None
44
+ self._step_count: int = 0
45
+ self._done: bool = False
46
+
47
+ def reset(self) -> PMOpsObservation:
48
+ seed = random.randint(0, 2 ** 31)
49
+ rng = random.Random(seed)
50
+ difficulty = rng.choice(_DIFFICULTY_POOL)
51
+ task_type = rng.choice(_TASK_TYPES)
52
+
53
+ for attempt in range(10):
54
+ org = generate_org_config(seed + attempt, difficulty)
55
+ scenario = generate_scenario(task_type, org, seed + attempt)
56
+ if _oracle_check(scenario):
57
+ break
58
+
59
+ channels = list(org["oncall_channels"].values())
60
+ noise = org.get("noise_channels", [])
61
+
62
+ self._org_config = org
63
+ self._scenario = scenario
64
+ self._ticketing = TicketingApp(org)
65
+ self._codebase = CodebaseApp(seed, org["services"])
66
+ self._chat = ChatApp(channels, noise)
67
+ self._step_count = 0
68
+ self._done = False
69
+
70
+ return PMOpsObservation(
71
+ step=0,
72
+ max_steps=MAX_STEPS,
73
+ task_brief=scenario["brief"],
74
+ last_action_result={
75
+ "ok": True,
76
+ "data": "Environment ready. Call meta.read_runbook to learn this org's conventions.",
77
+ },
78
+ app_state_deltas={"ticketing": [], "chat": [], "codebase": []},
79
+ steps_remaining=MAX_STEPS,
80
+ token_budget_remaining=12000,
81
+ reward=0.0,
82
+ done=False,
83
+ )
84
+
85
+ def step(self, action: PMOpsAction) -> PMOpsObservation:
86
+ if self._ticketing is None:
87
+ raise RuntimeError("Call reset() before step()")
88
+
89
+ if self._done:
90
+ return PMOpsObservation(
91
+ step=self._step_count,
92
+ max_steps=MAX_STEPS,
93
+ task_brief=self._scenario["brief"],
94
+ last_action_result={"ok": False, "error": "Episode already finished"},
95
+ app_state_deltas={"ticketing": [], "chat": [], "codebase": []},
96
+ steps_remaining=0,
97
+ token_budget_remaining=0,
98
+ reward=0.0,
99
+ done=True,
100
+ )
101
+
102
+ self._step_count += 1
103
+ result = self._dispatch(action)
104
+
105
+ done = action.action_type == "meta.finish" or self._step_count >= MAX_STEPS
106
+ reward = 0.0
107
+ if done:
108
+ reward = self._grade()
109
+ self._done = True
110
+
111
+ side_effects = result.get("side_effects", [])
112
+ deltas = {
113
+ "ticketing": side_effects if any("ticket" in s for s in side_effects) else [],
114
+ "chat": side_effects if any("message" in s for s in side_effects) else [],
115
+ "codebase": [],
116
+ }
117
+
118
+ return PMOpsObservation(
119
+ step=self._step_count,
120
+ max_steps=MAX_STEPS,
121
+ task_brief=self._scenario["brief"],
122
+ last_action_result=result,
123
+ app_state_deltas=deltas,
124
+ steps_remaining=max(0, MAX_STEPS - self._step_count),
125
+ token_budget_remaining=max(0, 12000 - self._step_count * 300),
126
+ reward=reward,
127
+ done=done,
128
+ )
129
+
130
+ def _dispatch(self, action: PMOpsAction) -> Dict[str, Any]:
131
+ at = action.action_type
132
+ args = action.args or {}
133
+
134
+ if at == "meta.noop":
135
+ return {"ok": True, "data": "No operation."}
136
+
137
+ if at == "meta.read_runbook":
138
+ return {
139
+ "ok": True,
140
+ "data": {
141
+ "org_config": copy.deepcopy(self._org_config),
142
+ "hint": (
143
+ "Use label_taxonomy for valid ticket labels, "
144
+ "priority_levels for valid priorities, "
145
+ "team_map[service] to find the owning team, "
146
+ "oncall_channels[service] to find the channel to notify."
147
+ ),
148
+ },
149
+ }
150
+
151
+ if at == "meta.finish":
152
+ return {"ok": True, "data": "Episode finishing. Score will be computed."}
153
+
154
+ if at.startswith("ticketing."):
155
+ op = at.split(".", 1)[1]
156
+ handlers = {
157
+ "create_ticket": self._ticketing.create_ticket,
158
+ "update_ticket": self._ticketing.update_ticket,
159
+ "get_ticket": self._ticketing.get_ticket,
160
+ "list_tickets": self._ticketing.list_tickets,
161
+ "assign_ticket": self._ticketing.assign_ticket,
162
+ "comment_ticket": self._ticketing.comment_ticket,
163
+ "transition_ticket": self._ticketing.transition_ticket,
164
+ }
165
+ if op not in handlers:
166
+ return {"ok": False, "error": f"Unknown ticketing action: {op}"}
167
+ return handlers[op](args)
168
+
169
+ if at.startswith("codebase."):
170
+ op = at.split(".", 1)[1]
171
+ handlers = {
172
+ "list_commits": self._codebase.list_commits,
173
+ "get_commit": self._codebase.get_commit,
174
+ "list_prs": self._codebase.list_prs,
175
+ }
176
+ if op not in handlers:
177
+ return {"ok": False, "error": f"Unknown codebase action: {op}"}
178
+ return handlers[op](args)
179
+
180
+ if at.startswith("chat."):
181
+ op = at.split(".", 1)[1]
182
+ handlers = {
183
+ "post_message": self._chat.post_message,
184
+ "read_channel": self._chat.read_channel,
185
+ "list_channels": self._chat.list_channels,
186
+ "search": self._chat.search,
187
+ }
188
+ if op not in handlers:
189
+ return {"ok": False, "error": f"Unknown chat action: {op}"}
190
+ return handlers[op](args)
191
+
192
+ return {"ok": False, "error": f"Unknown action_type: {at}"}
193
+
194
+ def _grade(self) -> float:
195
+ final_state = {
196
+ "ticketing": self._ticketing.snapshot(),
197
+ "chat": self._chat.snapshot(),
198
+ }
199
+ grader = _TASK_GRADERS.get(self._scenario["type"])
200
+ if not grader:
201
+ return 0.0
202
+ return grader.grade(final_state, self._org_config, self._scenario)
server/tasks/__init__.py ADDED
File without changes
server/tasks/base_task.py ADDED
@@ -0,0 +1,8 @@
 
 
 
 
 
 
 
 
 
1
+ from abc import ABC, abstractmethod
2
+ from typing import Any, Dict
3
+
4
+
5
+ class BaseTask(ABC):
6
+ @abstractmethod
7
+ def grade(self, final_state: Dict[str, Any], org_config: Dict[str, Any], scenario: Dict[str, Any]) -> float:
8
+ ...
server/tasks/dep_update_task.py ADDED
@@ -0,0 +1,40 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from typing import Any, Dict
2
+ from pm_ops.server.tasks.base_task import BaseTask
3
+
4
+
5
+ class DepUpdateTask(BaseTask):
6
+ def grade(self, final_state: Dict[str, Any], org_config: Dict[str, Any], scenario: Dict[str, Any]) -> float:
7
+ score = 0.0
8
+ expected = scenario["expected"]
9
+
10
+ msgs = final_state["chat"]["messages_posted_this_episode"]
11
+ tickets = final_state["ticketing"]["tickets"]
12
+ new_tickets = [t for t in tickets if t.get("created_this_episode")]
13
+
14
+ expected_channels = set(expected.get("channels", []))
15
+ expected_teams = set(expected.get("teams_to_notify", []))
16
+
17
+ notified = {m.get("channel") for m in msgs}
18
+ correct_notified = notified & expected_channels
19
+ wrong_notified = notified - expected_channels
20
+
21
+ if correct_notified:
22
+ score += 0.25 * (len(correct_notified) / max(len(expected_channels), 1))
23
+
24
+ assigned_teams = {t.get("assigned_team") for t in new_tickets if t.get("assigned_team")}
25
+ correct_teams = assigned_teams & expected_teams
26
+ if correct_teams:
27
+ score += 0.25 * (len(correct_teams) / max(len(expected_teams), 1))
28
+
29
+ if new_tickets:
30
+ score += 0.25
31
+
32
+ policy = expected.get("escalation_policy", "post-channel")
33
+ if policy == "post-channel" and correct_notified:
34
+ score += 0.25
35
+ elif policy in ("dm-manager", "page-oncall") and msgs:
36
+ score += 0.25
37
+
38
+ score -= 0.05 * len(wrong_notified)
39
+
40
+ return max(0.0, min(1.0, score))
server/tasks/incident_routing_task.py ADDED
@@ -0,0 +1,33 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from typing import Any, Dict
2
+ from pm_ops.server.tasks.base_task import BaseTask
3
+
4
+
5
+ class IncidentRoutingTask(BaseTask):
6
+ def grade(self, final_state: Dict[str, Any], org_config: Dict[str, Any], scenario: Dict[str, Any]) -> float:
7
+ score = 0.0
8
+ expected = scenario["expected"]
9
+
10
+ msgs = final_state["chat"]["messages_posted_this_episode"]
11
+ tickets = final_state["ticketing"]["tickets"]
12
+ new_tickets = [t for t in tickets if t.get("created_this_episode")]
13
+
14
+ correct_msgs = [m for m in msgs if m.get("channel") == expected.get("channel")]
15
+ wrong_msgs = [m for m in msgs if m.get("channel") != expected.get("channel")]
16
+
17
+ if correct_msgs:
18
+ score += 0.30
19
+
20
+ if new_tickets:
21
+ t = new_tickets[0]
22
+ if t.get("label") == expected.get("label"):
23
+ score += 0.20
24
+ if t.get("priority") == expected.get("priority"):
25
+ score += 0.20
26
+ if t.get("assigned_team") == expected.get("team"):
27
+ score += 0.15
28
+ if t.get("linked_pr"):
29
+ score += 0.05
30
+
31
+ score -= 0.15 * len(wrong_msgs)
32
+
33
+ return max(0.0, min(1.0, score))
server/tasks/release_notes_task.py ADDED
@@ -0,0 +1,42 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from typing import Any, Dict
2
+ from pm_ops.server.tasks.base_task import BaseTask
3
+
4
+
5
+ class ReleaseNotesTask(BaseTask):
6
+ def grade(self, final_state: Dict[str, Any], org_config: Dict[str, Any], scenario: Dict[str, Any]) -> float:
7
+ score = 0.0
8
+ expected = scenario["expected"]
9
+
10
+ tickets = final_state["ticketing"]["tickets"]
11
+ closed = [t for t in tickets if t.get("status") == "closed"]
12
+
13
+ msgs = final_state["chat"]["messages_posted_this_episode"]
14
+ correct_msgs = [m for m in msgs if m.get("channel") == expected.get("channel")]
15
+ wrong_msgs = [m for m in msgs if m.get("channel") != expected.get("channel")]
16
+
17
+ if not correct_msgs:
18
+ score -= 0.10 * len(wrong_msgs)
19
+ return max(0.0, score)
20
+
21
+ msg_text = correct_msgs[0].get("text", "")
22
+ score += 0.30
23
+
24
+ style = expected.get("style", "terse")
25
+ if style == "bulleted" and ("-" in msg_text or "*" in msg_text):
26
+ score += 0.20
27
+ elif style == "terse" and len(msg_text) < 500:
28
+ score += 0.20
29
+ elif style == "verbose" and len(msg_text) >= 200:
30
+ score += 0.20
31
+
32
+ min_tickets = expected.get("min_tickets", 2)
33
+ referenced = sum(1 for t in closed if t["id"] in msg_text)
34
+ if referenced >= min_tickets:
35
+ score += 0.30
36
+ elif referenced > 0:
37
+ score += 0.15
38
+
39
+ score += 0.20 if not wrong_msgs else 0.0
40
+ score -= 0.10 * len(wrong_msgs)
41
+
42
+ return max(0.0, min(1.0, score))
server/tasks/triage_task.py ADDED
@@ -0,0 +1,39 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from typing import Any, Dict
2
+ from pm_ops.server.tasks.base_task import BaseTask
3
+
4
+
5
+ class TriageTask(BaseTask):
6
+ def grade(self, final_state: Dict[str, Any], org_config: Dict[str, Any], scenario: Dict[str, Any]) -> float:
7
+ score = 0.0
8
+ expected = scenario["expected"]
9
+
10
+ tickets = final_state["ticketing"]["tickets"]
11
+ new_tickets = [t for t in tickets if t.get("created_this_episode")]
12
+
13
+ if not new_tickets:
14
+ return 0.0
15
+
16
+ score -= 0.10 * max(0, len(new_tickets) - 1) # duplicate penalty
17
+
18
+ t = new_tickets[0]
19
+ score += 0.25 # ticket exists
20
+
21
+ if t.get("label") == expected.get("label"):
22
+ score += 0.20
23
+ if t.get("priority") == expected.get("priority"):
24
+ score += 0.20
25
+ if t.get("assigned_team") == expected.get("team"):
26
+ score += 0.20
27
+
28
+ msgs = final_state["chat"]["messages_posted_this_episode"]
29
+ correct_msgs = [m for m in msgs if m.get("channel") == expected.get("channel")]
30
+ wrong_msgs = [m for m in msgs if m.get("channel") != expected.get("channel")]
31
+
32
+ if correct_msgs:
33
+ score += 0.10
34
+ if len(correct_msgs[0].get("text", "")) >= 20:
35
+ score += 0.05
36
+
37
+ score -= 0.05 * len(wrong_msgs)
38
+
39
+ return max(0.0, min(1.0, score))
server/verifiers/__init__.py ADDED
File without changes
server/verifiers/state_verifier.py ADDED
@@ -0,0 +1,23 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Deterministic state verifier — no LLM-as-judge."""
2
+ from typing import Any, Dict
3
+
4
+ from pm_ops.server.tasks.triage_task import TriageTask
5
+ from pm_ops.server.tasks.incident_routing_task import IncidentRoutingTask
6
+ from pm_ops.server.tasks.release_notes_task import ReleaseNotesTask
7
+ from pm_ops.server.tasks.dep_update_task import DepUpdateTask
8
+
9
+ _GRADERS = {
10
+ "triage": TriageTask(),
11
+ "incident_routing": IncidentRoutingTask(),
12
+ "release_notes": ReleaseNotesTask(),
13
+ "dep_update": DepUpdateTask(),
14
+ }
15
+
16
+
17
+ def verify(final_state: Dict[str, Any], org_config: Dict[str, Any], scenario: Dict[str, Any]) -> Dict[str, Any]:
18
+ task_type = scenario.get("type", "")
19
+ grader = _GRADERS.get(task_type)
20
+ if not grader:
21
+ return {"score": 0.0, "error": f"Unknown task type: {task_type}"}
22
+ score = grader.grade(final_state, org_config, scenario)
23
+ return {"score": score, "task_type": task_type}
server/world/__init__.py ADDED
File without changes
server/world/org_generator.py ADDED
@@ -0,0 +1,76 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import random
2
+ from typing import Any, Dict, List
3
+
4
+ _LABEL_VARIANTS = {
5
+ "bug": ["bug", "defect", "issue", "fault"],
6
+ "feature": ["feature", "enhancement", "request", "improvement"],
7
+ "docs": ["docs", "documentation", "doc-update", "guide"],
8
+ "security": ["security", "vuln", "vulnerability", "sec-issue"],
9
+ "performance": ["perf", "performance", "slowness", "optimization"],
10
+ }
11
+
12
+ _PRIORITY_SCHEMES = [
13
+ ["P0", "P1", "P2", "P3"],
14
+ ["critical", "high", "medium", "low"],
15
+ ["urgent", "high", "normal", "low"],
16
+ ["blocker", "major", "minor", "trivial"],
17
+ ]
18
+
19
+ _SERVICES = ["payments", "auth", "search", "notifications", "api-gateway", "checkout", "inventory"]
20
+ _TEAM_POOL = ["platform", "growth", "infra", "security", "backend", "frontend", "data"]
21
+ _CHANNEL_SUFFIXES = ["-alerts", "-oncall", "-ops", "-team", "-eng"]
22
+ _NOISE_CHANNELS = ["#random", "#general", "#water-cooler", "#announcements", "#off-topic", "#hiring"]
23
+ _ESCALATION_POLICIES = ["dm-manager", "page-oncall", "post-channel"]
24
+ _RELEASE_STYLES = ["terse", "verbose", "bulleted"]
25
+
26
+
27
+ def generate_org_config(seed: int, difficulty: str = "medium") -> Dict[str, Any]:
28
+ rng = random.Random(seed)
29
+
30
+ n_labels = {"easy": 3, "medium": 4, "hard": 5}.get(difficulty, 4)
31
+ label_keys = rng.sample(list(_LABEL_VARIANTS.keys()), n_labels)
32
+ label_taxonomy = {k: rng.choice(_LABEL_VARIANTS[k]) for k in label_keys}
33
+
34
+ priority_levels: List[str] = rng.choice(_PRIORITY_SCHEMES)
35
+ severity_to_priority = {
36
+ "critical": priority_levels[0],
37
+ "high": priority_levels[1] if len(priority_levels) > 1 else priority_levels[0],
38
+ "medium": priority_levels[2] if len(priority_levels) > 2 else priority_levels[-1],
39
+ "low": priority_levels[-1],
40
+ }
41
+
42
+ n_services = {"easy": 2, "medium": 3, "hard": 5}.get(difficulty, 3)
43
+ services = rng.sample(_SERVICES, min(n_services, len(_SERVICES)))
44
+ teams = rng.sample(_TEAM_POOL, min(len(services), len(_TEAM_POOL)))
45
+
46
+ team_map: Dict[str, str] = {svc: teams[i % len(teams)] for i, svc in enumerate(services)}
47
+
48
+ oncall_channels: Dict[str, str] = {}
49
+ for svc in services:
50
+ team = team_map[svc]
51
+ suffix = rng.choice(_CHANNEL_SUFFIXES)
52
+ oncall_channels[svc] = f"#{team}{suffix}"
53
+
54
+ escalation_policy: str = rng.choice(_ESCALATION_POLICIES)
55
+ release_notes_style: str = rng.choice(_RELEASE_STYLES)
56
+
57
+ all_fields = ["summary", "description", "priority", "label", "assignee"]
58
+ n_required = {"easy": 2, "medium": 3, "hard": 4}.get(difficulty, 3)
59
+ required_ticket_fields = ["summary"] + rng.sample(all_fields[1:], min(n_required - 1, len(all_fields) - 1))
60
+
61
+ n_noise = {"easy": 1, "medium": 2, "hard": 3}.get(difficulty, 2)
62
+ noise_channels = rng.sample(_NOISE_CHANNELS, min(n_noise, len(_NOISE_CHANNELS)))
63
+
64
+ return {
65
+ "label_taxonomy": label_taxonomy,
66
+ "priority_levels": priority_levels,
67
+ "severity_to_priority": severity_to_priority,
68
+ "team_map": team_map,
69
+ "oncall_channels": oncall_channels,
70
+ "escalation_policy": escalation_policy,
71
+ "release_notes_style": release_notes_style,
72
+ "required_ticket_fields": required_ticket_fields,
73
+ "noise_channels": noise_channels,
74
+ "services": services,
75
+ "teams": teams,
76
+ }
server/world/scenario_gen.py ADDED
@@ -0,0 +1,98 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import random
2
+ from typing import Any, Dict
3
+
4
+ _TRIAGE_BRIEFS = [
5
+ "A user reported that the {service} 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.",
6
+ "Support escalation: {service} is intermittently failing for EU users. Triage and route per org process.",
7
+ "Monitoring alert: elevated error rate on {service}. File a bug ticket and page the right team.",
8
+ ]
9
+
10
+ _INCIDENT_BRIEFS = [
11
+ "ALERT: {service} is completely down. This is a {severity_name} incident. Identify the owning team from recent commits and route immediately.",
12
+ "Production incident: {service} spiking to 100% CPU. Escalate properly -- create a ticket and page the oncall channel.",
13
+ "Critical alert: {service} health check failing. Identify the right team via the codebase and notify them.",
14
+ ]
15
+
16
+ _RELEASE_NOTES_BRIEFS = [
17
+ "Prepare release notes for this sprint's closed tickets. Use the org's standard format and post them to the right channel.",
18
+ "Write and publish the weekly release notes for all resolved tickets. Follow the org's release_notes_style.",
19
+ ]
20
+
21
+ _DEP_UPDATE_BRIEFS = [
22
+ "Security advisory: the `requests` library has a CVE. Find all services that depend on it and notify their owning teams per the org's escalation policy.",
23
+ "Dependency update needed: `pydantic` v2 breaking changes affect multiple services. Coordinate the update across all owning teams.",
24
+ ]
25
+
26
+
27
+ def generate_scenario(task_type: str, org_config: Dict[str, Any], seed: int) -> Dict[str, Any]:
28
+ rng = random.Random(seed + 42)
29
+ services = org_config["services"]
30
+ team_map = org_config["team_map"]
31
+ oncall_channels = org_config["oncall_channels"]
32
+ priority_levels = org_config["priority_levels"]
33
+ label_taxonomy = org_config["label_taxonomy"]
34
+
35
+ if task_type == "triage":
36
+ service = rng.choice(services)
37
+ label_key = "bug" if "bug" in label_taxonomy else list(label_taxonomy.keys())[0]
38
+ label = label_taxonomy[label_key]
39
+ priority = priority_levels[1] if len(priority_levels) > 1 else priority_levels[0]
40
+ brief = rng.choice(_TRIAGE_BRIEFS).format(service=service)
41
+ return {
42
+ "type": "triage",
43
+ "brief": brief,
44
+ "service": service,
45
+ "expected": {
46
+ "label": label,
47
+ "priority": priority,
48
+ "team": team_map[service],
49
+ "channel": oncall_channels[service],
50
+ },
51
+ }
52
+
53
+ elif task_type == "incident_routing":
54
+ service = rng.choice(services)
55
+ priority = priority_levels[0]
56
+ severity_name = org_config["severity_to_priority"].get("critical", priority)
57
+ brief = rng.choice(_INCIDENT_BRIEFS).format(service=service, severity_name=severity_name)
58
+ label_key = "bug" if "bug" in label_taxonomy else list(label_taxonomy.keys())[0]
59
+ return {
60
+ "type": "incident_routing",
61
+ "brief": brief,
62
+ "service": service,
63
+ "expected": {
64
+ "label": label_taxonomy[label_key],
65
+ "priority": priority,
66
+ "team": team_map[service],
67
+ "channel": oncall_channels[service],
68
+ },
69
+ }
70
+
71
+ elif task_type == "release_notes":
72
+ brief = rng.choice(_RELEASE_NOTES_BRIEFS)
73
+ channel = rng.choice(list(oncall_channels.values()))
74
+ return {
75
+ "type": "release_notes",
76
+ "brief": brief,
77
+ "expected": {
78
+ "style": org_config["release_notes_style"],
79
+ "channel": channel,
80
+ "min_tickets": 2,
81
+ },
82
+ }
83
+
84
+ elif task_type == "dep_update":
85
+ affected = rng.sample(services, min(2, len(services)))
86
+ brief = rng.choice(_DEP_UPDATE_BRIEFS)
87
+ return {
88
+ "type": "dep_update",
89
+ "brief": brief,
90
+ "affected_services": affected,
91
+ "expected": {
92
+ "teams_to_notify": [team_map[s] for s in affected],
93
+ "channels": [oncall_channels[s] for s in affected],
94
+ "escalation_policy": org_config["escalation_policy"],
95
+ },
96
+ }
97
+
98
+ raise ValueError(f"Unknown task type: {task_type}")
uv.lock ADDED
The diff for this file is too large to render. See raw diff