prashasti commited on
Commit
205f6c7
·
1 Parent(s): 79a05b5

Initial changes for ai-debugger

Browse files
.gitignore ADDED
@@ -0,0 +1,10 @@
 
 
 
 
 
 
 
 
 
 
 
1
+ __pycache__/
2
+ *.py[cod]
3
+ *.egg-info/
4
+ .env
5
+ .venv/
6
+ venv/
7
+ dist/
8
+ build/
9
+ *.log
10
+ .DS_Store
Dockerfile ADDED
@@ -0,0 +1,13 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ FROM python:3.10-slim
2
+
3
+ WORKDIR /app
4
+
5
+ # Install dependencies first
6
+ COPY requirements.txt .
7
+ RUN pip install --no-cache-dir -r requirements.txt
8
+
9
+ # Copy project
10
+ COPY . .
11
+
12
+ # Default: run inference script across all 3 tasks
13
+ CMD ["python", "inference.py"]
README copy.md ADDED
@@ -0,0 +1,211 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # DebugOps: AI Incident Response Environment
2
+
3
+ ## Overview
4
+
5
+ **DebugOps** is a reinforcement-learning environment that simulates real-world production incident response. An AI agent acts as an on-call Site Reliability Engineer (SRE), diagnosing system failures from noisy logs and degraded metrics, then executing the correct multi-step remediation sequence.
6
+
7
+ The environment models real DevOps/SRE workflows from cloud infrastructure and distributed systems — unlike toy environments, fixes require correct *ordered* sequences and the agent must separate signal from noise in log data.
8
+
9
+ Built with the [OpenEnv](https://github.com/raun/openenv-course) framework for the Meta × PyTorch Hackathon.
10
+
11
+ ---
12
+
13
+ ## Environment Description
14
+
15
+ At each step the agent receives an **observation** containing:
16
+ - **services** — per-service health (api, db, cache)
17
+ - **logs** — system log lines, some of which are noisy red herrings
18
+ - **metrics** — latency (ms), error_rate (0–1), cpu (%)
19
+ - **time_step** — elapsed steps
20
+
21
+ The agent must identify the hidden root cause and execute the correct multi-step fix sequence before the episode times out. System metrics degrade every step, creating real urgency.
22
+
23
+ ---
24
+
25
+ ### Observation Space
26
+
27
+ | Field | Type | Description |
28
+ |---|---|---|
29
+ | `services` | `Dict[str, str]` | Service health: `healthy` or `degraded` |
30
+ | `logs` | `List[str]` | System logs (may include noise/red herrings) |
31
+ | `metrics.latency` | `float` | Current system latency (ms) |
32
+ | `metrics.error_rate` | `float` | Error rate (0.0–1.0) |
33
+ | `metrics.cpu` | `float` | CPU utilisation (%) |
34
+ | `time_step` | `int` | Steps elapsed in this episode |
35
+
36
+ ---
37
+
38
+ ### Action Space (discrete, 5 actions)
39
+
40
+ | Action | Description |
41
+ |---|---|
42
+ | `restart_api` | Restart the API service |
43
+ | `restart_db` | Restart the database service |
44
+ | `restart_cache` | Restart the cache service |
45
+ | `scale_up` | Add compute capacity |
46
+ | `noop` | Take no action |
47
+
48
+ ---
49
+
50
+ ### Root Causes & Fix Sequences
51
+
52
+ | Root Cause | Fix Sequence | Affected Services |
53
+ |---|---|---|
54
+ | `api_timeout` | `scale_up → restart_api` | api |
55
+ | `db_connection_leak` | `restart_db → scale_up` | db |
56
+ | `cache_miss_storm` | `restart_cache → scale_up` | cache |
57
+ | `memory_leak` | `restart_api → restart_db` | api, db |
58
+
59
+ Actions must be performed **in order** — wrong steps degrade metrics further.
60
+
61
+ ---
62
+
63
+ ### Reward Function
64
+
65
+ ```
66
+ reward =
67
+ +150 full resolution bonus
68
+ + 30 correct intermediate fix step
69
+ - 15 wrong action (no progress)
70
+ -0.04 × latency (ms) per step
71
+ - 25 × error_rate per step
72
+ - 2 time penalty per step (escalates after step 10)
73
+ ```
74
+
75
+ The shaped reward provides dense signal throughout the episode, not just at termination.
76
+
77
+ ---
78
+
79
+ ## Tasks
80
+
81
+ | Task | Module | Description | Max Steps | Difficulty |
82
+ |---|---|---|---|---|
83
+ | Simple | `tasks.task_simple` | Single-service failure | 15 | Low |
84
+ | Multi-Service | `tasks.task_multi_service` | Two services degrade simultaneously | 12 | Medium |
85
+ | Critical | `tasks.task_critical` | Memory-leak with misleading logs + SLA penalties | 10 | High |
86
+
87
+ ---
88
+
89
+ ## Setup
90
+
91
+ ```bash
92
+ # Clone the repo
93
+ git clone <your-repo-url>
94
+ cd debugops
95
+
96
+ # Install dependencies
97
+ pip install -r requirements.txt
98
+ ```
99
+
100
+ ---
101
+
102
+ ## Running Inference (LLM Agent)
103
+
104
+ ```bash
105
+ python inference.py
106
+ ```
107
+
108
+ Expected output:
109
+
110
+ ```
111
+ [START] task=simple env=debugops model=Qwen/Qwen2.5-72B-Instruct
112
+ [STEP] step=0 action=scale_up reward=-18.4 done=false error=null
113
+ [STEP] step=1 action=restart_api reward=145.3 done=true error=null
114
+ [END] success=true steps=2 score=0.881 rewards=-18.4,145.3
115
+ ```
116
+
117
+ If `HF_TOKEN` is not set, the heuristic fallback agent is used automatically — no API key required.
118
+
119
+ ---
120
+
121
+ ## Running the Baseline Agent
122
+
123
+ ```bash
124
+ python app.py
125
+ ```
126
+
127
+ ---
128
+
129
+ ## Grader
130
+
131
+ ```bash
132
+ python -m grader.grader
133
+ ```
134
+
135
+ The grader scores each episode in `[0.0, 1.0]` using a weighted formula:
136
+
137
+ | Component | Weight | Description |
138
+ |---|---|---|
139
+ | Resolution | 50% | Was the incident resolved? |
140
+ | Efficiency | 30% | How quickly was it resolved? |
141
+ | Quality | 20% | Normalised average reward |
142
+
143
+ ---
144
+
145
+ ## Pre-submission Validation
146
+
147
+ ```bash
148
+ python scripts/validate_submission.py
149
+ # or
150
+ bash scripts/validate-submission.sh
151
+ ```
152
+
153
+ ---
154
+
155
+ ## Docker
156
+
157
+ ```bash
158
+ # Build
159
+ docker build -t debugops-env .
160
+
161
+ # Run (heuristic agent, no key required)
162
+ docker run debugops-env
163
+
164
+ # Run with LLM agent
165
+ docker run -e HF_TOKEN=your_token \
166
+ -e API_BASE_URL=https://router.huggingface.co/v1 \
167
+ -e MODEL_NAME=Qwen/Qwen2.5-72B-Instruct \
168
+ debugops-env
169
+ ```
170
+
171
+ ---
172
+
173
+ ## Project Structure
174
+
175
+ ```
176
+ .
177
+ ├── inference.py ← Main LLM inference script (required entry point)
178
+ ├── app.py ← Lightweight local runner (baseline agent)
179
+ ├── Dockerfile
180
+ ├── requirements.txt
181
+ ├── openenv.yaml ← OpenEnv spec
182
+ ├── README.md
183
+ ├── env/
184
+ │ ├── environment.py ← DebugEnv class (reset/step/state)
185
+ │ ├── dynamics.py ← State transition logic
186
+ │ ├── reward.py ← Shaped reward function
187
+ │ └── incident_generator.py ← Random incident sampling
188
+ ├── tasks/
189
+ │ ├── task_simple.py
190
+ │ ├── task_multi_service.py
191
+ │ └── task_critical.py
192
+ ├── agent/
193
+ │ └── baseline.py ← Heuristic baseline agent
194
+ ├── grader/
195
+ │ └── grader.py ← Episode evaluator → score in [0, 1]
196
+ └── scripts/
197
+ ├── validate_submission.py
198
+ └── validate-submission.sh
199
+ ```
200
+
201
+ ---
202
+
203
+ ## Design Decisions
204
+
205
+ **Why multi-step fix sequences?** Single-action fixes are trivially solved by keyword matching. Requiring ordered sequences forces the agent to model state transitions, not just classify root causes.
206
+
207
+ **Why noisy logs?** Real production systems always emit irrelevant log lines. An agent that cannot filter noise will be unreliable.
208
+
209
+ **Why escalating time penalties?** Incident SLAs are real constraints — an agent that solves the issue in 10 steps is materially worse than one that solves it in 2.
210
+
211
+ **Why shaped rewards?** Sparse rewards (terminal-only) are notoriously hard to learn from. Continuous metric penalties and partial-progress bonuses provide useful gradient signal at every step.
agent/__init__.py ADDED
@@ -0,0 +1 @@
 
 
1
+ from agent.baseline import act, reset_history
agent/baseline.py ADDED
@@ -0,0 +1,103 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #Baseline agent — heuristic rule-based agent for the DebugOps environment.
2
+
3
+ # Strategy
4
+ # --------
5
+ # 1. Parse log keywords to identify the most likely root cause.
6
+ # 2. Track own action history to avoid repeating wrong actions.
7
+ # 3. Fall back to metric-based decisions when logs are ambiguous.
8
+ # 4. Use the multi-step fix sequences documented in dynamics.py.
9
+ from __future__ import annotations
10
+ from typing import Dict, Any, List
11
+
12
+
13
+ # Known fix sequences per root cause (mirrors dynamics.py FIX_MAP)
14
+ _FIX_SEQUENCES: Dict[str, List[str]] = {
15
+ "api_timeout": ["scale_up", "restart_api"],
16
+ "db_connection_leak": ["restart_db", "scale_up"],
17
+ "cache_miss_storm": ["restart_cache", "scale_up"],
18
+ "memory_leak": ["restart_api", "restart_db"],
19
+ }
20
+
21
+ # Log keywords → probable root cause
22
+ _LOG_SIGNALS: Dict[str, str] = {
23
+ "timeout": "api_timeout",
24
+ "upstream": "api_timeout",
25
+ "connections": "db_connection_leak",
26
+ "db pool": "db_connection_leak",
27
+ "db": "db_connection_leak",
28
+ "cache miss": "cache_miss_storm",
29
+ "cache": "cache_miss_storm",
30
+ "memory": "memory_leak",
31
+ "oom": "memory_leak",
32
+ "heap": "memory_leak",
33
+ }
34
+
35
+ # Module-level action history (reset at the start of each episode call)
36
+ _action_history: List[str] = []
37
+
38
+
39
+ def reset_history() -> None:
40
+ """Call at the start of a new episode to clear action memory."""
41
+ global _action_history
42
+ _action_history = []
43
+
44
+
45
+ def act(state: Dict[str, Any]) -> str:
46
+ """
47
+ Choose the next action given the current observation.
48
+
49
+ Parameters
50
+ ----------
51
+ state : dict with keys services, logs, metrics, time_step
52
+
53
+ Returns
54
+ -------
55
+ str : one of restart_api | restart_db | restart_cache | scale_up | noop
56
+ """
57
+ logs_text = " ".join(state.get("logs", [])).lower()
58
+ metrics = state.get("metrics", {})
59
+
60
+ # Step 1: identify probable root cause from logs
61
+ cause = _infer_cause(logs_text)
62
+
63
+ if cause:
64
+ seq = _FIX_SEQUENCES[cause]
65
+ # Determine how many steps of this sequence we've already issued
66
+ progress = _count_progress(seq, _action_history)
67
+ if progress < len(seq):
68
+ action = seq[progress]
69
+ _action_history.append(action)
70
+ return action
71
+
72
+ # Step 2: metric-based fallback
73
+ if metrics.get("cpu", 0) > 80 and "scale_up" not in _action_history:
74
+ _action_history.append("scale_up")
75
+ return "scale_up"
76
+
77
+ if metrics.get("error_rate", 0) > 0.5 and "restart_api" not in _action_history:
78
+ _action_history.append("restart_api")
79
+ return "restart_api"
80
+
81
+ # Step 3: try any unused action (avoid noop loops)
82
+ for a in ["restart_api", "restart_db", "restart_cache", "scale_up"]:
83
+ if _action_history.count(a) < 2:
84
+ _action_history.append(a)
85
+ return a
86
+
87
+ _action_history.append("noop")
88
+ return "noop"
89
+
90
+ def _infer_cause(logs_text: str) -> str | None:
91
+ for keyword, cause in _LOG_SIGNALS.items():
92
+ if keyword in logs_text:
93
+ return cause
94
+ return None
95
+
96
+
97
+ def _count_progress(seq: List[str], history: List[str]) -> int:
98
+ # Count how many leading actions of `seq` appear in `history` in order.
99
+ idx = 0
100
+ for action in history:
101
+ if idx < len(seq) and action == seq[idx]:
102
+ idx += 1
103
+ return idx
app.py ADDED
@@ -0,0 +1,56 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ app.py — quick local episode runner using the heuristic baseline agent.
3
+
4
+ Usage
5
+ python app.py
6
+
7
+ This runs a single episode of the Simple task using the rule-based agent
8
+ and prints a per-step log. It is intentionally lightweight and dependency-free
9
+ (no LLM API key required) so it works out of the box.
10
+ """
11
+ from __future__ import annotations
12
+ from typing import Any, Dict, List
13
+
14
+ from tasks.task_simple import create_env
15
+ from agent.baseline import act, reset_history
16
+
17
+
18
+ def run_episode() -> List[Dict[str, Any]]:
19
+ env = create_env()
20
+ state = env.reset()
21
+ reset_history()
22
+
23
+ logs: List[Dict[str, Any]] = []
24
+ done = False
25
+ step = 0
26
+
27
+ print("[app.py] Starting simple-task episode with heuristic baseline agent.")
28
+
29
+ while not done:
30
+ action = act(state)
31
+ next_state, reward, done, info = env.step(action)
32
+
33
+ logs.append({
34
+ "step": step,
35
+ "state": state,
36
+ "action": action,
37
+ "reward": round(reward, 3),
38
+ "info": info,
39
+ })
40
+
41
+ print(
42
+ f" step={step} action={action:<15} reward={round(reward, 3):>8.3f} "
43
+ f"resolved={str(info.get('resolved', False)).lower()}"
44
+ )
45
+
46
+ state = next_state
47
+ step += 1
48
+
49
+ success = any(e["info"].get("resolved", False) for e in logs)
50
+ print(f"\n[app.py] Episode finished — steps={step} success={str(success).lower()}")
51
+ return logs
52
+
53
+
54
+ if __name__ == "__main__":
55
+ logs = run_episode()
56
+ print(f"Steps: {len(logs)}")
env/__init__.py ADDED
@@ -0,0 +1 @@
 
 
1
+ from env.environment import DebugEnv
env/dynamics.py ADDED
@@ -0,0 +1,51 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Dynamics - state transition logic for the DebugOps environment.
3
+ The agent must perform the correct multi-step fix sequence to resolve
4
+ an incident. Wrong actions degrade system metrics.
5
+ """
6
+ from __future__ import annotations
7
+ from typing import Dict, Any
8
+
9
+
10
+ def apply_action(state: Dict[str, Any], action: str) -> Dict[str, Any]:
11
+ """
12
+ Mutate-and-return state after the agent takes `action`.
13
+
14
+ Resolution logic
15
+ ----------------
16
+ Each root cause has a `fix_sequence` list stored inside state.
17
+ The agent must perform each action in order:
18
+ - Correct step → fix_progress += 1; metrics partially improve
19
+ - Wrong step → metrics degrade further
20
+ - All steps done → resolved = True, metrics recover
21
+ """
22
+ seq = state["fix_sequence"]
23
+ prog = state["fix_progress"]
24
+
25
+ if prog < len(seq) and action == seq[prog]:
26
+ # Correct action
27
+ state["fix_progress"] += 1
28
+ prog += 1
29
+
30
+ state["metrics"]["latency"] *= 0.85
31
+ state["metrics"]["error_rate"] *= 0.80
32
+ state["metrics"]["cpu"] *= 0.90
33
+
34
+ if prog == len(seq):
35
+ state["resolved"] = True
36
+ state["metrics"]["latency"] = max(state["metrics"]["latency"] * 0.5, 30)
37
+ state["metrics"]["error_rate"] = max(state["metrics"]["error_rate"] * 0.1, 0.01)
38
+ state["metrics"]["cpu"] = max(state["metrics"]["cpu"] * 0.6, 20)
39
+
40
+ for svc in state["services"]:
41
+ state["services"][svc] = "healthy"
42
+ else:
43
+ state["metrics"]["latency"] = min(state["metrics"]["latency"] * 1.12, 2000)
44
+ state["metrics"]["error_rate"] = min(state["metrics"]["error_rate"] * 1.10, 1.0)
45
+ state["metrics"]["cpu"] = min(state["metrics"]["cpu"] * 1.05, 100)
46
+
47
+ if not state["resolved"]:
48
+ state["metrics"]["latency"] = min(state["metrics"]["latency"] * 1.03, 2000)
49
+ state["metrics"]["cpu"] = min(state["metrics"]["cpu"] * 1.01, 100)
50
+
51
+ return state
env/environment.py ADDED
@@ -0,0 +1,126 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # DebugOps Environment — core RL environment for AI incident response.
2
+ # Follows the OpenEnv step()/reset()/state() interface.
3
+ from __future__ import annotations
4
+ import copy
5
+ from dataclasses import dataclass, field, asdict
6
+ from typing import Dict, List, Tuple, Any
7
+
8
+ from env.incident_generator import generate_incident
9
+ from env.dynamics import apply_action
10
+ from env.reward import compute_reward
11
+
12
+ # Typed observation / state model
13
+ @dataclass
14
+ class Observation:
15
+ services: Dict[str, str] # service_name -> "healthy" | "degraded"
16
+ logs: List[str] # ordered log lines (may be noisy)
17
+ metrics: Dict[str, float] # latency, error_rate, cpu
18
+ time_step: int # steps elapsed in the current episode
19
+ fix_progress: int # number of correct steps completed so far
20
+ metric_trend: str # "improving" | "degrading" | "stable"
21
+
22
+ def to_dict(self) -> Dict[str, Any]:
23
+ return asdict(self)
24
+
25
+
26
+ # Core environment
27
+ class DebugEnv:
28
+ """
29
+ Single-episode production incident environment.
30
+
31
+ Observation space:
32
+ services : Dict[str, str] — per-service health status
33
+ logs : List[str] — system logs (may contain noise)
34
+ metrics : Dict[str,float] — latency (ms), error_rate (0-1), cpu (%)
35
+ time_step : int
36
+
37
+ Action space (discrete, 5 actions):
38
+ restart_api | restart_db | restart_cache | scale_up | noop
39
+
40
+ Episode terminates when:
41
+ - state_data["resolved"] is True (success)
42
+ - t >= max_steps (timeout / failure)
43
+ """
44
+
45
+ VALID_ACTIONS = ["restart_api", "restart_db", "restart_cache", "scale_up", "noop"]
46
+
47
+ def __init__(self, max_steps: int = 20):
48
+ self.max_steps = max_steps
49
+ self.t: int = 0
50
+ self.done: bool = False
51
+ self.success: bool = False
52
+ self.state_data: Dict[str, Any] = {}
53
+ self._prev_latency: float = 0.0
54
+
55
+ #---
56
+ def reset(self) -> Dict[str, Any]:
57
+ """Return fresh observation; episode counter reset."""
58
+ self.t = 0
59
+ self.done = False
60
+ self.success = False
61
+ self.state_data = generate_incident()
62
+ self._prev_latency = self.state_data["metrics"]["latency"]
63
+ return self._obs()
64
+
65
+ #---
66
+ def state(self) -> Dict[str, Any]:
67
+ """Return current observation (idempotent)."""
68
+ return self._obs()
69
+
70
+
71
+ def step(self, action: str) -> Tuple[Dict[str, Any], float, bool, Dict[str, Any]]:
72
+ """
73
+ Apply action and advance one time-step.
74
+
75
+ Returns
76
+ -------
77
+ observation : Dict
78
+ reward : float
79
+ done : bool
80
+ info : Dict — latency, error_rate, progress, resolved, success
81
+ """
82
+ if self.done:
83
+ raise RuntimeError("Episode is finished. Call reset() before stepping.")
84
+
85
+ if action not in self.VALID_ACTIONS:
86
+ raise ValueError(f"Invalid action '{action}'. Must be one of {self.VALID_ACTIONS}")
87
+
88
+ prev_state = copy.deepcopy(self.state_data)
89
+ prev_latency = self.state_data["metrics"]["latency"]
90
+ self.state_data = apply_action(self.state_data, action)
91
+ reward, info = compute_reward(prev_state, self.state_data, action, self.t)
92
+ self._prev_latency = prev_latency
93
+
94
+ self.t += 1
95
+
96
+ if self.state_data["resolved"]:
97
+ self.done = True
98
+ self.success = True
99
+ elif self.t >= self.max_steps:
100
+ self.done = True
101
+ self.success = False
102
+
103
+ info["success"] = self.success
104
+ info["time_step"] = self.t
105
+
106
+ return self._obs(), reward, self.done, info
107
+
108
+
109
+ def _obs(self) -> Dict[str, Any]:
110
+ curr_latency = self.state_data["metrics"]["latency"]
111
+ delta = curr_latency - self._prev_latency
112
+ if delta < -10:
113
+ trend = "improving"
114
+ elif delta > 10:
115
+ trend = "degrading"
116
+ else:
117
+ trend = "stable"
118
+
119
+ return Observation(
120
+ services=dict(self.state_data["services"]),
121
+ logs=list(self.state_data["logs"]),
122
+ metrics={k: round(v, 2) for k, v in self.state_data["metrics"].items()},
123
+ time_step=self.t,
124
+ fix_progress=self.state_data["fix_progress"],
125
+ metric_trend=trend,
126
+ ).to_dict()
env/incident_generator.py ADDED
@@ -0,0 +1,91 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Incident generator - randomly samples a production incident scenario.
3
+ Each incident has a hidden root cause that the agent must diagnose
4
+ from noisy logs and degraded metrics.
5
+ """
6
+ from __future__ import annotations
7
+ import random
8
+ from typing import Dict, List, Any
9
+
10
+ ROOT_CAUSES: Dict[str, Dict[str, Any]] = {
11
+ "api_timeout": {
12
+ "affected": ["api"],
13
+ "fix_sequence": ["scale_up", "restart_api"],
14
+ "log_hints": ["timeout error", "upstream request failed", "connection refused"],
15
+ },
16
+ "db_connection_leak": {
17
+ "affected": ["db"],
18
+ "fix_sequence": ["restart_db", "scale_up"],
19
+ "log_hints": ["too many connections", "db pool exhausted", "connection refused to db"],
20
+ },
21
+ "cache_miss_storm": {
22
+ "affected": ["cache"],
23
+ "fix_sequence": ["restart_cache", "scale_up"],
24
+ "log_hints": ["cache miss spike", "high backend load", "cache key not found"],
25
+ },
26
+ "memory_leak": {
27
+ "affected": ["api", "db"],
28
+ "fix_sequence": ["restart_api", "restart_db"],
29
+ "log_hints": ["memory usage increasing", "OOM warning", "heap allocation failure"],
30
+ },
31
+ }
32
+
33
+ _NOISE_POOL: List[str] = [
34
+ "disk warning: 78% used",
35
+ "temporary network glitch resolved",
36
+ "unrelated service restarted (metrics-exporter)",
37
+ "certificate renewal scheduled",
38
+ "cron job completed",
39
+ "health check passed for load-balancer",
40
+ "rate limiter triggered on /api/v2/bulk",
41
+ ]
42
+
43
+
44
+ def generate_incident() -> Dict[str, Any]:
45
+ """Return a fresh incident state dict."""
46
+ cause_key = random.choice(list(ROOT_CAUSES.keys()))
47
+ cause_cfg = ROOT_CAUSES[cause_key]
48
+
49
+ services = {s: "healthy" for s in ["api", "db", "cache"]}
50
+ for s in cause_cfg["affected"]:
51
+ services[s] = "degraded"
52
+
53
+ logs = _generate_logs(cause_key, cause_cfg["log_hints"])
54
+ metrics = _generate_metrics(cause_key)
55
+
56
+ return {
57
+ "services": services,
58
+ "logs": logs,
59
+ "metrics": metrics,
60
+ "root_cause": cause_key,
61
+ "fix_sequence": list(cause_cfg["fix_sequence"]), # copy
62
+ "resolved": False,
63
+ "fix_progress": 0,
64
+ }
65
+
66
+ def _generate_logs(cause: str, hints: List[str]) -> List[str]:
67
+ logs = list(hints)
68
+
69
+ # 20 % chance of a genuinely misleading entry
70
+ if random.random() < 0.2:
71
+ logs.append("corrupted log entry: [binary garbage]")
72
+
73
+ # Always add 2 noise entries
74
+ logs += random.sample(_NOISE_POOL, k=min(2, len(_NOISE_POOL)))
75
+ random.shuffle(logs)
76
+ return logs
77
+
78
+
79
+ def _generate_metrics(cause: str) -> Dict[str, float]:
80
+ base = {
81
+ "api_timeout": {"latency": 350, "error_rate": 0.55, "cpu": 75},
82
+ "db_connection_leak": {"latency": 280, "error_rate": 0.45, "cpu": 60},
83
+ "cache_miss_storm": {"latency": 220, "error_rate": 0.35, "cpu": 85},
84
+ "memory_leak": {"latency": 400, "error_rate": 0.65, "cpu": 92},
85
+ }[cause]
86
+
87
+ return {
88
+ "latency": base["latency"] + random.randint(-20, 20),
89
+ "error_rate": round(base["error_rate"] + random.uniform(-0.05, 0.05), 3),
90
+ "cpu": base["cpu"] + random.randint(-5, 5),
91
+ }
env/reward.py ADDED
@@ -0,0 +1,52 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Reward function - provides dense, shaped signals to guide learning.
3
+
4
+ Signal summary
5
+ --------------
6
+ +150 full resolution bonus
7
+ + 30 correct intermediate fix step
8
+ - 15 wrong action (no progress)
9
+ -0.04 per ms of latency (continuous cost)
10
+ - 25 per unit of error_rate (continuous cost)
11
+ - 2 time penalty per step (urgency)
12
+ """
13
+ from __future__ import annotations
14
+ from typing import Dict, Any, Tuple
15
+
16
+
17
+ def compute_reward(
18
+ prev: Dict[str, Any],
19
+ curr: Dict[str, Any],
20
+ action: str,
21
+ time_step: int = 0,
22
+ ) -> Tuple[float, Dict[str, Any]]:
23
+ reward = 0.0
24
+
25
+ # Resolution bonus
26
+ if curr["resolved"]:
27
+ reward += 150.0
28
+
29
+ # Partial progress
30
+ if curr["fix_progress"] > prev["fix_progress"]:
31
+ reward += 30.0
32
+ elif action != "noop":
33
+ # Wrong action (no progress, not a passive noop)
34
+ reward -= 15.0
35
+
36
+ # Continuous metric penalties
37
+ reward -= curr["metrics"]["latency"] * 0.04
38
+ reward -= curr["metrics"]["error_rate"] * 25.0
39
+
40
+ # Time penalty (escalates after step 10 for urgency)
41
+ time_penalty = 2.0 + (0.5 * max(0, time_step - 10))
42
+ reward -= time_penalty
43
+
44
+ info = {
45
+ "latency": round(curr["metrics"]["latency"], 2),
46
+ "error_rate": round(curr["metrics"]["error_rate"], 4),
47
+ "cpu": round(curr["metrics"]["cpu"], 2),
48
+ "progress": curr["fix_progress"],
49
+ "resolved": curr["resolved"],
50
+ }
51
+
52
+ return reward, info
grader/__init__.py ADDED
@@ -0,0 +1 @@
 
 
1
+ from grader.grader import evaluate_episode
grader/grader.py ADDED
@@ -0,0 +1,126 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Grader - evaluates a completed episode and returns a score in [0.0, 1.0].
2
+ # Scoring formula: Base score is a weighted combination of:
3
+ # - Resolution bonus (50 % weight)
4
+ # - Efficiency bonus (30 % weight) - fewer steps = higher score
5
+ # - Quality bonus (20 % weight) - average normalised reward
6
+
7
+ # All components are clipped to [0, 1] before weighting.
8
+ # Final score is also clipped to [0.0, 1.0].
9
+
10
+ from __future__ import annotations
11
+ from typing import List, Dict, Any
12
+
13
+ _RESOLUTION_WEIGHT = 0.50
14
+ _EFFICIENCY_WEIGHT = 0.30
15
+ _QUALITY_WEIGHT = 0.20
16
+
17
+ _MAX_STEPS_SIMPLE = 15
18
+ _MAX_STEPS_MULTI = 12
19
+ _MAX_STEPS_CRITICAL = 10
20
+
21
+ # Reference reward range for normalisation (empirically determined)
22
+ _REWARD_MIN = -200.0
23
+ _REWARD_MAX = 200.0
24
+
25
+
26
+ def evaluate_episode(
27
+ episode_logs: List[Dict[str, Any]],
28
+ max_steps: int = _MAX_STEPS_SIMPLE,
29
+ ) -> Dict[str, Any]:
30
+ """
31
+ Parameters
32
+ ----------
33
+ episode_logs : list of step dicts containing at minimum
34
+ {"reward": float, "info": {"resolved": bool, ...}}
35
+ max_steps : maximum allowed steps for this task (used in efficiency calc)
36
+
37
+ Returns
38
+ -------
39
+ dict with keys: score (float 0-1), resolved (bool), steps (int),
40
+ total_reward (float), efficiency (float), quality (float)
41
+ """
42
+ if not episode_logs:
43
+ return {"score": 0.0, "resolved": False, "steps": 0,
44
+ "total_reward": 0.0, "efficiency": 0.0, "quality": 0.0}
45
+
46
+ steps = len(episode_logs)
47
+ total_reward = sum(entry["reward"] for entry in episode_logs)
48
+ resolved = any(entry["info"].get("resolved", False) for entry in episode_logs)
49
+
50
+ # Component 1: resolution (binary, but partial credit for progress)
51
+ if resolved:
52
+ resolution_score = 1.0
53
+ else:
54
+ max_progress = max(
55
+ (entry["info"].get("progress", 0) for entry in episode_logs),
56
+ default=0,
57
+ )
58
+ # Guess fix sequence length ≈ 2 for all tasks
59
+ resolution_score = min(max_progress / 2.0, 0.49)
60
+
61
+ # Component 2: efficiency (resolved faster → higher score)
62
+ if resolved:
63
+ efficiency_score = max(0.0, 1.0 - (steps - 1) / max(max_steps - 1, 1))
64
+ else:
65
+ efficiency_score = 0.0
66
+
67
+ # Component 3: quality (normalised average reward)
68
+ avg_reward = total_reward / steps
69
+ norm_reward = (avg_reward - _REWARD_MIN) / (_REWARD_MAX - _REWARD_MIN)
70
+ quality_score = max(0.0, min(norm_reward, 1.0))
71
+
72
+ # Weighted combination
73
+ score = (
74
+ _RESOLUTION_WEIGHT * resolution_score +
75
+ _EFFICIENCY_WEIGHT * efficiency_score +
76
+ _QUALITY_WEIGHT * quality_score
77
+ )
78
+ score = round(max(0.0, min(score, 1.0)), 4)
79
+
80
+ return {
81
+ "score": score,
82
+ "resolved": resolved,
83
+ "steps": steps,
84
+ "total_reward": round(total_reward, 3),
85
+ "efficiency": round(efficiency_score, 4),
86
+ "quality": round(quality_score, 4),
87
+ }
88
+
89
+ if __name__ == "__main__":
90
+ import sys
91
+ import importlib
92
+
93
+ task_configs = [
94
+ ("simple", "tasks.task_simple", _MAX_STEPS_SIMPLE),
95
+ ("multi_service", "tasks.task_multi_service", _MAX_STEPS_MULTI),
96
+ ("critical", "tasks.task_critical", _MAX_STEPS_CRITICAL),
97
+ ]
98
+
99
+ from agent.baseline import act
100
+
101
+ all_passed = True
102
+ for task_name, module_path, max_steps in task_configs:
103
+ mod = importlib.import_module(module_path)
104
+ env = mod.create_env()
105
+ state = env.reset()
106
+
107
+ logs = []
108
+ done = False
109
+ while not done:
110
+ action = act(state)
111
+ next_state, reward, done, info = env.step(action)
112
+ logs.append({"reward": reward, "info": info})
113
+ state = next_state
114
+
115
+ result = evaluate_episode(logs, max_steps=max_steps)
116
+ ok = 0.0 <= result["score"] <= 1.0
117
+ all_passed = all_passed and ok
118
+
119
+ status = "PASS" if ok else "FAIL"
120
+ print(
121
+ f"[GRADER] task={task_name:<15} score={result['score']:.4f} "
122
+ f"resolved={str(result['resolved']).lower():<5} "
123
+ f"steps={result['steps']:<3} status={status}"
124
+ )
125
+
126
+ sys.exit(0 if all_passed else 1)
inference.py ADDED
@@ -0,0 +1,224 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ DebugOps inference script
3
+ =
4
+ Runs the LLM agent (or heuristic fallback) against all three tasks and
5
+ emits structured stdout logs in the exact required format:
6
+
7
+ [START] task=<name> env=debugops model=<model>
8
+ [STEP] step=<n> action=<a> reward=<r> done=<bool> error=<null|err>
9
+ [END] success=<bool> steps=<n> score=<s> rewards=<comma-list>
10
+
11
+ Environment variables
12
+
13
+ API_BASE_URL LLM endpoint (default: HuggingFace router)
14
+ MODEL_NAME Model ID (default: Qwen/Qwen2.5-72B-Instruct)
15
+ HF_TOKEN API key (also checked as OPENAI_API_KEY or API_KEY)
16
+ """
17
+ from __future__ import annotations
18
+ import os
19
+ import sys
20
+ from typing import Any, Dict, List
21
+
22
+ try:
23
+ from openai import OpenAI
24
+ _OPENAI_AVAILABLE = True
25
+ except ImportError:
26
+ _OPENAI_AVAILABLE = False
27
+
28
+ from grader.grader import evaluate_episode
29
+
30
+ # Configuration
31
+ API_KEY = os.getenv("HF_TOKEN") or os.getenv("OPENAI_API_KEY") or os.getenv("API_KEY")
32
+ API_BASE_URL = os.getenv("API_BASE_URL", "https://router.huggingface.co/v1")
33
+ MODEL_NAME = os.getenv("MODEL_NAME", "Qwen/Qwen2.5-72B-Instruct")
34
+
35
+ VALID_ACTIONS = ["restart_api", "restart_db", "restart_cache", "scale_up", "noop"]
36
+
37
+ # OpenAI client (spec requires OpenAI client for all LLM calls)
38
+ _client: Any = None
39
+ if _OPENAI_AVAILABLE and API_KEY:
40
+ _client = OpenAI(api_key=API_KEY, base_url=API_BASE_URL)
41
+
42
+
43
+ # Heuristic fallback agent (used when no LLM key or on API error)
44
+ def _fallback_agent(state: Dict[str, Any], action_history: List[str]) -> str:
45
+ """
46
+ Rule-based agent driven by log keywords and fix_progress from state.
47
+ Uses fix_progress from the observation to know exactly which step to issue next.
48
+ """
49
+ logs = " ".join(state.get("logs", [])).lower()
50
+ metrics = state.get("metrics", {})
51
+ progress = state.get("fix_progress", 0)
52
+
53
+ # Known fix sequences keyed by log signal (ordered by specificity)
54
+ _log_to_seq = [
55
+ ("timeout", ["scale_up", "restart_api"]),
56
+ ("upstream", ["scale_up", "restart_api"]),
57
+ ("heap", ["restart_api", "restart_db"]),
58
+ ("oom", ["restart_api", "restart_db"]),
59
+ ("memory", ["restart_api", "restart_db"]),
60
+ ("db pool", ["restart_db", "scale_up"]),
61
+ ("connections", ["restart_db", "scale_up"]),
62
+ ("cache miss", ["restart_cache", "scale_up"]),
63
+ ("cache", ["restart_cache", "scale_up"]),
64
+ ("db", ["restart_db", "scale_up"]),
65
+ ]
66
+
67
+ # Identify sequence from logs
68
+ seq = None
69
+ for keyword, candidate_seq in _log_to_seq:
70
+ if keyword in logs:
71
+ seq = candidate_seq
72
+ break
73
+
74
+ if seq and progress < len(seq):
75
+ # Issue the next step in the correct sequence
76
+ return seq[progress]
77
+
78
+ # Metric-based fallback
79
+ if metrics.get("cpu", 0) > 80 and "scale_up" not in action_history:
80
+ return "scale_up"
81
+ if metrics.get("error_rate", 0) > 0.5 and "restart_api" not in action_history:
82
+ return "restart_api"
83
+
84
+ # Try any action not yet used twice
85
+ for a in ["restart_api", "restart_db", "restart_cache", "scale_up"]:
86
+ if action_history.count(a) < 2:
87
+ return a
88
+
89
+ return "noop"
90
+
91
+ # LLM call (OpenAI client as required)
92
+ def _call_llm(
93
+ state: Dict[str, Any],
94
+ action_history: List[str],
95
+ reward_history: List[float],
96
+ ) -> str:
97
+ if _client is None:
98
+ return _fallback_agent(state, action_history)
99
+
100
+ # Build a human-readable step history with outcome signals
101
+ step_lines = []
102
+ for i, (a, r) in enumerate(zip(action_history, reward_history)):
103
+ outcome = "✓ progress made" if r > 20 else ("✗ wrong / no effect" if r < -5 else "~ neutral")
104
+ step_lines.append(f" step {i}: {a:18s} reward={r:>8.1f} [{outcome}]")
105
+ history_block = "\n".join(step_lines) if step_lines else " (none yet)"
106
+
107
+ services_degraded = [s for s, h in state["services"].items() if h == "degraded"]
108
+ metrics = state["metrics"]
109
+
110
+ prompt = f"""You are an expert SRE triaging a production incident. Your goal is to resolve it in as few steps as possible.
111
+
112
+ SYSTEM STATE (step {state['time_step']})
113
+ Degraded services : {services_degraded if services_degraded else 'none'}
114
+ Metrics : latency={metrics.get('latency', 0):.0f}ms error_rate={metrics.get('error_rate', 0):.2%} cpu={metrics.get('cpu', 0):.0f}%
115
+ Metric trend : {state.get('metric_trend', 'unknown')}
116
+ Fix progress : {state.get('fix_progress', 0)} step(s) completed correctly so far
117
+
118
+ SYSTEM LOGS
119
+ {chr(10).join(' ' + l for l in state['logs'])}
120
+
121
+ ACTION HISTORY & OUTCOMES
122
+ {history_block}
123
+
124
+ INSTRUCTIONS
125
+ Root causes have multi-step fix sequences that MUST be performed in order.
126
+ A positive reward means the last action was a correct step — continue the sequence.
127
+ A negative reward means the last action was wrong — try something different.
128
+ Do NOT repeat an action that already got a negative reward.
129
+ If fix_progress increased after your last action, continue to the NEXT step in the sequence.
130
+
131
+ Choose ONE action from: restart_api, restart_db, restart_cache, scale_up, noop
132
+
133
+ Respond with ONLY the action name."""
134
+
135
+ try:
136
+ response = _client.chat.completions.create(
137
+ model=MODEL_NAME,
138
+ messages=[{"role": "user", "content": prompt}],
139
+ temperature=0,
140
+ max_tokens=20,
141
+ timeout=15,
142
+ )
143
+ action = response.choices[0].message.content.strip().lower()
144
+ action = action.split()[0] if action else "noop"
145
+ # strip any punctuation
146
+ action = "".join(c for c in action if c.isalnum() or c == "_")
147
+
148
+ if action not in VALID_ACTIONS:
149
+ return _fallback_agent(state, action_history)
150
+ return action
151
+
152
+ except Exception:
153
+ return _fallback_agent(state, action_history)
154
+
155
+
156
+ def run_episode(task_name: str = "simple") -> None:
157
+ if task_name == "simple":
158
+ from tasks.task_simple import create_env
159
+ max_steps = 15
160
+ elif task_name == "multi_service":
161
+ from tasks.task_multi_service import create_env
162
+ max_steps = 12
163
+ elif task_name == "critical":
164
+ from tasks.task_critical import create_env
165
+ max_steps = 10
166
+ else:
167
+ raise ValueError(f"Unknown task: {task_name!r}")
168
+
169
+ env = create_env()
170
+ state = env.reset()
171
+
172
+ print(
173
+ f"[START] task={task_name} env=debugops model={MODEL_NAME}",
174
+ flush=True,
175
+ )
176
+
177
+ action_history: List[str] = []
178
+ reward_history: List[float] = []
179
+ episode_logs: List[Dict[str, Any]] = []
180
+ done = False
181
+ step = 0
182
+
183
+ while not done and step < max_steps:
184
+ action = _call_llm(state, action_history, reward_history)
185
+
186
+ try:
187
+ next_state, reward, done, info = env.step(action)
188
+ error = "null"
189
+ except Exception as exc:
190
+ next_state, reward, done, info = state, 0.0, True, {}
191
+ error = type(exc).__name__
192
+
193
+ action_history.append(action)
194
+ reward_history.append(reward)
195
+ episode_logs.append({"reward": reward, "info": info})
196
+
197
+ print(
198
+ f"[STEP] step={step} action={action} reward={round(reward, 3)} "
199
+ f"done={str(done).lower()} error={error}",
200
+ flush=True,
201
+ )
202
+
203
+ # Early exit on explicit success flag
204
+ if info.get("success", False):
205
+ done = True
206
+
207
+ state = next_state
208
+ step += 1
209
+
210
+ result = evaluate_episode(episode_logs, max_steps=max_steps)
211
+ score = result["score"] # already in [0, 1]
212
+ success = result["resolved"]
213
+
214
+ print(
215
+ f"[END] success={str(success).lower()} steps={step} "
216
+ f"score={round(score, 3)} "
217
+ f"rewards={','.join(str(round(e['reward'], 3)) for e in episode_logs)}",
218
+ flush=True,
219
+ )
220
+
221
+
222
+ if __name__ == "__main__":
223
+ for task in ["simple", "multi_service", "critical"]:
224
+ run_episode(task)
openenv.yaml ADDED
@@ -0,0 +1,82 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ name: debugops
2
+ version: "1.0.0"
3
+ description: >
4
+ DebugOps — an AI Incident Response environment where an agent acts as an
5
+ on-call SRE to diagnose and resolve production system failures using
6
+ noisy logs, degraded metrics, and multi-step fix sequences.
7
+
8
+ observation_space:
9
+ type: dict
10
+ fields:
11
+ services:
12
+ type: dict
13
+ description: "Per-service health status: api, db, cache → healthy | degraded"
14
+ logs:
15
+ type: list[str]
16
+ description: "System log lines (may contain noise and red herrings)"
17
+ metrics:
18
+ type: dict
19
+ fields:
20
+ latency: { type: float, description: "Latency in ms" }
21
+ error_rate: { type: float, description: "Error rate 0.0–1.0" }
22
+ cpu: { type: float, description: "CPU utilisation %" }
23
+ time_step:
24
+ type: int
25
+ description: "Steps elapsed in the current episode"
26
+
27
+ action_space:
28
+ type: discrete
29
+ actions:
30
+ - restart_api
31
+ - restart_db
32
+ - restart_cache
33
+ - scale_up
34
+ - noop
35
+
36
+ tasks:
37
+ - name: simple
38
+ description: "Single-service failure; 2-step fix; 15-step budget."
39
+ module: tasks.task_simple
40
+ max_steps: 15
41
+ difficulty: low
42
+
43
+ - name: multi_service
44
+ description: "Two services degrade simultaneously; 12-step budget; extra latency penalty."
45
+ module: tasks.task_multi_service
46
+ max_steps: 12
47
+ difficulty: medium
48
+
49
+ - name: critical
50
+ description: "Memory-leak root cause with misleading logs; SLA penalty; 10-step budget."
51
+ module: tasks.task_critical
52
+ max_steps: 10
53
+ difficulty: high
54
+
55
+ grader:
56
+ module: grader.grader
57
+ function: evaluate_episode
58
+ score_range: [0.0, 1.0]
59
+ deterministic: true
60
+
61
+ baseline:
62
+ module: agent.baseline
63
+ function: act
64
+ description: "Heuristic agent using log-keyword matching and metric thresholds."
65
+
66
+ inference:
67
+ script: inference.py
68
+ env_vars:
69
+ - API_BASE_URL
70
+ - MODEL_NAME
71
+ - HF_TOKEN
72
+
73
+ required_env:
74
+ - name: API_BASE_URL
75
+ description: "OpenAI-compatible LLM endpoint"
76
+ default: "https://router.huggingface.co/v1"
77
+ - name: MODEL_NAME
78
+ description: "Model identifier"
79
+ default: "Qwen/Qwen2.5-72B-Instruct"
80
+ - name: HF_TOKEN
81
+ description: "HuggingFace / OpenAI API key"
82
+ required: true
requirements.txt ADDED
@@ -0,0 +1,2 @@
 
 
 
1
+ openai>=1.12.0
2
+ pyyaml>=6.0
scripts/validate-submission.sh ADDED
@@ -0,0 +1,185 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env bash
2
+ #
3
+ # validate-submission.sh - OpenEnv Submission Validator
4
+ #
5
+ # Checks that your HF Space is live, Docker image builds, and openenv validate passes.
6
+ #
7
+ # Prerequisites:
8
+ # - Docker: https://docs.docker.com/get-docker/
9
+ # - openenv-core: pip install openenv-core
10
+ # - curl (usually pre-installed)
11
+ #
12
+ # Run:
13
+ # curl -fsSL https://raw.githubusercontent.com/<owner>/<repo>/main/scripts/validate-submission.sh | bash -s -- <ping_url> [repo_dir]
14
+ #
15
+ # Or download and run locally:
16
+ # chmod +x validate-submission.sh
17
+ # ./validate-submission.sh <ping_url> [repo_dir]
18
+ #
19
+ # Arguments:
20
+ # ping_url Your HuggingFace Space URL (e.g. https://your-space.hf.space)
21
+ # repo_dir Path to your repo (default: current directory)
22
+ #
23
+ # Examples:
24
+ # ./validate-submission.sh https://my-team.hf.space
25
+ # ./validate-submission.sh https://my-team.hf.space ./my-repo
26
+ #
27
+
28
+ set -uo pipefail
29
+
30
+ DOCKER_BUILD_TIMEOUT=600
31
+ if [ -t 1 ]; then
32
+ RED='\033[0;31m'
33
+ GREEN='\033[0;32m'
34
+ YELLOW='\033[1;33m'
35
+ BOLD='\033[1m'
36
+ NC='\033[0m'
37
+ else
38
+ RED='' GREEN='' YELLOW='' BOLD='' NC=''
39
+ fi
40
+
41
+ run_with_timeout() {
42
+ local secs="$1"; shift
43
+ if command -v timeout &>/dev/null; then
44
+ timeout "$secs" "$@"
45
+ elif command -v gtimeout &>/dev/null; then
46
+ gtimeout "$secs" "$@"
47
+ else
48
+ "$@" &
49
+ local pid=$!
50
+ ( sleep "$secs" && kill "$pid" 2>/dev/null ) &
51
+ local watcher=$!
52
+ wait "$pid" 2>/dev/null
53
+ local rc=$?
54
+ kill "$watcher" 2>/dev/null
55
+ wait "$watcher" 2>/dev/null
56
+ return $rc
57
+ fi
58
+ }
59
+
60
+ portable_mktemp() {
61
+ local prefix="${1:-validate}"
62
+ mktemp "${TMPDIR:-/tmp}/${prefix}-XXXXXX" 2>/dev/null || mktemp
63
+ }
64
+
65
+ CLEANUP_FILES=()
66
+ cleanup() { rm -f "${CLEANUP_FILES[@]+"${CLEANUP_FILES[@]}"}"; }
67
+ trap cleanup EXIT
68
+
69
+ PING_URL="${1:-}"
70
+ REPO_DIR="${2:-.}"
71
+
72
+ if [ -z "$PING_URL" ]; then
73
+ printf "Usage: %s <ping_url> [repo_dir]\n" "$0"
74
+ printf "\n"
75
+ printf " ping_url Your HuggingFace Space URL (e.g. https://your-space.hf.space)\n"
76
+ printf " repo_dir Path to your repo (default: current directory)\n"
77
+ exit 1
78
+ fi
79
+
80
+ if ! REPO_DIR="$(cd "$REPO_DIR" 2>/dev/null && pwd)"; then
81
+ printf "Error: directory '%s' not found\n" "${2:-.}"
82
+ exit 1
83
+ fi
84
+ PING_URL="${PING_URL%/}"
85
+ export PING_URL
86
+ PASS=0
87
+
88
+ log() { printf "[%s] %b\n" "$(date -u +%H:%M:%S)" "$*"; }
89
+ pass() { log "${GREEN}PASSED${NC} -- $1"; PASS=$((PASS + 1)); }
90
+ fail() { log "${RED}FAILED${NC} -- $1"; }
91
+ hint() { printf " ${YELLOW}Hint:${NC} %b\n" "$1"; }
92
+ stop_at() {
93
+ printf "\n"
94
+ printf "${RED}${BOLD}Validation stopped at %s.${NC} Fix the above before continuing.\n" "$1"
95
+ exit 1
96
+ }
97
+
98
+ printf "\n"
99
+ printf "${BOLD}========================================${NC}\n"
100
+ printf "${BOLD} OpenEnv Submission Validator${NC}\n"
101
+ printf "${BOLD}========================================${NC}\n"
102
+ log "Repo: $REPO_DIR"
103
+ log "Ping URL: $PING_URL"
104
+ printf "\n"
105
+
106
+ log "${BOLD}Step 1/3: Pinging HF Space${NC} ($PING_URL/reset) ..."
107
+
108
+ CURL_OUTPUT=$(portable_mktemp "validate-curl")
109
+ CLEANUP_FILES+=("$CURL_OUTPUT")
110
+ HTTP_CODE=$(curl -s -o "$CURL_OUTPUT" -w "%{http_code}" -X POST \
111
+ -H "Content-Type: application/json" -d '{}' \
112
+ "$PING_URL/reset" --max-time 30 2>"$CURL_OUTPUT" || printf "000")
113
+
114
+ if [ "$HTTP_CODE" = "200" ]; then
115
+ pass "HF Space is live and responds to /reset"
116
+ elif [ "$HTTP_CODE" = "000" ]; then
117
+ fail "HF Space not reachable (connection failed or timed out)"
118
+ hint "Check your network connection and that the Space is running."
119
+ hint "Try: curl -s -o /dev/null -w '%%{http_code}' -X POST $PING_URL/reset"
120
+ stop_at "Step 1"
121
+ else
122
+ fail "HF Space /reset returned HTTP $HTTP_CODE (expected 200)"
123
+ hint "Make sure your Space is running and the URL is correct."
124
+ hint "Try opening $PING_URL in your browser first."
125
+ stop_at "Step 1"
126
+ fi
127
+
128
+ log "${BOLD}Step 2/3: Running docker build${NC} ..."
129
+
130
+ if ! command -v docker &>/dev/null; then
131
+ fail "docker command not found"
132
+ hint "Install Docker: https://docs.docker.com/get-docker/"
133
+ stop_at "Step 2"
134
+ fi
135
+
136
+ if [ -f "$REPO_DIR/Dockerfile" ]; then
137
+ DOCKER_CONTEXT="$REPO_DIR"
138
+ elif [ -f "$REPO_DIR/server/Dockerfile" ]; then
139
+ DOCKER_CONTEXT="$REPO_DIR/server"
140
+ else
141
+ fail "No Dockerfile found in repo root or server/ directory"
142
+ stop_at "Step 2"
143
+ fi
144
+
145
+ log " Found Dockerfile in $DOCKER_CONTEXT"
146
+
147
+ BUILD_OK=false
148
+ BUILD_OUTPUT=$(run_with_timeout "$DOCKER_BUILD_TIMEOUT" docker build "$DOCKER_CONTEXT" 2>&1) && BUILD_OK=true
149
+
150
+ if [ "$BUILD_OK" = true ]; then
151
+ pass "Docker build succeeded"
152
+ else
153
+ fail "Docker build failed (timeout=${DOCKER_BUILD_TIMEOUT}s)"
154
+ printf "%s\n" "$BUILD_OUTPUT" | tail -20
155
+ stop_at "Step 2"
156
+ fi
157
+
158
+ log "${BOLD}Step 3/3: Running openenv validate${NC} ..."
159
+
160
+ if ! command -v openenv &>/dev/null; then
161
+ fail "openenv command not found"
162
+ hint "Install it: pip install openenv-core"
163
+ stop_at "Step 3"
164
+ fi
165
+
166
+ VALIDATE_OK=false
167
+ VALIDATE_OUTPUT=$(cd "$REPO_DIR" && openenv validate 2>&1) && VALIDATE_OK=true
168
+
169
+ if [ "$VALIDATE_OK" = true ]; then
170
+ pass "openenv validate passed"
171
+ [ -n "$VALIDATE_OUTPUT" ] && log " $VALIDATE_OUTPUT"
172
+ else
173
+ fail "openenv validate failed"
174
+ printf "%s\n" "$VALIDATE_OUTPUT"
175
+ stop_at "Step 3"
176
+ fi
177
+
178
+ printf "\n"
179
+ printf "${BOLD}========================================${NC}\n"
180
+ printf "${GREEN}${BOLD} All 3/3 checks passed!${NC}\n"
181
+ printf "${GREEN}${BOLD} Your submission is ready to submit.${NC}\n"
182
+ printf "${BOLD}========================================${NC}\n"
183
+ printf "\n"
184
+
185
+ exit 0
tasks/__init__.py ADDED
File without changes
tasks/task_critical.py ADDED
@@ -0,0 +1,80 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Task: Critical — SLA-breach scenario with controlled randomness and realism.
2
+ # Introduces noisy logs, metric variance, and slight ambiguity while preserving solvability.
3
+
4
+ from __future__ import annotations
5
+ from typing import Dict, Any, Tuple
6
+ import random
7
+
8
+ from env.environment import DebugEnv
9
+
10
+
11
+ class CriticalEnv(DebugEnv):
12
+ """
13
+ High-stakes incident:
14
+ - Root cause: memory_leak (restart_api → restart_db)
15
+ - Noisy + misleading logs
16
+ - Randomized metrics per episode
17
+ - SLA penalties + time pressure
18
+ - Designed for score variance across runs
19
+ """
20
+
21
+ SLA_LATENCY_THRESHOLD = 250 # ms
22
+ SLA_PENALTY = 80.0
23
+ TIME_PRESSURE_START = 8
24
+ TIME_PRESSURE_PENALTY = 30.0
25
+
26
+ def reset(self) -> Dict[str, Any]:
27
+ state = super().reset()
28
+
29
+ self.state_data["root_cause"] = "memory_leak"
30
+ self.state_data["fix_sequence"] = ["restart_api", "restart_db"]
31
+
32
+ self.state_data["services"]["api"] = "degraded"
33
+ self.state_data["services"]["db"] = "degraded"
34
+
35
+ base_logs = [
36
+ "memory usage increasing",
37
+ "OOM warning",
38
+ "heap allocation failure",
39
+ ]
40
+
41
+ noise_logs = [
42
+ "network latency spike (transient)",
43
+ "disk almost full: /var/log 94%",
44
+ "temporary service restart: metrics-collector",
45
+ "cache eviction rate elevated",
46
+ "connection pool retry",
47
+ "upstream request timeout",
48
+ ]
49
+
50
+ # Randomly inject noise
51
+ selected_noise = random.sample(noise_logs, k=random.randint(2, 4))
52
+
53
+ logs = base_logs + selected_noise
54
+ random.shuffle(logs)
55
+
56
+ self.state_data["logs"] = logs
57
+ self.state_data["metrics"]["latency"] = random.randint(350, 500)
58
+ self.state_data["metrics"]["error_rate"] = round(random.uniform(0.6, 0.9), 2)
59
+ self.state_data["metrics"]["cpu"] = random.randint(85, 98)
60
+
61
+ self._prev_latency = self.state_data["metrics"]["latency"]
62
+
63
+ return self._obs()
64
+
65
+ def step(self, action: str) -> Tuple[Dict[str, Any], float, bool, Dict[str, Any]]:
66
+ obs, reward, done, info = super().step(action)
67
+
68
+ if info["latency"] > self.SLA_LATENCY_THRESHOLD:
69
+ reward -= self.SLA_PENALTY
70
+
71
+ if self.t > self.TIME_PRESSURE_START and not info["resolved"]:
72
+ reward -= self.TIME_PRESSURE_PENALTY
73
+
74
+ reward += random.uniform(-3, 3)
75
+
76
+ return obs, reward, done, info
77
+
78
+
79
+ def create_env() -> CriticalEnv:
80
+ return CriticalEnv(max_steps=10)
tasks/task_multi_service.py ADDED
@@ -0,0 +1,37 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Task: Multi-Service: Forces two services into a degraded state simultaneously.
2
+ # Agent must correctly sequence actions across both services.
3
+ # Tighter step budget increases difficulty.
4
+
5
+ from __future__ import annotations
6
+ from typing import Dict, Any, Tuple
7
+
8
+ from env.environment import DebugEnv
9
+
10
+ class MultiServiceEnv(DebugEnv):
11
+ """Both API and DB start degraded regardless of sampled root cause."""
12
+
13
+ def reset(self) -> Dict[str, Any]:
14
+ state = super().reset()
15
+ self.state_data["services"]["api"] = "degraded"
16
+ self.state_data["services"]["db"] = "degraded"
17
+ # Increase starting metric pressure
18
+ self.state_data["metrics"]["latency"] = min(
19
+ self.state_data["metrics"]["latency"] * 1.3, 600
20
+ )
21
+ self.state_data["metrics"]["error_rate"] = min(
22
+ self.state_data["metrics"]["error_rate"] * 1.2, 0.95
23
+ )
24
+ return self._obs()
25
+
26
+ def step(self, action: str) -> Tuple[Dict[str, Any], float, bool, Dict[str, Any]]:
27
+ obs, reward, done, info = super().step(action)
28
+
29
+ # Additional latency penalty for multi-service SLA
30
+ if info["latency"] > 300:
31
+ reward -= 10.0
32
+
33
+ return obs, reward, done, info
34
+
35
+
36
+ def create_env() -> MultiServiceEnv:
37
+ return MultiServiceEnv(max_steps=12)
tasks/task_simple.py ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ #Task: Simple: A single-service failure with a 2-step fix sequence.
2
+ #Entry-level task - low penalty, generous step budget.
3
+ from env.environment import DebugEnv
4
+
5
+ def create_env() -> DebugEnv:
6
+ return DebugEnv(max_steps=15)