Spaces:
Runtime error
Runtime error
prashasti commited on
Commit ·
205f6c7
1
Parent(s): 79a05b5
Initial changes for ai-debugger
Browse files- .gitignore +10 -0
- Dockerfile +13 -0
- README copy.md +211 -0
- agent/__init__.py +1 -0
- agent/baseline.py +103 -0
- app.py +56 -0
- env/__init__.py +1 -0
- env/dynamics.py +51 -0
- env/environment.py +126 -0
- env/incident_generator.py +91 -0
- env/reward.py +52 -0
- grader/__init__.py +1 -0
- grader/grader.py +126 -0
- inference.py +224 -0
- openenv.yaml +82 -0
- requirements.txt +2 -0
- scripts/validate-submission.sh +185 -0
- tasks/__init__.py +0 -0
- tasks/task_critical.py +80 -0
- tasks/task_multi_service.py +37 -0
- tasks/task_simple.py +6 -0
.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)
|