Spaces:
Sleeping
Sleeping
v1.0.0
Browse files- .dockerignore +6 -0
- .gitignore +207 -0
- Dockerfile +27 -0
- LICENSE +1 -1
- README.md +103 -0
- openenv.yaml +37 -0
- pyproject.toml +33 -0
- server/api/routes/environment.py +23 -0
- server/app.py +28 -0
- server/core/deps.py +12 -0
- server/environment.py +769 -0
- server/requirements.txt +5 -0
- server/rubrics.py +68 -0
- server/schemas/reset.py +10 -0
- sre_env/__init__.py +3 -0
- sre_env/constants/prompts.py +44 -0
- sre_env/core/client.py +20 -0
- sre_env/inference.py +187 -0
- sre_env/models.py +141 -0
- sre_env/tests/test_environment.py +113 -0
- sre_env/utils/parser.py +15 -0
- uv.lock +0 -0
.dockerignore
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
.venv
|
| 2 |
+
.env
|
| 3 |
+
__pycache__
|
| 4 |
+
.pytest_cache
|
| 5 |
+
*.pyc
|
| 6 |
+
.git
|
.gitignore
ADDED
|
@@ -0,0 +1,207 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Byte-compiled / optimized / DLL files
|
| 2 |
+
__pycache__/
|
| 3 |
+
*.py[codz]
|
| 4 |
+
*$py.class
|
| 5 |
+
|
| 6 |
+
# C extensions
|
| 7 |
+
*.so
|
| 8 |
+
|
| 9 |
+
# Distribution / packaging
|
| 10 |
+
.Python
|
| 11 |
+
build/
|
| 12 |
+
develop-eggs/
|
| 13 |
+
dist/
|
| 14 |
+
downloads/
|
| 15 |
+
eggs/
|
| 16 |
+
.eggs/
|
| 17 |
+
lib/
|
| 18 |
+
lib64/
|
| 19 |
+
parts/
|
| 20 |
+
sdist/
|
| 21 |
+
var/
|
| 22 |
+
wheels/
|
| 23 |
+
share/python-wheels/
|
| 24 |
+
*.egg-info/
|
| 25 |
+
.installed.cfg
|
| 26 |
+
*.egg
|
| 27 |
+
MANIFEST
|
| 28 |
+
|
| 29 |
+
# PyInstaller
|
| 30 |
+
# Usually these files are written by a python script from a template
|
| 31 |
+
# before PyInstaller builds the exe, so as to inject date/other infos into it.
|
| 32 |
+
*.manifest
|
| 33 |
+
*.spec
|
| 34 |
+
|
| 35 |
+
# Installer logs
|
| 36 |
+
pip-log.txt
|
| 37 |
+
pip-delete-this-directory.txt
|
| 38 |
+
|
| 39 |
+
# Unit test / coverage reports
|
| 40 |
+
htmlcov/
|
| 41 |
+
.tox/
|
| 42 |
+
.nox/
|
| 43 |
+
.coverage
|
| 44 |
+
.coverage.*
|
| 45 |
+
.cache
|
| 46 |
+
nosetests.xml
|
| 47 |
+
coverage.xml
|
| 48 |
+
*.cover
|
| 49 |
+
*.py.cover
|
| 50 |
+
.hypothesis/
|
| 51 |
+
.pytest_cache/
|
| 52 |
+
cover/
|
| 53 |
+
|
| 54 |
+
# Translations
|
| 55 |
+
*.mo
|
| 56 |
+
*.pot
|
| 57 |
+
|
| 58 |
+
# Django stuff:
|
| 59 |
+
*.log
|
| 60 |
+
local_settings.py
|
| 61 |
+
db.sqlite3
|
| 62 |
+
db.sqlite3-journal
|
| 63 |
+
|
| 64 |
+
# Flask stuff:
|
| 65 |
+
instance/
|
| 66 |
+
.webassets-cache
|
| 67 |
+
|
| 68 |
+
# Scrapy stuff:
|
| 69 |
+
.scrapy
|
| 70 |
+
|
| 71 |
+
# Sphinx documentation
|
| 72 |
+
docs/_build/
|
| 73 |
+
|
| 74 |
+
# PyBuilder
|
| 75 |
+
.pybuilder/
|
| 76 |
+
target/
|
| 77 |
+
|
| 78 |
+
# Jupyter Notebook
|
| 79 |
+
.ipynb_checkpoints
|
| 80 |
+
|
| 81 |
+
# IPython
|
| 82 |
+
profile_default/
|
| 83 |
+
ipython_config.py
|
| 84 |
+
|
| 85 |
+
# pyenv
|
| 86 |
+
# For a library or package, you might want to ignore these files since the code is
|
| 87 |
+
# intended to run in multiple environments; otherwise, check them in:
|
| 88 |
+
# .python-version
|
| 89 |
+
|
| 90 |
+
# pipenv
|
| 91 |
+
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
|
| 92 |
+
# However, in case of collaboration, if having platform-specific dependencies or dependencies
|
| 93 |
+
# having no cross-platform support, pipenv may install dependencies that don't work, or not
|
| 94 |
+
# install all needed dependencies.
|
| 95 |
+
#Pipfile.lock
|
| 96 |
+
|
| 97 |
+
# UV
|
| 98 |
+
# Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control.
|
| 99 |
+
# This is especially recommended for binary packages to ensure reproducibility, and is more
|
| 100 |
+
# commonly ignored for libraries.
|
| 101 |
+
#uv.lock
|
| 102 |
+
|
| 103 |
+
# poetry
|
| 104 |
+
# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
|
| 105 |
+
# This is especially recommended for binary packages to ensure reproducibility, and is more
|
| 106 |
+
# commonly ignored for libraries.
|
| 107 |
+
# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
|
| 108 |
+
#poetry.lock
|
| 109 |
+
#poetry.toml
|
| 110 |
+
|
| 111 |
+
# pdm
|
| 112 |
+
# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
|
| 113 |
+
# pdm recommends including project-wide configuration in pdm.toml, but excluding .pdm-python.
|
| 114 |
+
# https://pdm-project.org/en/latest/usage/project/#working-with-version-control
|
| 115 |
+
#pdm.lock
|
| 116 |
+
#pdm.toml
|
| 117 |
+
.pdm-python
|
| 118 |
+
.pdm-build/
|
| 119 |
+
|
| 120 |
+
# pixi
|
| 121 |
+
# Similar to Pipfile.lock, it is generally recommended to include pixi.lock in version control.
|
| 122 |
+
#pixi.lock
|
| 123 |
+
# Pixi creates a virtual environment in the .pixi directory, just like venv module creates one
|
| 124 |
+
# in the .venv directory. It is recommended not to include this directory in version control.
|
| 125 |
+
.pixi
|
| 126 |
+
|
| 127 |
+
# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
|
| 128 |
+
__pypackages__/
|
| 129 |
+
|
| 130 |
+
# Celery stuff
|
| 131 |
+
celerybeat-schedule
|
| 132 |
+
celerybeat.pid
|
| 133 |
+
|
| 134 |
+
# SageMath parsed files
|
| 135 |
+
*.sage.py
|
| 136 |
+
|
| 137 |
+
# Environments
|
| 138 |
+
.env
|
| 139 |
+
.envrc
|
| 140 |
+
.venv
|
| 141 |
+
env/
|
| 142 |
+
venv/
|
| 143 |
+
ENV/
|
| 144 |
+
env.bak/
|
| 145 |
+
venv.bak/
|
| 146 |
+
|
| 147 |
+
# Spyder project settings
|
| 148 |
+
.spyderproject
|
| 149 |
+
.spyproject
|
| 150 |
+
|
| 151 |
+
# Rope project settings
|
| 152 |
+
.ropeproject
|
| 153 |
+
|
| 154 |
+
# mkdocs documentation
|
| 155 |
+
/site
|
| 156 |
+
|
| 157 |
+
# mypy
|
| 158 |
+
.mypy_cache/
|
| 159 |
+
.dmypy.json
|
| 160 |
+
dmypy.json
|
| 161 |
+
|
| 162 |
+
# Pyre type checker
|
| 163 |
+
.pyre/
|
| 164 |
+
|
| 165 |
+
# pytype static type analyzer
|
| 166 |
+
.pytype/
|
| 167 |
+
|
| 168 |
+
# Cython debug symbols
|
| 169 |
+
cython_debug/
|
| 170 |
+
|
| 171 |
+
# PyCharm
|
| 172 |
+
# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
|
| 173 |
+
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
|
| 174 |
+
# and can be added to the global gitignore or merged into this file. For a more nuclear
|
| 175 |
+
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
|
| 176 |
+
#.idea/
|
| 177 |
+
|
| 178 |
+
# Abstra
|
| 179 |
+
# Abstra is an AI-powered process automation framework.
|
| 180 |
+
# Ignore directories containing user credentials, local state, and settings.
|
| 181 |
+
# Learn more at https://abstra.io/docs
|
| 182 |
+
.abstra/
|
| 183 |
+
|
| 184 |
+
# Visual Studio Code
|
| 185 |
+
# Visual Studio Code specific template is maintained in a separate VisualStudioCode.gitignore
|
| 186 |
+
# that can be found at https://github.com/github/gitignore/blob/main/Global/VisualStudioCode.gitignore
|
| 187 |
+
# and can be added to the global gitignore or merged into this file. However, if you prefer,
|
| 188 |
+
# you could uncomment the following to ignore the entire vscode folder
|
| 189 |
+
# .vscode/
|
| 190 |
+
|
| 191 |
+
# Ruff stuff:
|
| 192 |
+
.ruff_cache/
|
| 193 |
+
|
| 194 |
+
# PyPI configuration file
|
| 195 |
+
.pypirc
|
| 196 |
+
|
| 197 |
+
# Cursor
|
| 198 |
+
# Cursor is an AI-powered code editor. `.cursorignore` specifies files/directories to
|
| 199 |
+
# exclude from AI features like autocomplete and code analysis. Recommended for sensitive data
|
| 200 |
+
# refer to https://docs.cursor.com/context/ignore-files
|
| 201 |
+
.cursorignore
|
| 202 |
+
.cursorindexingignore
|
| 203 |
+
|
| 204 |
+
# Marimo
|
| 205 |
+
marimo/_static/
|
| 206 |
+
marimo/_lsp/
|
| 207 |
+
__marimo__/
|
Dockerfile
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# ─── SRE Incident Response Environment ───────────────────────────────────────
|
| 2 |
+
# Based on openenv-base for the Meta OpenEnv framework.
|
| 3 |
+
# Optimised for HF Spaces CPU Basic tier (2 vCPUs, 8 GB RAM).
|
| 4 |
+
# ──────────────────────────────────────────────────────────────────────────────
|
| 5 |
+
|
| 6 |
+
FROM python:3.11-slim
|
| 7 |
+
|
| 8 |
+
WORKDIR /app
|
| 9 |
+
|
| 10 |
+
# Install curl for the Docker HEALTHCHECK
|
| 11 |
+
RUN apt-get update && apt-get install -y curl && rm -rf /var/lib/apt/lists/*
|
| 12 |
+
|
| 13 |
+
# Copy environment code
|
| 14 |
+
COPY . .
|
| 15 |
+
|
| 16 |
+
# Install server dependencies
|
| 17 |
+
RUN pip install --no-cache-dir -r server/requirements.txt
|
| 18 |
+
|
| 19 |
+
# Expose the default OpenEnv port
|
| 20 |
+
EXPOSE 8000
|
| 21 |
+
|
| 22 |
+
# Docker HEALTHCHECK (every 30 s, 5 s timeout, 3 retries)
|
| 23 |
+
HEALTHCHECK --interval=30s --timeout=5s --retries=3 \
|
| 24 |
+
CMD curl -f http://localhost:8000/health || exit 1
|
| 25 |
+
|
| 26 |
+
# Start uvicorn bound to all interfaces
|
| 27 |
+
CMD ["uvicorn", "server.app:app", "--host", "0.0.0.0", "--port", "8000"]
|
LICENSE
CHANGED
|
@@ -1,6 +1,6 @@
|
|
| 1 |
MIT License
|
| 2 |
|
| 3 |
-
Copyright (c) 2026 Jay
|
| 4 |
|
| 5 |
Permission is hereby granted, free of charge, to any person obtaining a copy
|
| 6 |
of this software and associated documentation files (the "Software"), to deal
|
|
|
|
| 1 |
MIT License
|
| 2 |
|
| 3 |
+
Copyright (c) 2026 Jay
|
| 4 |
|
| 5 |
Permission is hereby granted, free of charge, to any person obtaining a copy
|
| 6 |
of this software and associated documentation files (the "Software"), to deal
|
README.md
ADDED
|
@@ -0,0 +1,103 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# 🛡️ Sentinel-SRE-OpenEnv: Autonomous Incident Response
|
| 2 |
+
[](https://www.python.org/downloads/release/python-3110/)
|
| 3 |
+
[](https://github.com/facebookresearch/openenv)
|
| 4 |
+
[](LICENSE)
|
| 5 |
+
[](https://github.com/astral-sh/ruff)
|
| 6 |
+
|
| 7 |
+
A production-grade Site Reliability Engineering (SRE) Reinforcement Learning environment built on the **Meta OpenEnv** framework.
|
| 8 |
+
|
| 9 |
+
**Sentinel-SRE** simulates catastrophic infrastructure failures to evaluate LLM agents on their ability to act as autonomous SREs. It features a dense reward system, multiple static incident tiers, and a first-of-its-kind **Dynamic Chaos Generator**.
|
| 10 |
+
|
| 11 |
+
---
|
| 12 |
+
|
| 13 |
+
## 🏗️ Architecture
|
| 14 |
+
|
| 15 |
+
The repository adheres to Meta's RFC-004 standard for distributed AI environments.
|
| 16 |
+
|
| 17 |
+
```mermaid
|
| 18 |
+
graph TD
|
| 19 |
+
A[LLM Inference Agent] -->|JSON Actions| B(FastAPI Server)
|
| 20 |
+
B -->|HTTPEnvServer Validator| C{SRE Environment}
|
| 21 |
+
C -->|Simulated Side Effects| D[State Mutator]
|
| 22 |
+
D -->|Continuous Grades| E(SREGraderRubric)
|
| 23 |
+
E -->|Observations + Metrics| A
|
| 24 |
+
```
|
| 25 |
+
|
| 26 |
+
---
|
| 27 |
+
|
| 28 |
+
## 🔥 Incident Topology (Tiers)
|
| 29 |
+
|
| 30 |
+
The environment challenges the agent with escalating scenarios:
|
| 31 |
+
|
| 32 |
+
| Difficulty | Incident Scenario | Primary Diagnostic Tool | Grader Metric |
|
| 33 |
+
|---|---|---|---|
|
| 34 |
+
| 🟢 **Easy** | **OOMKilled Pod Alert** | `diagnose` | Restored cluster uptime. |
|
| 35 |
+
| 🟡 **Medium** | **High DB Latency (12,000ms)** | `run_sql` | Database latency reduction. |
|
| 36 |
+
| 🔴 **Hard** | **Sudden Traffic Spike (10×)** | `scale_servers` | Time to absorb traffic (Budget: $500). |
|
| 37 |
+
| 💀 **Extreme** | **Bad Code Deployment** | `rollback` | Total resolution step/time. |
|
| 38 |
+
| 🧠 **Dynamic** | **User-Defined SRE Incident** | *Matches Archetype* | *Matches Archetype* |
|
| 39 |
+
|
| 40 |
+
### 🧠 Featured: Dynamic Chaos Generator (Option 5)
|
| 41 |
+
Leverage an **SRE Incident Router** (LLM) to classify free-text natural language prompts into solvable incident archetypes. Submit any problem (e.g., *"My checkout service is timing out due to slow database queries"*) and the environment will dynamically generate matching logs and descriptions while maintaining mathematical evaluation integrity.
|
| 42 |
+
|
| 43 |
+
---
|
| 44 |
+
|
| 45 |
+
## 🚀 Getting Started
|
| 46 |
+
|
| 47 |
+
### 1. Prerequisites
|
| 48 |
+
We use [`uv`](https://docs.astral.sh/uv/) for incredibly fast dependency management.
|
| 49 |
+
```bash
|
| 50 |
+
# Install environment & dependencies
|
| 51 |
+
uv sync
|
| 52 |
+
```
|
| 53 |
+
|
| 54 |
+
### 2. Local Server Deployment
|
| 55 |
+
Start the OpenEnv FastAPI layer:
|
| 56 |
+
```bash
|
| 57 |
+
uv run server/app.py
|
| 58 |
+
```
|
| 59 |
+
*The server is now listening for OpenEnv schema actions on `http://localhost:8000`.*
|
| 60 |
+
|
| 61 |
+
### 3. Run the Sentinel-SRE Agent
|
| 62 |
+
To test the environment natively, run the interactive inference script:
|
| 63 |
+
```bash
|
| 64 |
+
# Set your API key
|
| 65 |
+
export HF_TOKEN="your_key"
|
| 66 |
+
|
| 67 |
+
# Execute the agent loop
|
| 68 |
+
uv run sre_env/inference.py
|
| 69 |
+
```
|
| 70 |
+
|
| 71 |
+
---
|
| 72 |
+
|
| 73 |
+
## 🛠️ Testing & Linting
|
| 74 |
+
|
| 75 |
+
### Automated Validation (Tests)
|
| 76 |
+
Ensure the environment math is sound before submission:
|
| 77 |
+
```bash
|
| 78 |
+
# Execute the core test suite (Windows PowerShell)
|
| 79 |
+
$env:PYTHONPATH="."; uv run pytest sre_env/tests/
|
| 80 |
+
|
| 81 |
+
# Execute the core test suite (Linux/MacOS)
|
| 82 |
+
PYTHONPATH=. uv run pytest sre_env/tests/
|
| 83 |
+
```
|
| 84 |
+
|
| 85 |
+
### Quality Assurance (Linting)
|
| 86 |
+
Sentinel-SRE is built with industrial-grade linting standards:
|
| 87 |
+
```bash
|
| 88 |
+
# Scan for errors
|
| 89 |
+
uv run ruff check .
|
| 90 |
+
|
| 91 |
+
# Auto-format codebase
|
| 92 |
+
uv run ruff format .
|
| 93 |
+
```
|
| 94 |
+
|
| 95 |
+
---
|
| 96 |
+
|
| 97 |
+
## ⚖️ Evaluation & Grading
|
| 98 |
+
The `SREGraderRubric` computes continuous floats `[0.0, 1.0]` based on:
|
| 99 |
+
1. **Resolution Speed:** Rewards the fastest fix.
|
| 100 |
+
2. **Budget Efficiency:** Penalties for wasteful cloud spending.
|
| 101 |
+
3. **Collateral Damage:** Severe penalties for terminating healthy services or executing rogue production SQL without cause.
|
| 102 |
+
|
| 103 |
+
Built for the 2026 OpenEnv AI Hackathon.
|
openenv.yaml
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# ─── SRE Incident Response Environment — OpenEnv Manifest ─────────────────────
|
| 2 |
+
# Targets Hugging Face Spaces "CPU Basic" tier (2 vCPUs, 8 GB RAM).
|
| 3 |
+
# ──────────────────────────────────────────────────────────────────────────────
|
| 4 |
+
|
| 5 |
+
name: sentinel-sre-agent
|
| 6 |
+
version: "1.0.0"
|
| 7 |
+
description: >
|
| 8 |
+
Autonomous SRE incident response RL environment.
|
| 9 |
+
Simulates three stratified scenarios — pod crash recovery (easy),
|
| 10 |
+
database index optimisation (medium), and dynamic server scaling under
|
| 11 |
+
a strict budget (hard) — with proportional grading and dense rewards.
|
| 12 |
+
|
| 13 |
+
hardware:
|
| 14 |
+
tier: cpu-basic
|
| 15 |
+
max_vcpus: 2
|
| 16 |
+
max_ram_gb: 8
|
| 17 |
+
|
| 18 |
+
server:
|
| 19 |
+
port: 8000
|
| 20 |
+
dockerfile: Dockerfile
|
| 21 |
+
|
| 22 |
+
tasks:
|
| 23 |
+
- name: pod-restart
|
| 24 |
+
difficulty: easy
|
| 25 |
+
description: Parse alert logs and restart the failing pod to restore uptime.
|
| 26 |
+
|
| 27 |
+
- name: db-index-optimisation
|
| 28 |
+
difficulty: medium
|
| 29 |
+
description: Query slow-log, identify missing index, and execute optimisation SQL.
|
| 30 |
+
|
| 31 |
+
- name: dynamic-scaling
|
| 32 |
+
difficulty: hard
|
| 33 |
+
description: Provision servers to absorb 10× traffic spike within a $500 budget.
|
| 34 |
+
|
| 35 |
+
- name: bad-deployment-rollback
|
| 36 |
+
difficulty: extreme
|
| 37 |
+
description: Diagnose cascading 500 errors from auth-service, check deployment logs, and execute a rollback to the stable revision.
|
pyproject.toml
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
[project]
|
| 2 |
+
name = "sentinel-sre"
|
| 3 |
+
version = "0.1.0"
|
| 4 |
+
description = "Add your description here"
|
| 5 |
+
readme = "README.md"
|
| 6 |
+
requires-python = ">=3.11"
|
| 7 |
+
license = "MIT"
|
| 8 |
+
dependencies = [
|
| 9 |
+
"huggingface-hub>=1.8.0",
|
| 10 |
+
"openenv-core>=0.2.2",
|
| 11 |
+
"fastapi>=0.100.0",
|
| 12 |
+
"uvicorn>=0.22.0",
|
| 13 |
+
"requests>=2.31.0",
|
| 14 |
+
"rich>=13.0.0",
|
| 15 |
+
"python-dotenv>=1.0.0",
|
| 16 |
+
]
|
| 17 |
+
|
| 18 |
+
[tool.uv]
|
| 19 |
+
package = false
|
| 20 |
+
|
| 21 |
+
[dependency-groups]
|
| 22 |
+
dev = [
|
| 23 |
+
"ruff>=0.15.9",
|
| 24 |
+
"pytest>=8.0.0",
|
| 25 |
+
]
|
| 26 |
+
|
| 27 |
+
[tool.ruff]
|
| 28 |
+
line-length = 120
|
| 29 |
+
target-version = "py311"
|
| 30 |
+
|
| 31 |
+
[tool.ruff.lint]
|
| 32 |
+
select = ["E", "F", "I"]
|
| 33 |
+
ignore = ["E501"] # Ignore long lines for SRE logs and prompts
|
server/api/routes/environment.py
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from fastapi import APIRouter
|
| 2 |
+
from openenv.core.env_server.http_server import StepResponse
|
| 3 |
+
|
| 4 |
+
from server.core.deps import env_instance
|
| 5 |
+
from server.schemas.reset import CustomResetRequest
|
| 6 |
+
|
| 7 |
+
router = APIRouter()
|
| 8 |
+
|
| 9 |
+
|
| 10 |
+
@router.post("/custom_reset")
|
| 11 |
+
def custom_reset(payload: CustomResetRequest):
|
| 12 |
+
"""Custom endpoint to inject dynamically generated incident contexts."""
|
| 13 |
+
obs = env_instance.reset(seed=payload.seed)
|
| 14 |
+
|
| 15 |
+
# Override the static archetype text with the LLM's dynamic flavor
|
| 16 |
+
env_instance._state.task_description = payload.custom_description
|
| 17 |
+
|
| 18 |
+
# Refresh observation to reflect overrides
|
| 19 |
+
obs = env_instance._make_observation(
|
| 20 |
+
message=f"🚨 Custom Incident Registered! {payload.custom_description}", logs=payload.custom_logs, success=True
|
| 21 |
+
)
|
| 22 |
+
|
| 23 |
+
return StepResponse(observation=obs.model_dump(), reward=obs.reward, done=obs.done)
|
server/app.py
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import uvicorn
|
| 2 |
+
from fastapi import FastAPI
|
| 3 |
+
from fastapi.responses import JSONResponse
|
| 4 |
+
|
| 5 |
+
from server.api.routes import environment
|
| 6 |
+
from server.core.deps import env_server
|
| 7 |
+
|
| 8 |
+
app = FastAPI(
|
| 9 |
+
title="SRE Incident Response Environment",
|
| 10 |
+
description="OpenEnv RL environment simulating autonomous SRE incident response.",
|
| 11 |
+
version="1.0.0",
|
| 12 |
+
)
|
| 13 |
+
|
| 14 |
+
# Standard endpoints (OpenEnv framework)
|
| 15 |
+
env_server.register_routes(app)
|
| 16 |
+
|
| 17 |
+
# Custom extensions
|
| 18 |
+
app.include_router(environment.router, tags=["SRE Chaos"])
|
| 19 |
+
|
| 20 |
+
|
| 21 |
+
@app.get("/health", tags=["Utilities"])
|
| 22 |
+
async def health() -> JSONResponse:
|
| 23 |
+
"""Return 200 OK for container health checks."""
|
| 24 |
+
return JSONResponse(content={"status": "ok"}, status_code=200)
|
| 25 |
+
|
| 26 |
+
|
| 27 |
+
if __name__ == "__main__":
|
| 28 |
+
uvicorn.run("server.app:app", host="0.0.0.0", port=8000)
|
server/core/deps.py
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from openenv.core.env_server.http_server import HTTPEnvServer
|
| 2 |
+
|
| 3 |
+
from server.environment import SREEnvironment
|
| 4 |
+
from sre_env.models import SREAction, SREObservation
|
| 5 |
+
|
| 6 |
+
env_instance = SREEnvironment()
|
| 7 |
+
|
| 8 |
+
env_server = HTTPEnvServer(
|
| 9 |
+
env=lambda: env_instance,
|
| 10 |
+
action_cls=SREAction,
|
| 11 |
+
observation_cls=SREObservation,
|
| 12 |
+
)
|
server/environment.py
ADDED
|
@@ -0,0 +1,769 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
|
| 3 |
+
import random
|
| 4 |
+
import uuid
|
| 5 |
+
from typing import Any, Optional
|
| 6 |
+
|
| 7 |
+
from openenv.core.env_server.interfaces import Environment
|
| 8 |
+
|
| 9 |
+
from server.rubrics import SREGraderRubric
|
| 10 |
+
from sre_env.models import (
|
| 11 |
+
CommandType,
|
| 12 |
+
SREAction,
|
| 13 |
+
SREObservation,
|
| 14 |
+
SREState,
|
| 15 |
+
SystemMetrics,
|
| 16 |
+
TaskDifficulty,
|
| 17 |
+
)
|
| 18 |
+
|
| 19 |
+
TASK_CONFIGS = {
|
| 20 |
+
TaskDifficulty.EASY: {
|
| 21 |
+
"description": (
|
| 22 |
+
"INCIDENT: Pod 'pod-web-3' in the 'production' namespace is crash-looping. "
|
| 23 |
+
"Alerts indicate OOMKilled errors. Parse the alert logs, identify the failing "
|
| 24 |
+
"pod, and execute a restart to restore service uptime."
|
| 25 |
+
),
|
| 26 |
+
"max_steps": 15,
|
| 27 |
+
"failing_pod": "pod-web-3",
|
| 28 |
+
"initial_uptime": 0.45,
|
| 29 |
+
"budget": 500.0,
|
| 30 |
+
},
|
| 31 |
+
TaskDifficulty.MEDIUM: {
|
| 32 |
+
"description": (
|
| 33 |
+
"INCIDENT: The 'orders' API is responding with P99 latency of 12,000 ms "
|
| 34 |
+
"(threshold: 200 ms). The slow-query log points to a full table scan on "
|
| 35 |
+
"'orders_table'. Diagnose the issue, find the missing index, and execute "
|
| 36 |
+
"the optimisation SQL command."
|
| 37 |
+
),
|
| 38 |
+
"max_steps": 20,
|
| 39 |
+
"slow_table": "orders_table",
|
| 40 |
+
"missing_index_column": "customer_id",
|
| 41 |
+
"initial_latency_ms": 12000.0,
|
| 42 |
+
"target_latency_ms": 200.0,
|
| 43 |
+
"initial_uptime": 0.70,
|
| 44 |
+
"budget": 500.0,
|
| 45 |
+
},
|
| 46 |
+
TaskDifficulty.HARD: {
|
| 47 |
+
"description": (
|
| 48 |
+
"INCIDENT: Traffic spike detected - incoming requests jumped 10× to "
|
| 49 |
+
"50,000 RPS. Current capacity can handle 5,000 RPS. You must dynamically "
|
| 50 |
+
"scale servers to absorb the load WITHOUT exceeding a strict $500 cloud "
|
| 51 |
+
"budget. Each additional server costs $50/unit and adds 5,000 RPS capacity."
|
| 52 |
+
),
|
| 53 |
+
"max_steps": 30,
|
| 54 |
+
"current_rps": 50000,
|
| 55 |
+
"capacity_per_server": 5000,
|
| 56 |
+
"cost_per_server": 50.0,
|
| 57 |
+
"initial_servers": 1,
|
| 58 |
+
"initial_uptime": 0.10,
|
| 59 |
+
"budget": 500.0,
|
| 60 |
+
},
|
| 61 |
+
TaskDifficulty.EXTREME: {
|
| 62 |
+
"description": (
|
| 63 |
+
"INCIDENT: A new deployment of 'auth-service' has resulted in "
|
| 64 |
+
"cascading 500 errors and user lockouts. Diagnose the issue, check "
|
| 65 |
+
"the deployment logs, and execute a rollback to the stable revision."
|
| 66 |
+
),
|
| 67 |
+
"max_steps": 25,
|
| 68 |
+
"buggy_deployment": "auth-service",
|
| 69 |
+
"stable_revision": "v1.4.2",
|
| 70 |
+
"initial_uptime": 0.05,
|
| 71 |
+
"budget": 500.0,
|
| 72 |
+
},
|
| 73 |
+
}
|
| 74 |
+
|
| 75 |
+
|
| 76 |
+
class SREEnvironment(Environment[SREAction, SREObservation, SREState]):
|
| 77 |
+
"""Autonomous SRE incident-response RL environment.
|
| 78 |
+
|
| 79 |
+
Supports three stratified tasks:
|
| 80 |
+
• Easy - restart a crash-looping pod
|
| 81 |
+
• Medium - find and fix a missing DB index
|
| 82 |
+
• Hard - scale servers under budget constraints
|
| 83 |
+
"""
|
| 84 |
+
|
| 85 |
+
def __init__(self, **kwargs: Any) -> None:
|
| 86 |
+
super().__init__(**kwargs)
|
| 87 |
+
self._state = SREState()
|
| 88 |
+
self._rng = random.Random()
|
| 89 |
+
self.rubric = SREGraderRubric(self)
|
| 90 |
+
|
| 91 |
+
# Internal simulation variables (not exposed in State)
|
| 92 |
+
self._pod_restarted: bool = False
|
| 93 |
+
self._index_added: bool = False
|
| 94 |
+
self._servers_added: int = 0
|
| 95 |
+
self._diagnosed: bool = False
|
| 96 |
+
self._diagnosis_target: str = ""
|
| 97 |
+
self._current_latency_ms: float = 0.0
|
| 98 |
+
self._current_servers: int = 1
|
| 99 |
+
self._current_capacity_rps: int = 5000
|
| 100 |
+
self._catastrophic: bool = False
|
| 101 |
+
self._logs_checked: bool = False
|
| 102 |
+
self._rolled_back: bool = False
|
| 103 |
+
|
| 104 |
+
def reset(
|
| 105 |
+
self,
|
| 106 |
+
seed: Optional[int] = None,
|
| 107 |
+
episode_id: Optional[str] = None,
|
| 108 |
+
**kwargs: Any,
|
| 109 |
+
) -> SREObservation:
|
| 110 |
+
"""Reset the environment and select a new incident scenario."""
|
| 111 |
+
|
| 112 |
+
if seed is not None:
|
| 113 |
+
self._rng = random.Random(seed)
|
| 114 |
+
else:
|
| 115 |
+
self._rng = random.Random()
|
| 116 |
+
|
| 117 |
+
difficulty = self._rng.choice(list(TaskDifficulty))
|
| 118 |
+
config = TASK_CONFIGS[difficulty]
|
| 119 |
+
|
| 120 |
+
self._state = SREState(
|
| 121 |
+
episode_id=episode_id or str(uuid.uuid4()),
|
| 122 |
+
step_count=0,
|
| 123 |
+
task_difficulty=difficulty,
|
| 124 |
+
task_description=config["description"],
|
| 125 |
+
current_uptime=config["initial_uptime"],
|
| 126 |
+
budget_remaining=config["budget"],
|
| 127 |
+
max_steps=config["max_steps"],
|
| 128 |
+
incident_resolved=False,
|
| 129 |
+
root_cause_found=False,
|
| 130 |
+
total_reward=0.0,
|
| 131 |
+
)
|
| 132 |
+
|
| 133 |
+
# Reset internal sim variables
|
| 134 |
+
self._pod_restarted = False
|
| 135 |
+
self._index_added = False
|
| 136 |
+
self._servers_added = 0
|
| 137 |
+
self._diagnosed = False
|
| 138 |
+
self._diagnosis_target = ""
|
| 139 |
+
self._catastrophic = False
|
| 140 |
+
self._logs_checked = False
|
| 141 |
+
self._rolled_back = False
|
| 142 |
+
|
| 143 |
+
if difficulty == TaskDifficulty.MEDIUM:
|
| 144 |
+
self._current_latency_ms = config["initial_latency_ms"]
|
| 145 |
+
else:
|
| 146 |
+
self._current_latency_ms = 50.0 # healthy baseline
|
| 147 |
+
|
| 148 |
+
if difficulty == TaskDifficulty.HARD:
|
| 149 |
+
self._current_servers = config["initial_servers"]
|
| 150 |
+
self._current_capacity_rps = self._current_servers * config["capacity_per_server"]
|
| 151 |
+
else:
|
| 152 |
+
self._current_servers = 1
|
| 153 |
+
self._current_capacity_rps = 5000
|
| 154 |
+
|
| 155 |
+
return self._make_observation(
|
| 156 |
+
message=f"🚨 New incident assigned ({difficulty.value} difficulty). {config['description']}",
|
| 157 |
+
logs=self._generate_initial_logs(difficulty),
|
| 158 |
+
success=True,
|
| 159 |
+
)
|
| 160 |
+
|
| 161 |
+
def step(
|
| 162 |
+
self,
|
| 163 |
+
action: SREAction,
|
| 164 |
+
timeout_s: Optional[float] = None,
|
| 165 |
+
**kwargs: Any,
|
| 166 |
+
) -> SREObservation:
|
| 167 |
+
"""Execute one SRE action and return observation + dense reward."""
|
| 168 |
+
|
| 169 |
+
self._state.step_count += 1
|
| 170 |
+
difficulty = self._state.task_difficulty
|
| 171 |
+
|
| 172 |
+
# ── compute reward components ────────────────────────────────
|
| 173 |
+
progress_reward = 0.0
|
| 174 |
+
constraint_penalty = 0.0
|
| 175 |
+
time_penalty = -0.05 # constant per-step cost
|
| 176 |
+
|
| 177 |
+
message = ""
|
| 178 |
+
logs: list[str] = []
|
| 179 |
+
success = False
|
| 180 |
+
|
| 181 |
+
# ── validate action against dynamic rules ─────────────────────
|
| 182 |
+
valid_actions = self._get_available_actions()
|
| 183 |
+
if action.command_type not in valid_actions:
|
| 184 |
+
self._state.total_reward += time_penalty - 0.1
|
| 185 |
+
return self._make_observation(
|
| 186 |
+
message=f"❌ Action '{action.command_type}' is invalid or not currently available. Available: {', '.join(valid_actions)}",
|
| 187 |
+
logs=["[ERROR] Action rejected by environment schema."],
|
| 188 |
+
success=False,
|
| 189 |
+
reward=time_penalty - 0.1,
|
| 190 |
+
done=False,
|
| 191 |
+
)
|
| 192 |
+
|
| 193 |
+
cmd = CommandType(action.command_type)
|
| 194 |
+
|
| 195 |
+
if difficulty == TaskDifficulty.EASY:
|
| 196 |
+
progress_reward, message, logs, success = self._step_easy(action, cmd)
|
| 197 |
+
elif difficulty == TaskDifficulty.MEDIUM:
|
| 198 |
+
progress_reward, message, logs, success = self._step_medium(action, cmd)
|
| 199 |
+
elif difficulty == TaskDifficulty.HARD:
|
| 200 |
+
progress_reward, message, logs, success, constraint_penalty = self._step_hard(action, cmd)
|
| 201 |
+
elif difficulty == TaskDifficulty.EXTREME:
|
| 202 |
+
progress_reward, message, logs, success, constraint_penalty = self._step_extreme(action, cmd)
|
| 203 |
+
|
| 204 |
+
if cmd == CommandType.RESTART_POD and difficulty != TaskDifficulty.EASY:
|
| 205 |
+
constraint_penalty += -0.8
|
| 206 |
+
message += " ⚠️ Restarting a healthy pod caused a service disruption!"
|
| 207 |
+
self._state.current_uptime = max(0.0, self._state.current_uptime - 0.15)
|
| 208 |
+
self._catastrophic = True
|
| 209 |
+
|
| 210 |
+
if cmd == CommandType.RUN_SQL and difficulty != TaskDifficulty.MEDIUM and action.target_resource != "":
|
| 211 |
+
constraint_penalty += -0.4
|
| 212 |
+
message += " ⚠️ Running SQL on a production database without cause is risky."
|
| 213 |
+
|
| 214 |
+
step_reward = progress_reward + time_penalty + constraint_penalty
|
| 215 |
+
self._state.total_reward += step_reward
|
| 216 |
+
|
| 217 |
+
done = False
|
| 218 |
+
if self._state.incident_resolved:
|
| 219 |
+
done = True
|
| 220 |
+
message += " ✅ Incident fully resolved!"
|
| 221 |
+
elif self._state.step_count >= self._state.max_steps:
|
| 222 |
+
done = True
|
| 223 |
+
message += " ⏰ Max steps reached - episode terminated."
|
| 224 |
+
elif self._state.budget_remaining <= 0:
|
| 225 |
+
done = True
|
| 226 |
+
message += " 💸 Budget exhausted - episode terminated."
|
| 227 |
+
elif self._catastrophic and self._state.current_uptime <= 0.0:
|
| 228 |
+
done = True
|
| 229 |
+
message += " 💀 Catastrophic failure - all services down."
|
| 230 |
+
|
| 231 |
+
return self._make_observation(
|
| 232 |
+
message=message,
|
| 233 |
+
logs=logs,
|
| 234 |
+
success=success,
|
| 235 |
+
reward=step_reward,
|
| 236 |
+
done=done,
|
| 237 |
+
)
|
| 238 |
+
|
| 239 |
+
@property
|
| 240 |
+
def state(self) -> SREState:
|
| 241 |
+
"""Return the current internal state."""
|
| 242 |
+
return self._state
|
| 243 |
+
|
| 244 |
+
def _step_easy(self, action: SREAction, cmd: CommandType) -> tuple[float, str, list[str], bool]:
|
| 245 |
+
"""Easy: restart the correct crash-looping pod."""
|
| 246 |
+
config = TASK_CONFIGS[TaskDifficulty.EASY]
|
| 247 |
+
reward = 0.0
|
| 248 |
+
logs: list[str] = []
|
| 249 |
+
|
| 250 |
+
if cmd == CommandType.DIAGNOSE:
|
| 251 |
+
if not self._diagnosed:
|
| 252 |
+
self._diagnosed = True
|
| 253 |
+
self._diagnosis_target = action.target_resource
|
| 254 |
+
reward = 0.15
|
| 255 |
+
logs = [
|
| 256 |
+
"[DIAG] Scanning pods in 'production' namespace...",
|
| 257 |
+
"[DIAG] pod-web-1: Running (healthy)",
|
| 258 |
+
"[DIAG] pod-web-2: Running (healthy)",
|
| 259 |
+
"[DIAG] pod-web-3: CrashLoopBackOff (OOMKilled, 12 restarts)",
|
| 260 |
+
"[DIAG] pod-web-4: Running (healthy)",
|
| 261 |
+
"[DIAG] Root cause: pod-web-3 exceeding memory limits.",
|
| 262 |
+
]
|
| 263 |
+
self._state.root_cause_found = True
|
| 264 |
+
return (
|
| 265 |
+
reward,
|
| 266 |
+
"Diagnosis complete. pod-web-3 identified as failing.",
|
| 267 |
+
logs,
|
| 268 |
+
True,
|
| 269 |
+
)
|
| 270 |
+
else:
|
| 271 |
+
return (
|
| 272 |
+
0.0,
|
| 273 |
+
"Already diagnosed. Consider taking corrective action.",
|
| 274 |
+
logs,
|
| 275 |
+
False,
|
| 276 |
+
)
|
| 277 |
+
|
| 278 |
+
elif cmd == CommandType.RESTART_POD:
|
| 279 |
+
if action.target_resource == config["failing_pod"]:
|
| 280 |
+
self._pod_restarted = True
|
| 281 |
+
self._state.current_uptime = min(1.0, self._state.current_uptime + 0.50)
|
| 282 |
+
if self._diagnosed:
|
| 283 |
+
reward = 0.40
|
| 284 |
+
else:
|
| 285 |
+
reward = 0.25 # less reward if you skipped diagnosis
|
| 286 |
+
if self._state.current_uptime >= 0.95:
|
| 287 |
+
self._state.incident_resolved = True
|
| 288 |
+
logs = [
|
| 289 |
+
"[K8S] kubectl rollout restart deployment/web -n production",
|
| 290 |
+
"[K8S] pod-web-3 terminated.",
|
| 291 |
+
"[K8S] pod-web-3-new: Running (healthy)",
|
| 292 |
+
f"[METRICS] Uptime restored to {self._state.current_uptime:.0%}",
|
| 293 |
+
]
|
| 294 |
+
return (
|
| 295 |
+
reward,
|
| 296 |
+
f"Pod '{action.target_resource}' restarted successfully.",
|
| 297 |
+
logs,
|
| 298 |
+
True,
|
| 299 |
+
)
|
| 300 |
+
else:
|
| 301 |
+
reward = -0.3
|
| 302 |
+
logs = [
|
| 303 |
+
f"[K8S] pod '{action.target_resource}' not found or already healthy.",
|
| 304 |
+
]
|
| 305 |
+
return (
|
| 306 |
+
reward,
|
| 307 |
+
f"Wrong pod targeted. '{action.target_resource}' is not the failing pod.",
|
| 308 |
+
logs,
|
| 309 |
+
False,
|
| 310 |
+
)
|
| 311 |
+
|
| 312 |
+
elif cmd == CommandType.NOOP:
|
| 313 |
+
self._state.current_uptime = max(0.0, self._state.current_uptime - 0.03)
|
| 314 |
+
logs = [f"[METRICS] Uptime degraded to {self._state.current_uptime:.0%} (no action taken)."]
|
| 315 |
+
return 0.0, "No action taken. Service continues to degrade.", logs, False
|
| 316 |
+
|
| 317 |
+
else:
|
| 318 |
+
return (
|
| 319 |
+
0.0,
|
| 320 |
+
f"Command '{action.command_type.value}' is not effective for this incident.",
|
| 321 |
+
[],
|
| 322 |
+
False,
|
| 323 |
+
)
|
| 324 |
+
|
| 325 |
+
def _step_medium(self, action: SREAction, cmd: CommandType) -> tuple[float, str, list[str], bool]:
|
| 326 |
+
"""Medium: find missing DB index and execute optimisation SQL."""
|
| 327 |
+
config = TASK_CONFIGS[TaskDifficulty.MEDIUM]
|
| 328 |
+
reward = 0.0
|
| 329 |
+
logs: list[str] = []
|
| 330 |
+
|
| 331 |
+
if cmd == CommandType.DIAGNOSE:
|
| 332 |
+
if not self._diagnosed:
|
| 333 |
+
self._diagnosed = True
|
| 334 |
+
reward = 0.15
|
| 335 |
+
logs = [
|
| 336 |
+
"[DB] Querying slow_log for queries > 1000ms...",
|
| 337 |
+
"[DB] SELECT * FROM orders_table WHERE customer_id = ? - avg 11,800 ms (FULL TABLE SCAN)",
|
| 338 |
+
"[DB] EXPLAIN shows: type=ALL, rows=2,400,000, no index on 'customer_id'",
|
| 339 |
+
"[DB] Recommendation: CREATE INDEX idx_customer_id ON orders_table(customer_id)",
|
| 340 |
+
]
|
| 341 |
+
self._state.root_cause_found = True
|
| 342 |
+
return (
|
| 343 |
+
reward,
|
| 344 |
+
"Slow query identified. Missing index on orders_table.customer_id.",
|
| 345 |
+
logs,
|
| 346 |
+
True,
|
| 347 |
+
)
|
| 348 |
+
else:
|
| 349 |
+
return 0.0, "Already diagnosed. Apply the SQL optimisation.", [], False
|
| 350 |
+
|
| 351 |
+
elif cmd == CommandType.RUN_SQL:
|
| 352 |
+
target = action.target_resource.lower()
|
| 353 |
+
sql_param = action.parameters.get("sql", "").lower()
|
| 354 |
+
|
| 355 |
+
# Check if the agent is creating the right index
|
| 356 |
+
is_correct_table = config["slow_table"] in target or config["slow_table"] in sql_param
|
| 357 |
+
is_correct_column = config["missing_index_column"] in sql_param or config["missing_index_column"] in target
|
| 358 |
+
has_create_index = "create index" in sql_param or "create index" in target
|
| 359 |
+
|
| 360 |
+
if has_create_index and is_correct_table and is_correct_column:
|
| 361 |
+
self._index_added = True
|
| 362 |
+
old_latency = self._current_latency_ms
|
| 363 |
+
self._current_latency_ms = 45.0 # excellent after indexing
|
| 364 |
+
self._state.current_uptime = min(1.0, self._state.current_uptime + 0.30)
|
| 365 |
+
|
| 366 |
+
if self._diagnosed:
|
| 367 |
+
reward = 0.40
|
| 368 |
+
else:
|
| 369 |
+
reward = 0.25
|
| 370 |
+
|
| 371 |
+
if self._current_latency_ms <= config["target_latency_ms"]:
|
| 372 |
+
self._state.incident_resolved = True
|
| 373 |
+
|
| 374 |
+
logs = [
|
| 375 |
+
"[DB] Executing: CREATE INDEX idx_customer_id ON orders_table(customer_id);",
|
| 376 |
+
"[DB] Index created successfully. Build time: 12.3s",
|
| 377 |
+
f"[DB] P99 latency dropped from {old_latency:.0f}ms to {self._current_latency_ms:.0f}ms",
|
| 378 |
+
f"[METRICS] Uptime improved to {self._state.current_uptime:.0%}",
|
| 379 |
+
]
|
| 380 |
+
return (
|
| 381 |
+
reward,
|
| 382 |
+
"Index created. Query performance dramatically improved.",
|
| 383 |
+
logs,
|
| 384 |
+
True,
|
| 385 |
+
)
|
| 386 |
+
|
| 387 |
+
elif has_create_index and is_correct_table and not is_correct_column:
|
| 388 |
+
# Partial credit - right table, wrong column
|
| 389 |
+
self._current_latency_ms *= 0.6
|
| 390 |
+
self._state.current_uptime = min(1.0, self._state.current_uptime + 0.10)
|
| 391 |
+
reward = 0.10
|
| 392 |
+
logs = [
|
| 393 |
+
"[DB] Index created on different column. Partial improvement.",
|
| 394 |
+
f"[DB] P99 latency: {self._current_latency_ms:.0f}ms (still above threshold).",
|
| 395 |
+
]
|
| 396 |
+
return (
|
| 397 |
+
reward,
|
| 398 |
+
"Index created but on wrong column. Partial improvement.",
|
| 399 |
+
logs,
|
| 400 |
+
True,
|
| 401 |
+
)
|
| 402 |
+
|
| 403 |
+
else:
|
| 404 |
+
reward = -0.1
|
| 405 |
+
logs = ["[DB] SQL executed but had no meaningful effect on performance."]
|
| 406 |
+
return (
|
| 407 |
+
reward,
|
| 408 |
+
"SQL command did not address the root cause.",
|
| 409 |
+
logs,
|
| 410 |
+
False,
|
| 411 |
+
)
|
| 412 |
+
|
| 413 |
+
elif cmd == CommandType.NOOP:
|
| 414 |
+
self._current_latency_ms *= 1.05 # latency slowly worsens
|
| 415 |
+
self._state.current_uptime = max(0.0, self._state.current_uptime - 0.02)
|
| 416 |
+
logs = [f"[METRICS] Latency: {self._current_latency_ms:.0f}ms. Uptime: {self._state.current_uptime:.0%}."]
|
| 417 |
+
return (
|
| 418 |
+
0.0,
|
| 419 |
+
"No action taken. System performance continues to degrade.",
|
| 420 |
+
logs,
|
| 421 |
+
False,
|
| 422 |
+
)
|
| 423 |
+
|
| 424 |
+
else:
|
| 425 |
+
return (
|
| 426 |
+
0.0,
|
| 427 |
+
f"Command '{action.command_type.value}' is not relevant to this DB incident.",
|
| 428 |
+
[],
|
| 429 |
+
False,
|
| 430 |
+
)
|
| 431 |
+
|
| 432 |
+
def _step_hard(self, action: SREAction, cmd: CommandType) -> tuple[float, str, list[str], bool, float]:
|
| 433 |
+
"""Hard: scale servers under budget to handle traffic spike."""
|
| 434 |
+
config = TASK_CONFIGS[TaskDifficulty.HARD]
|
| 435 |
+
reward = 0.0
|
| 436 |
+
constraint_penalty = 0.0
|
| 437 |
+
logs: list[str] = []
|
| 438 |
+
|
| 439 |
+
if cmd == CommandType.DIAGNOSE:
|
| 440 |
+
if not self._diagnosed:
|
| 441 |
+
self._diagnosed = True
|
| 442 |
+
reward = 0.10
|
| 443 |
+
needed = config["current_rps"] - self._current_capacity_rps
|
| 444 |
+
servers_needed = max(1, needed // config["capacity_per_server"])
|
| 445 |
+
cost_estimate = servers_needed * config["cost_per_server"]
|
| 446 |
+
logs = [
|
| 447 |
+
f"[LB] Current traffic: {config['current_rps']:,} RPS",
|
| 448 |
+
f"[LB] Current capacity: {self._current_capacity_rps:,} RPS ({self._current_servers} servers)",
|
| 449 |
+
f"[LB] Deficit: {needed:,} RPS",
|
| 450 |
+
f"[LB] Estimated servers needed: {servers_needed} (${cost_estimate:.0f} total)",
|
| 451 |
+
f"[BUDGET] Remaining: ${self._state.budget_remaining:.0f}",
|
| 452 |
+
]
|
| 453 |
+
self._state.root_cause_found = True
|
| 454 |
+
return (
|
| 455 |
+
reward,
|
| 456 |
+
"Traffic analysis complete. Scaling plan identified.",
|
| 457 |
+
logs,
|
| 458 |
+
True,
|
| 459 |
+
0.0,
|
| 460 |
+
)
|
| 461 |
+
else:
|
| 462 |
+
return 0.0, "Already diagnosed. Proceed with scaling.", [], False, 0.0
|
| 463 |
+
|
| 464 |
+
elif cmd == CommandType.SCALE_SERVERS:
|
| 465 |
+
replicas = action.parameters.get("replicas", 1)
|
| 466 |
+
try:
|
| 467 |
+
replicas = int(replicas)
|
| 468 |
+
except (ValueError, TypeError):
|
| 469 |
+
replicas = 1
|
| 470 |
+
|
| 471 |
+
replicas = max(1, min(replicas, 20)) # clamp to sane range
|
| 472 |
+
cost = replicas * config["cost_per_server"]
|
| 473 |
+
|
| 474 |
+
# Check budget
|
| 475 |
+
if cost > self._state.budget_remaining:
|
| 476 |
+
constraint_penalty = -0.8
|
| 477 |
+
self._state.budget_remaining -= cost # negative budget
|
| 478 |
+
logs = [
|
| 479 |
+
f"[CLOUD] Provisioned {replicas} servers at ${cost:.0f}.",
|
| 480 |
+
f"[BUDGET] ⚠️ EXCEEDED! Remaining: ${self._state.budget_remaining:.0f}",
|
| 481 |
+
]
|
| 482 |
+
return (
|
| 483 |
+
0.0,
|
| 484 |
+
f"Budget exceeded! Over by ${abs(self._state.budget_remaining):.0f}.",
|
| 485 |
+
logs,
|
| 486 |
+
False,
|
| 487 |
+
constraint_penalty,
|
| 488 |
+
)
|
| 489 |
+
|
| 490 |
+
# Provision servers
|
| 491 |
+
self._state.budget_remaining -= cost
|
| 492 |
+
self._current_servers += replicas
|
| 493 |
+
self._servers_added += replicas
|
| 494 |
+
self._current_capacity_rps = self._current_servers * config["capacity_per_server"]
|
| 495 |
+
|
| 496 |
+
# Update uptime based on capacity vs demand
|
| 497 |
+
capacity_ratio = min(1.0, self._current_capacity_rps / config["current_rps"])
|
| 498 |
+
self._state.current_uptime = capacity_ratio
|
| 499 |
+
|
| 500 |
+
# Proportional reward based on how much capacity gap is closed
|
| 501 |
+
reward = 0.05 + 0.35 * capacity_ratio
|
| 502 |
+
if self._diagnosed:
|
| 503 |
+
reward += 0.05
|
| 504 |
+
|
| 505 |
+
if capacity_ratio >= 1.0:
|
| 506 |
+
self._state.incident_resolved = True
|
| 507 |
+
|
| 508 |
+
logs = [
|
| 509 |
+
f"[CLOUD] Provisioned {replicas} servers. Total: {self._current_servers}.",
|
| 510 |
+
f"[CLOUD] Capacity: {self._current_capacity_rps:,} / {config['current_rps']:,} RPS ({capacity_ratio:.0%})",
|
| 511 |
+
f"[BUDGET] Spent: ${cost:.0f}. Remaining: ${self._state.budget_remaining:.0f}.",
|
| 512 |
+
f"[METRICS] Uptime: {self._state.current_uptime:.0%}",
|
| 513 |
+
]
|
| 514 |
+
return (
|
| 515 |
+
reward,
|
| 516 |
+
f"Scaled by {replicas} servers. Capacity at {capacity_ratio:.0%}.",
|
| 517 |
+
logs,
|
| 518 |
+
True,
|
| 519 |
+
0.0,
|
| 520 |
+
)
|
| 521 |
+
|
| 522 |
+
elif cmd == CommandType.NOOP:
|
| 523 |
+
# Uptime drops further with no action
|
| 524 |
+
self._state.current_uptime = max(0.0, self._state.current_uptime - 0.05)
|
| 525 |
+
logs = [
|
| 526 |
+
f"[METRICS] Uptime: {self._state.current_uptime:.0%} (degrading).",
|
| 527 |
+
f"[LB] Dropping {max(0, config['current_rps'] - self._current_capacity_rps):,} requests.",
|
| 528 |
+
]
|
| 529 |
+
return 0.0, "No action. Requests being dropped.", logs, False, 0.0
|
| 530 |
+
|
| 531 |
+
else:
|
| 532 |
+
return (
|
| 533 |
+
0.0,
|
| 534 |
+
f"Command '{action.command_type.value}' is not relevant to the scaling incident.",
|
| 535 |
+
[],
|
| 536 |
+
False,
|
| 537 |
+
0.0,
|
| 538 |
+
)
|
| 539 |
+
|
| 540 |
+
def _step_extreme(self, action: SREAction, cmd: CommandType) -> tuple[float, str, list[str], bool, float]:
|
| 541 |
+
"""Extreme: Rollback a bad deployment based on logs."""
|
| 542 |
+
config = TASK_CONFIGS[TaskDifficulty.EXTREME]
|
| 543 |
+
reward = 0.0
|
| 544 |
+
constraint_penalty = 0.0
|
| 545 |
+
logs: list[str] = []
|
| 546 |
+
|
| 547 |
+
if cmd == CommandType.DIAGNOSE:
|
| 548 |
+
if not self._diagnosed:
|
| 549 |
+
self._diagnosed = True
|
| 550 |
+
reward = 0.15
|
| 551 |
+
logs = [
|
| 552 |
+
"[ALERT] auth-service returning 500 Internal Server Error for 98% of requests.",
|
| 553 |
+
"[DIAG] CPU/Mem healthy. Network healthy.",
|
| 554 |
+
"[DIAG] Root cause appears to be a recent code deployment in auth-service.",
|
| 555 |
+
"[HINT] Use 'check_logs' on 'auth-service' to identify the failing revision.",
|
| 556 |
+
]
|
| 557 |
+
self._state.root_cause_found = True
|
| 558 |
+
return (
|
| 559 |
+
reward,
|
| 560 |
+
"Diagnosis complete. auth-service deployment is failing.",
|
| 561 |
+
logs,
|
| 562 |
+
True,
|
| 563 |
+
0.0,
|
| 564 |
+
)
|
| 565 |
+
return (
|
| 566 |
+
0.0,
|
| 567 |
+
"Already diagnosed. Proceed to check deployment logs.",
|
| 568 |
+
[],
|
| 569 |
+
False,
|
| 570 |
+
0.0,
|
| 571 |
+
)
|
| 572 |
+
|
| 573 |
+
elif cmd == CommandType.CHECK_LOGS:
|
| 574 |
+
if action.target_resource == config["buggy_deployment"]:
|
| 575 |
+
self._logs_checked = True
|
| 576 |
+
reward = 0.15
|
| 577 |
+
logs = [
|
| 578 |
+
"[LOGS] Tail of auth-service deployment history:",
|
| 579 |
+
" Revision 3 (v1.4.3) - FAILED (NullPointerException in AuthProvider)",
|
| 580 |
+
" Revision 2 (v1.4.2) - STABLE (active 3 days ago)",
|
| 581 |
+
" Revision 1 (v1.4.1) - STABLE",
|
| 582 |
+
"[HINT] Use 'rollback' on 'auth-service' with parameter {'revision': 'v1.4.2'}",
|
| 583 |
+
]
|
| 584 |
+
return (
|
| 585 |
+
reward,
|
| 586 |
+
"Deployment logs retrieved successfully.",
|
| 587 |
+
logs,
|
| 588 |
+
True,
|
| 589 |
+
0.0,
|
| 590 |
+
)
|
| 591 |
+
return (
|
| 592 |
+
-0.1,
|
| 593 |
+
f"No deployment history found for {action.target_resource}",
|
| 594 |
+
[],
|
| 595 |
+
False,
|
| 596 |
+
0.0,
|
| 597 |
+
)
|
| 598 |
+
|
| 599 |
+
elif cmd == CommandType.ROLLBACK:
|
| 600 |
+
target = action.target_resource
|
| 601 |
+
revision = action.parameters.get("revision", "")
|
| 602 |
+
|
| 603 |
+
if target != config["buggy_deployment"]:
|
| 604 |
+
return (
|
| 605 |
+
-0.3,
|
| 606 |
+
f"Cannot rollback {target}. It is not the source of the incident.",
|
| 607 |
+
[],
|
| 608 |
+
False,
|
| 609 |
+
0.0,
|
| 610 |
+
)
|
| 611 |
+
|
| 612 |
+
if revision != config["stable_revision"]:
|
| 613 |
+
constraint_penalty = -0.5
|
| 614 |
+
self._catastrophic = True
|
| 615 |
+
self._state.current_uptime = 0.0
|
| 616 |
+
return (
|
| 617 |
+
0.0,
|
| 618 |
+
f"Rolled back to bad revision '{revision}'! Complete outage.",
|
| 619 |
+
["[FATAL] Rollback failed. No service startup."],
|
| 620 |
+
False,
|
| 621 |
+
constraint_penalty,
|
| 622 |
+
)
|
| 623 |
+
|
| 624 |
+
# Success path
|
| 625 |
+
self._rolled_back = True
|
| 626 |
+
self._state.current_uptime = min(1.0, self._state.current_uptime + 0.90)
|
| 627 |
+
reward = 0.50
|
| 628 |
+
if self._state.current_uptime >= 0.95:
|
| 629 |
+
self._state.incident_resolved = True
|
| 630 |
+
|
| 631 |
+
logs = [
|
| 632 |
+
"[K8S] kubectl rollout undo deployment/auth-service --to-revision=2",
|
| 633 |
+
"[K8S] auth-service rolled back to v1.4.2",
|
| 634 |
+
f"[METRICS] Uptime recovered to {self._state.current_uptime:.0%}",
|
| 635 |
+
]
|
| 636 |
+
return (
|
| 637 |
+
reward,
|
| 638 |
+
f"Successfully rolled back to {revision}. Service restored.",
|
| 639 |
+
logs,
|
| 640 |
+
True,
|
| 641 |
+
0.0,
|
| 642 |
+
)
|
| 643 |
+
|
| 644 |
+
elif cmd == CommandType.NOOP:
|
| 645 |
+
self._state.current_uptime = max(0.0, self._state.current_uptime - 0.05)
|
| 646 |
+
logs = [f"[METRICS] Users continually locked out. Uptime {self._state.current_uptime:.0%}"]
|
| 647 |
+
return 0.0, "No action. Outage continues.", logs, False, 0.0
|
| 648 |
+
|
| 649 |
+
return (
|
| 650 |
+
0.0,
|
| 651 |
+
f"Command '{action.command_type.value}' doesn't help with a bad deployment.",
|
| 652 |
+
[],
|
| 653 |
+
False,
|
| 654 |
+
0.0,
|
| 655 |
+
)
|
| 656 |
+
|
| 657 |
+
def _make_observation(
|
| 658 |
+
self,
|
| 659 |
+
message: str,
|
| 660 |
+
logs: list[str],
|
| 661 |
+
success: bool,
|
| 662 |
+
reward: float | None = None,
|
| 663 |
+
done: bool = False,
|
| 664 |
+
) -> SREObservation:
|
| 665 |
+
"""Build a full SREObservation with current metrics."""
|
| 666 |
+
config = TASK_CONFIGS[self._state.task_difficulty]
|
| 667 |
+
|
| 668 |
+
metrics = SystemMetrics(
|
| 669 |
+
cpu_percent=self._rng.uniform(30, 85),
|
| 670 |
+
memory_percent=self._rng.uniform(40, 90),
|
| 671 |
+
latency_ms=self._current_latency_ms,
|
| 672 |
+
uptime=self._state.current_uptime,
|
| 673 |
+
error_rate=max(0.0, 1.0 - self._state.current_uptime),
|
| 674 |
+
budget_used=config["budget"] - self._state.budget_remaining,
|
| 675 |
+
)
|
| 676 |
+
|
| 677 |
+
available = self._get_available_actions()
|
| 678 |
+
|
| 679 |
+
metadata = {}
|
| 680 |
+
if done:
|
| 681 |
+
temp_obs = SREObservation(
|
| 682 |
+
message=message,
|
| 683 |
+
logs=logs,
|
| 684 |
+
success=success,
|
| 685 |
+
metrics=metrics,
|
| 686 |
+
available_actions=available,
|
| 687 |
+
task_description=self._state.task_description,
|
| 688 |
+
done=done,
|
| 689 |
+
reward=reward,
|
| 690 |
+
)
|
| 691 |
+
score = self.rubric(None, temp_obs)
|
| 692 |
+
metadata["grader_score"] = score
|
| 693 |
+
metadata["total_accumulated_reward"] = self._state.total_reward
|
| 694 |
+
|
| 695 |
+
message = f"{message} [GRADER_SCORE: {score:.3f}]"
|
| 696 |
+
|
| 697 |
+
obs = SREObservation(
|
| 698 |
+
message=message,
|
| 699 |
+
logs=logs,
|
| 700 |
+
success=success,
|
| 701 |
+
metrics=metrics,
|
| 702 |
+
available_actions=available,
|
| 703 |
+
task_description=self._state.task_description,
|
| 704 |
+
done=done,
|
| 705 |
+
reward=reward,
|
| 706 |
+
metadata=metadata,
|
| 707 |
+
)
|
| 708 |
+
|
| 709 |
+
return obs
|
| 710 |
+
|
| 711 |
+
def _get_available_actions(self) -> list[str]:
|
| 712 |
+
"""Return contextually reasonable action types."""
|
| 713 |
+
actions = [CommandType.DIAGNOSE.value, CommandType.NOOP.value]
|
| 714 |
+
difficulty = self._state.task_difficulty
|
| 715 |
+
|
| 716 |
+
# Dynamically restrict dangerous actions until diagnosis is complete
|
| 717 |
+
if difficulty == TaskDifficulty.EASY:
|
| 718 |
+
if self._diagnosed:
|
| 719 |
+
actions.append(CommandType.RESTART_POD.value)
|
| 720 |
+
elif difficulty == TaskDifficulty.MEDIUM:
|
| 721 |
+
if self._diagnosed:
|
| 722 |
+
actions.append(CommandType.RUN_SQL.value)
|
| 723 |
+
elif difficulty == TaskDifficulty.HARD:
|
| 724 |
+
if self._diagnosed:
|
| 725 |
+
actions.append(CommandType.SCALE_SERVERS.value)
|
| 726 |
+
elif difficulty == TaskDifficulty.EXTREME:
|
| 727 |
+
if self._diagnosed:
|
| 728 |
+
actions.append(CommandType.CHECK_LOGS.value)
|
| 729 |
+
if self._logs_checked:
|
| 730 |
+
actions.append(CommandType.ROLLBACK.value)
|
| 731 |
+
|
| 732 |
+
return actions
|
| 733 |
+
|
| 734 |
+
def _generate_initial_logs(self, difficulty: TaskDifficulty) -> list[str]:
|
| 735 |
+
"""Generate realistic initial alert logs for each scenario."""
|
| 736 |
+
if difficulty == TaskDifficulty.EASY:
|
| 737 |
+
return [
|
| 738 |
+
"[ALERT] PagerDuty: High-severity alert triggered for 'production/web'",
|
| 739 |
+
"[K8S] Event: pod-web-3 - Back-off restarting failed container (OOMKilled)",
|
| 740 |
+
"[K8S] Event: pod-web-3 - Container 'web-app' memory limit 512Mi exceeded",
|
| 741 |
+
"[METRICS] Cluster uptime dropped to 45%. Error rate: 55%.",
|
| 742 |
+
"[HINT] Available commands: diagnose, restart_pod, noop",
|
| 743 |
+
]
|
| 744 |
+
elif difficulty == TaskDifficulty.MEDIUM:
|
| 745 |
+
return [
|
| 746 |
+
"[ALERT] Datadog: API latency P99 > 10,000 ms on 'orders' service",
|
| 747 |
+
"[DB] Slow query log (last 5 min): 847 queries exceeding 1,000 ms",
|
| 748 |
+
"[DB] Top offender: SELECT * FROM orders_table WHERE customer_id = ?",
|
| 749 |
+
"[METRICS] API uptime: 70%. User-facing errors increasing.",
|
| 750 |
+
"[HINT] Available commands: diagnose, run_sql, noop",
|
| 751 |
+
]
|
| 752 |
+
elif difficulty == TaskDifficulty.HARD:
|
| 753 |
+
return [
|
| 754 |
+
"[ALERT] CloudWatch: Incoming traffic surge - 50,000 RPS (10× normal)",
|
| 755 |
+
"[LB] Load balancer: 90% of requests timing out. Current capacity: 5,000 RPS.",
|
| 756 |
+
"[CLOUD] Active servers: 1. Budget remaining: $500.",
|
| 757 |
+
"[CLOUD] Server cost: $50/unit. Each adds 5,000 RPS capacity.",
|
| 758 |
+
"[METRICS] Uptime: 10%. Error rate: 90%. 45,000 requests/sec being dropped.",
|
| 759 |
+
"[HINT] Available commands: diagnose, scale_servers, noop",
|
| 760 |
+
]
|
| 761 |
+
elif difficulty == TaskDifficulty.EXTREME:
|
| 762 |
+
return [
|
| 763 |
+
"[ALERT] Sentry: Massive spike in 500 errors for 'auth-service'",
|
| 764 |
+
"[ALERT] Zendesk: 1,200 new customer tickets 'Cannot login'",
|
| 765 |
+
"[K8S] Event: auth-service deployment v1.4.3 just completed",
|
| 766 |
+
"[METRICS] Global uptime dropped to 5%. Critical outage.",
|
| 767 |
+
"[HINT] Available commands: diagnose, check_logs, rollback, noop",
|
| 768 |
+
]
|
| 769 |
+
return []
|
server/requirements.txt
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
fastapi>=0.100.0
|
| 2 |
+
uvicorn[standard]>=0.23.0
|
| 3 |
+
pydantic>=2.0.0
|
| 4 |
+
openenv-core>=0.2.2
|
| 5 |
+
rich>=13.0.0
|
server/rubrics.py
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from typing import Any
|
| 2 |
+
|
| 3 |
+
from openenv.core.rubrics.base import Rubric
|
| 4 |
+
|
| 5 |
+
from sre_env.models import TaskDifficulty
|
| 6 |
+
|
| 7 |
+
|
| 8 |
+
class SREGraderRubric(Rubric):
|
| 9 |
+
def __init__(self, env: Any) -> None:
|
| 10 |
+
super().__init__()
|
| 11 |
+
self._env = env
|
| 12 |
+
|
| 13 |
+
def forward(self, action: Any, observation: Any) -> float:
|
| 14 |
+
difficulty = self._env.state.task_difficulty
|
| 15 |
+
|
| 16 |
+
if difficulty == TaskDifficulty.EASY:
|
| 17 |
+
return self._grade_easy()
|
| 18 |
+
elif difficulty == TaskDifficulty.MEDIUM:
|
| 19 |
+
return self._grade_medium()
|
| 20 |
+
elif difficulty == TaskDifficulty.HARD:
|
| 21 |
+
return self._grade_hard()
|
| 22 |
+
elif difficulty == TaskDifficulty.EXTREME:
|
| 23 |
+
return self._grade_extreme()
|
| 24 |
+
return 0.0
|
| 25 |
+
|
| 26 |
+
def _grade_easy(self) -> float:
|
| 27 |
+
"""Proportional to restored uptime (target ≥ 0.95)."""
|
| 28 |
+
return min(1.0, self._env.state.current_uptime / 0.95)
|
| 29 |
+
|
| 30 |
+
def _grade_medium(self) -> float:
|
| 31 |
+
"""Proportional to latency improvement."""
|
| 32 |
+
from server.environment import TASK_CONFIGS
|
| 33 |
+
|
| 34 |
+
config = TASK_CONFIGS[TaskDifficulty.MEDIUM]
|
| 35 |
+
baseline = config["initial_latency_ms"]
|
| 36 |
+
target = config["target_latency_ms"]
|
| 37 |
+
|
| 38 |
+
# Access internal environment state
|
| 39 |
+
current = self._env._current_latency_ms
|
| 40 |
+
|
| 41 |
+
if current <= target:
|
| 42 |
+
return 1.0
|
| 43 |
+
|
| 44 |
+
improvement_ratio = (baseline - current) / (baseline - target)
|
| 45 |
+
return max(0.0, min(1.0, improvement_ratio))
|
| 46 |
+
|
| 47 |
+
def _grade_hard(self) -> float:
|
| 48 |
+
"""Composite: 50% uptime + 30% budget + 20% speed bonus."""
|
| 49 |
+
from server.environment import TASK_CONFIGS
|
| 50 |
+
|
| 51 |
+
config = TASK_CONFIGS[TaskDifficulty.HARD]
|
| 52 |
+
|
| 53 |
+
# Uptime score
|
| 54 |
+
uptime_score = min(1.0, self._env.state.current_uptime / 0.90)
|
| 55 |
+
|
| 56 |
+
# Budget score — proportion of budget remaining
|
| 57 |
+
budget_score = max(0.0, self._env.state.budget_remaining / config["budget"])
|
| 58 |
+
|
| 59 |
+
# Speed bonus — fewer steps = higher bonus
|
| 60 |
+
speed_bonus = max(0.0, 1.0 - (self._env.state.step_count / self._env.state.max_steps))
|
| 61 |
+
|
| 62 |
+
return 0.5 * uptime_score + 0.3 * budget_score + 0.2 * speed_bonus
|
| 63 |
+
|
| 64 |
+
def _grade_extreme(self) -> float:
|
| 65 |
+
"""Composite: 70% uptime + 30% speed bonus (No budget used in rollback)."""
|
| 66 |
+
uptime_score = min(1.0, self._env.state.current_uptime / 0.95)
|
| 67 |
+
speed_bonus = max(0.0, 1.0 - (self._env.state.step_count / self._env.state.max_steps))
|
| 68 |
+
return 0.7 * uptime_score + 0.3 * speed_bonus
|
server/schemas/reset.py
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from typing import List
|
| 2 |
+
|
| 3 |
+
from pydantic import BaseModel
|
| 4 |
+
|
| 5 |
+
|
| 6 |
+
class CustomResetRequest(BaseModel):
|
| 7 |
+
seed: int
|
| 8 |
+
difficulty: str
|
| 9 |
+
custom_description: str
|
| 10 |
+
custom_logs: List[str]
|
sre_env/__init__.py
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from sre_env.models import SREAction, SREObservation, SREState
|
| 2 |
+
|
| 3 |
+
__all__ = ["SREAction", "SREObservation", "SREState"]
|
sre_env/constants/prompts.py
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
SYSTEM_PROMPT = """
|
| 2 |
+
You are an expert SRE AI agent. You must respond ONLY with a JSON object.
|
| 3 |
+
Your goal is to resolve the incident with the highest uptime and lowest cost.
|
| 4 |
+
Always 'diagnose' first to understand the root cause before taking corrective actions.
|
| 5 |
+
|
| 6 |
+
Schema:
|
| 7 |
+
{
|
| 8 |
+
"command_type": "diagnose" | "restart_pod" | "run_sql" | "scale_servers" | "noop",
|
| 9 |
+
"target_resource": "<resource_id>",
|
| 10 |
+
"parameters": {}
|
| 11 |
+
}
|
| 12 |
+
|
| 13 |
+
Command Details:
|
| 14 |
+
- diagnose: Sets target_resource to 'system' or the afflicted component.
|
| 15 |
+
- restart_pod: Sets target_resource to the exact failing pod name.
|
| 16 |
+
- run_sql: Sets target_resource to the table name. Requires "sql" parameter, e.g., {"sql": "CREATE INDEX ..."}.
|
| 17 |
+
- scale_servers: Sets target_resource to the cluster name. Requires "replicas" integer parameter, e.g., {"replicas": 5}.
|
| 18 |
+
|
| 19 |
+
Examples:
|
| 20 |
+
1. {"command_type": "diagnose", "target_resource": "production", "parameters": {}}
|
| 21 |
+
2. {"command_type": "restart_pod", "target_resource": "pod-web-3", "parameters": {}}
|
| 22 |
+
3. {"command_type": "run_sql", "target_resource": "orders_table", "parameters": {"sql": "CREATE INDEX idx_customer_id ON orders_table(customer_id)"}}
|
| 23 |
+
4. {"command_type": "scale_servers", "target_resource": "us-east-cluster", "parameters": {"replicas": 5}}
|
| 24 |
+
|
| 25 |
+
Respond strictly with the JSON object. Do not include markdown formatting or conversational text.
|
| 26 |
+
"""
|
| 27 |
+
|
| 28 |
+
ROUTER_SYSTEM_PROMPT = """
|
| 29 |
+
You are an SRE Incident Router. Map the user's custom incident specifically to one of these 4 archetypes:
|
| 30 |
+
1. "easy": Requires pod restarts. Target MUST BE 'pod-web-3'.
|
| 31 |
+
2. "medium": Requires adding DB indexes. Target MUST BE 'orders_table'.
|
| 32 |
+
3. "hard": Requires scaling servers. Target MUST BE 'system'.
|
| 33 |
+
4. "extreme": Requires diagnosing auth failures and rollback. Target MUST BE 'auth-service'.
|
| 34 |
+
|
| 35 |
+
If the custom incident is unrelated (e.g. hacking, UI design), reject it.
|
| 36 |
+
Return JSON ONLY:
|
| 37 |
+
{
|
| 38 |
+
"accepted": true/false,
|
| 39 |
+
"archetype": "easy|medium|hard|extreme",
|
| 40 |
+
"reason": "If rejected, explain why. Omit if accepted.",
|
| 41 |
+
"custom_description": "INCIDENT: Rewritten professional incident describing the problem, retaining the target requirement.",
|
| 42 |
+
"custom_logs": ["Simulated terminal log 1", "log 2", "log 3"]
|
| 43 |
+
}
|
| 44 |
+
"""
|
sre_env/core/client.py
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import sys
|
| 2 |
+
|
| 3 |
+
import requests
|
| 4 |
+
|
| 5 |
+
|
| 6 |
+
def env_reset(base_url: str, seed: int | None = None) -> dict:
|
| 7 |
+
try:
|
| 8 |
+
payload = {"seed": seed} if seed is not None else {}
|
| 9 |
+
resp = requests.post(f"{base_url}/reset", json=payload, timeout=30)
|
| 10 |
+
resp.raise_for_status()
|
| 11 |
+
return resp.json()
|
| 12 |
+
except requests.exceptions.ConnectionError:
|
| 13 |
+
print(f"❌ Error: Could not connect to environment server at {base_url}")
|
| 14 |
+
sys.exit(1)
|
| 15 |
+
|
| 16 |
+
|
| 17 |
+
def env_step(base_url: str, action: dict) -> dict:
|
| 18 |
+
resp = requests.post(f"{base_url}/step", json={"action": action}, timeout=30)
|
| 19 |
+
resp.raise_for_status()
|
| 20 |
+
return resp.json()
|
sre_env/inference.py
ADDED
|
@@ -0,0 +1,187 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import json
|
| 2 |
+
import os
|
| 3 |
+
import sys
|
| 4 |
+
import time
|
| 5 |
+
from datetime import datetime
|
| 6 |
+
|
| 7 |
+
import requests
|
| 8 |
+
from dotenv import load_dotenv
|
| 9 |
+
from openai import OpenAI
|
| 10 |
+
from rich.console import Console
|
| 11 |
+
from rich.panel import Panel
|
| 12 |
+
from rich.prompt import Prompt
|
| 13 |
+
|
| 14 |
+
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), "..")))
|
| 15 |
+
|
| 16 |
+
from sre_env.constants.prompts import ROUTER_SYSTEM_PROMPT, SYSTEM_PROMPT
|
| 17 |
+
from sre_env.core.client import env_reset, env_step
|
| 18 |
+
from sre_env.utils.parser import extract_json
|
| 19 |
+
|
| 20 |
+
load_dotenv()
|
| 21 |
+
|
| 22 |
+
API_BASE_URL = os.getenv("API_BASE_URL")
|
| 23 |
+
MODEL_NAME = os.getenv("MODEL_NAME")
|
| 24 |
+
HF_TOKEN = os.getenv("HF_TOKEN")
|
| 25 |
+
ENV_BASE_URL = os.getenv("ENV_BASE_URL")
|
| 26 |
+
|
| 27 |
+
MAX_STEPS = 30
|
| 28 |
+
TIMEOUT_MINUTES = 18
|
| 29 |
+
|
| 30 |
+
|
| 31 |
+
def main():
|
| 32 |
+
console = Console()
|
| 33 |
+
client = OpenAI(base_url=API_BASE_URL, api_key=HF_TOKEN or "dummy")
|
| 34 |
+
|
| 35 |
+
console.print(
|
| 36 |
+
Panel.fit(
|
| 37 |
+
"[bold cyan]🛡️ Sentinel-SRE Autonomous Agent[/bold cyan]\nInteractive Incident Injector",
|
| 38 |
+
border_style="cyan",
|
| 39 |
+
)
|
| 40 |
+
)
|
| 41 |
+
|
| 42 |
+
console.print("\n[bold white]Select an Incident to inject into the environment:[/bold white]")
|
| 43 |
+
console.print(" [green][1][/green] 🟢 OOMKilled Pod Crash (Easy)")
|
| 44 |
+
console.print(" [yellow][2][/yellow] 🟡 Missing Database Index (Medium)")
|
| 45 |
+
console.print(" [red][3][/red] 🔴 50,000 RPS Traffic Spike (Hard)")
|
| 46 |
+
console.print(" [magenta][4][/magenta] 💀 Bad Code Deployment (Extreme)")
|
| 47 |
+
console.print(" [blue][5][/blue] 🧠 Dynamic Task Generation (Custom Prompt)")
|
| 48 |
+
|
| 49 |
+
while True:
|
| 50 |
+
choice = Prompt.ask(
|
| 51 |
+
"\n[bold cyan]Enter scenario[/bold cyan]",
|
| 52 |
+
choices=["1", "2", "3", "4", "5"],
|
| 53 |
+
default="1",
|
| 54 |
+
)
|
| 55 |
+
|
| 56 |
+
if choice == "5":
|
| 57 |
+
user_prompt = Prompt.ask("\n[bold blue]Describe the incident you want to simulate[/bold blue]")
|
| 58 |
+
|
| 59 |
+
with console.status("[bold blue]LLM Analyzing Scenario...", spinner="dots"):
|
| 60 |
+
try:
|
| 61 |
+
resp = client.chat.completions.create(
|
| 62 |
+
model=MODEL_NAME,
|
| 63 |
+
messages=[
|
| 64 |
+
{"role": "system", "content": ROUTER_SYSTEM_PROMPT},
|
| 65 |
+
{"role": "user", "content": user_prompt},
|
| 66 |
+
],
|
| 67 |
+
response_format={"type": "json_object"},
|
| 68 |
+
)
|
| 69 |
+
result = json.loads(resp.choices[0].message.content)
|
| 70 |
+
if not result.get("accepted"):
|
| 71 |
+
console.print(f"[bold red]❌ Rejected:[/bold red] {result.get('reason')}")
|
| 72 |
+
continue
|
| 73 |
+
|
| 74 |
+
diff_map = {"easy": 2, "medium": 1, "hard": 5, "extreme": 0}
|
| 75 |
+
chosen_seed = diff_map.get(result.get("archetype", "easy"), 2)
|
| 76 |
+
|
| 77 |
+
with console.status("[bold green]Injecting Custom Chaos...", spinner="aesthetic"):
|
| 78 |
+
payload = {
|
| 79 |
+
"seed": chosen_seed,
|
| 80 |
+
"difficulty": result.get("archetype", "easy"),
|
| 81 |
+
"custom_description": result["custom_description"],
|
| 82 |
+
"custom_logs": result.get("custom_logs", []),
|
| 83 |
+
}
|
| 84 |
+
r = requests.post(f"{ENV_BASE_URL}/custom_reset", json=payload, timeout=10)
|
| 85 |
+
reset_resp = r.json()
|
| 86 |
+
break
|
| 87 |
+
except Exception as e:
|
| 88 |
+
console.print(f"[bold red]LLM Error:[/bold red] {e}")
|
| 89 |
+
continue
|
| 90 |
+
else:
|
| 91 |
+
seed_map = {"1": 2, "2": 1, "3": 5, "4": 0}
|
| 92 |
+
chosen_seed = seed_map[choice]
|
| 93 |
+
with console.status("[bold green]Injecting Static Chaos...", spinner="aesthetic"):
|
| 94 |
+
reset_resp = env_reset(ENV_BASE_URL, seed=chosen_seed)
|
| 95 |
+
break
|
| 96 |
+
|
| 97 |
+
observation = reset_resp.get("observation", {})
|
| 98 |
+
done = reset_resp.get("done", False)
|
| 99 |
+
|
| 100 |
+
total_reward = 0.0
|
| 101 |
+
step_count = 0
|
| 102 |
+
start_time = time.time()
|
| 103 |
+
|
| 104 |
+
audit_trail = {
|
| 105 |
+
"timestamp": datetime.utcnow().isoformat(),
|
| 106 |
+
"task_description": observation.get("task_description", "N/A"),
|
| 107 |
+
"trajectory": [],
|
| 108 |
+
}
|
| 109 |
+
|
| 110 |
+
console.print(f"[bold yellow]📋 Task:[/bold yellow] {audit_trail['task_description']}")
|
| 111 |
+
|
| 112 |
+
while not done and step_count < MAX_STEPS:
|
| 113 |
+
if (time.time() - start_time) / 60 > TIMEOUT_MINUTES:
|
| 114 |
+
console.print("[bold red]Timeout Reached![/bold red]")
|
| 115 |
+
break
|
| 116 |
+
|
| 117 |
+
step_count += 1
|
| 118 |
+
console.print(f"\n[bold magenta]── Step {step_count} ──────────────────────────────────[/bold magenta]")
|
| 119 |
+
|
| 120 |
+
action = {}
|
| 121 |
+
with console.status("[bold green]Agent Thinking...", spinner="dots"):
|
| 122 |
+
try:
|
| 123 |
+
history = [
|
| 124 |
+
{"role": "system", "content": SYSTEM_PROMPT},
|
| 125 |
+
{"role": "user", "content": f"Context: {json.dumps(observation)}"},
|
| 126 |
+
]
|
| 127 |
+
response = client.chat.completions.create(
|
| 128 |
+
model=MODEL_NAME, messages=history, max_tokens=512, temperature=0.1
|
| 129 |
+
)
|
| 130 |
+
raw_content = response.choices[0].message.content.strip()
|
| 131 |
+
action = extract_json(raw_content)
|
| 132 |
+
except Exception as e:
|
| 133 |
+
console.print(f"[bold red]❌ LLM/Parse Error:[/bold red] {e}")
|
| 134 |
+
break
|
| 135 |
+
|
| 136 |
+
cmd_type = action.get("command_type", "UNKNOWN")
|
| 137 |
+
target = action.get("target_resource", "UNKNOWN")
|
| 138 |
+
console.print(f" [bold blue]🚀 Action Request:[/bold blue] {cmd_type} [white]on[/white] {target}")
|
| 139 |
+
|
| 140 |
+
step_resp = env_step(ENV_BASE_URL, action)
|
| 141 |
+
prev_obs = observation
|
| 142 |
+
observation = step_resp.get("observation", {})
|
| 143 |
+
reward = step_resp.get("reward", 0.0) or 0.0
|
| 144 |
+
done = step_resp.get("done", False)
|
| 145 |
+
total_reward += reward
|
| 146 |
+
|
| 147 |
+
audit_trail["trajectory"].append(
|
| 148 |
+
{
|
| 149 |
+
"step": step_count,
|
| 150 |
+
"observation_in": prev_obs,
|
| 151 |
+
"action": action,
|
| 152 |
+
"reward": reward,
|
| 153 |
+
"observation_out": observation,
|
| 154 |
+
}
|
| 155 |
+
)
|
| 156 |
+
|
| 157 |
+
msg = observation.get("message", "")
|
| 158 |
+
color = "green" if "Success" in msg or "restored" in msg else "yellow" if "Already" in msg else "red"
|
| 159 |
+
console.print(f" [bold {color}]► Result:[/bold {color}] {msg}")
|
| 160 |
+
console.print(
|
| 161 |
+
f" [bold cyan]► Reward:[/bold cyan] {reward:+.3f} | [bold cyan]Total:[/bold cyan] {total_reward:+.3f}"
|
| 162 |
+
)
|
| 163 |
+
|
| 164 |
+
score = observation.get("metadata", {}).get("grader_score", "N/A")
|
| 165 |
+
final_msg = observation.get("message", "")
|
| 166 |
+
if "GRADER_SCORE:" in final_msg:
|
| 167 |
+
try:
|
| 168 |
+
score = final_msg.split("GRADER_SCORE: ")[1].split("]")[0]
|
| 169 |
+
except Exception:
|
| 170 |
+
pass
|
| 171 |
+
|
| 172 |
+
panel = Panel.fit(
|
| 173 |
+
f"[bold white]Resolution Complete[/bold white]\n"
|
| 174 |
+
f"[bold green]Final Grade:[/bold green] {score}\n"
|
| 175 |
+
f"[bold green]Total Steps:[/bold green] {step_count}",
|
| 176 |
+
title="[bold cyan]End of Episode[/bold cyan]",
|
| 177 |
+
border_style="cyan",
|
| 178 |
+
)
|
| 179 |
+
console.print("\n", panel)
|
| 180 |
+
|
| 181 |
+
with open("agent_trace.json", "w") as f:
|
| 182 |
+
json.dump(audit_trail, f, indent=2)
|
| 183 |
+
console.print("[dim]Audit trail saved to agent_trace.json[/dim]")
|
| 184 |
+
|
| 185 |
+
|
| 186 |
+
if __name__ == "__main__":
|
| 187 |
+
main()
|
sre_env/models.py
ADDED
|
@@ -0,0 +1,141 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from enum import Enum
|
| 2 |
+
from typing import Any, Dict, List
|
| 3 |
+
|
| 4 |
+
from openenv.core.env_server.types import Action, Observation, State
|
| 5 |
+
from pydantic import Field
|
| 6 |
+
|
| 7 |
+
|
| 8 |
+
class CommandType(str, Enum):
|
| 9 |
+
"""Available SRE command types."""
|
| 10 |
+
|
| 11 |
+
RESTART_POD = "restart_pod"
|
| 12 |
+
RUN_SQL = "run_sql"
|
| 13 |
+
SCALE_SERVERS = "scale_servers"
|
| 14 |
+
DIAGNOSE = "diagnose"
|
| 15 |
+
CHECK_LOGS = "check_logs"
|
| 16 |
+
ROLLBACK = "rollback"
|
| 17 |
+
NOOP = "noop"
|
| 18 |
+
|
| 19 |
+
|
| 20 |
+
class TaskDifficulty(str, Enum):
|
| 21 |
+
"""Task difficulty tiers."""
|
| 22 |
+
|
| 23 |
+
EASY = "easy"
|
| 24 |
+
MEDIUM = "medium"
|
| 25 |
+
HARD = "hard"
|
| 26 |
+
EXTREME = "extreme"
|
| 27 |
+
|
| 28 |
+
|
| 29 |
+
class SREAction(Action):
|
| 30 |
+
"""An SRE command issued by the agent.
|
| 31 |
+
|
| 32 |
+
Attributes:
|
| 33 |
+
command_type: The type of SRE command to execute. Validated dynamically in the environment.
|
| 34 |
+
target_resource: The resource to target (e.g. pod name, table name, cluster).
|
| 35 |
+
parameters: Additional key-value parameters for the command.
|
| 36 |
+
"""
|
| 37 |
+
|
| 38 |
+
command_type: str = Field(
|
| 39 |
+
default="noop",
|
| 40 |
+
description="Type of SRE command to execute (restart_pod, run_sql, scale_servers, diagnose, check_logs, rollback, noop).",
|
| 41 |
+
)
|
| 42 |
+
target_resource: str = Field(
|
| 43 |
+
default="",
|
| 44 |
+
description="The target resource identifier (e.g. 'pod-web-3', 'orders_table', 'us-east-cluster').",
|
| 45 |
+
)
|
| 46 |
+
parameters: Dict[str, Any] = Field(
|
| 47 |
+
default_factory=dict,
|
| 48 |
+
description="Additional command parameters (e.g. {'replicas': 5} for scale_servers).",
|
| 49 |
+
)
|
| 50 |
+
|
| 51 |
+
|
| 52 |
+
class SystemMetrics(Action):
|
| 53 |
+
"""Current system metrics snapshot — embedded inside observations.
|
| 54 |
+
|
| 55 |
+
Note: Inherits from Action (BaseModel) purely for Pydantic config;
|
| 56 |
+
semantically this is just a nested model.
|
| 57 |
+
"""
|
| 58 |
+
|
| 59 |
+
model_config = Action.model_config.copy()
|
| 60 |
+
model_config["extra"] = "allow"
|
| 61 |
+
|
| 62 |
+
cpu_percent: float = Field(default=0.0, description="CPU utilisation 0-100")
|
| 63 |
+
memory_percent: float = Field(default=0.0, description="Memory utilisation 0-100")
|
| 64 |
+
latency_ms: float = Field(default=0.0, description="P99 latency in milliseconds")
|
| 65 |
+
uptime: float = Field(default=1.0, description="Service uptime ratio 0.0-1.0")
|
| 66 |
+
error_rate: float = Field(default=0.0, description="Error rate 0.0-1.0")
|
| 67 |
+
budget_used: float = Field(default=0.0, description="Cloud budget consumed in $")
|
| 68 |
+
|
| 69 |
+
|
| 70 |
+
class SREObservation(Observation):
|
| 71 |
+
"""Observation returned after each environment step.
|
| 72 |
+
|
| 73 |
+
Combines human-readable messages, structured logs, success flags,
|
| 74 |
+
and real-time system metrics.
|
| 75 |
+
"""
|
| 76 |
+
|
| 77 |
+
message: str = Field(
|
| 78 |
+
default="",
|
| 79 |
+
description="Human-readable status message describing the current situation.",
|
| 80 |
+
)
|
| 81 |
+
logs: List[str] = Field(
|
| 82 |
+
default_factory=list,
|
| 83 |
+
description="Simulated structured log lines from the SRE system.",
|
| 84 |
+
)
|
| 85 |
+
success: bool = Field(
|
| 86 |
+
default=False,
|
| 87 |
+
description="Whether the last action was successful.",
|
| 88 |
+
)
|
| 89 |
+
metrics: SystemMetrics = Field(
|
| 90 |
+
default_factory=SystemMetrics,
|
| 91 |
+
description="Current system metrics snapshot.",
|
| 92 |
+
)
|
| 93 |
+
available_actions: List[str] = Field(
|
| 94 |
+
default_factory=list,
|
| 95 |
+
description="List of contextually valid action types for the current state.",
|
| 96 |
+
)
|
| 97 |
+
task_description: str = Field(
|
| 98 |
+
default="",
|
| 99 |
+
description="Description of the current incident scenario.",
|
| 100 |
+
)
|
| 101 |
+
|
| 102 |
+
|
| 103 |
+
class SREState(State):
|
| 104 |
+
"""Internal episode state tracked by the environment.
|
| 105 |
+
|
| 106 |
+
Extends the OpenEnv base State (which already provides episode_id and
|
| 107 |
+
step_count) with SRE-specific telemetry.
|
| 108 |
+
"""
|
| 109 |
+
|
| 110 |
+
task_difficulty: TaskDifficulty = Field(
|
| 111 |
+
default=TaskDifficulty.EASY,
|
| 112 |
+
description="Difficulty tier of the current task.",
|
| 113 |
+
)
|
| 114 |
+
task_description: str = Field(
|
| 115 |
+
default="",
|
| 116 |
+
description="Human-readable description of the active incident.",
|
| 117 |
+
)
|
| 118 |
+
current_uptime: float = Field(
|
| 119 |
+
default=1.0,
|
| 120 |
+
description="Current service uptime ratio (0.0 to 1.0).",
|
| 121 |
+
)
|
| 122 |
+
budget_remaining: float = Field(
|
| 123 |
+
default=500.0,
|
| 124 |
+
description="Remaining simulated cloud budget in USD.",
|
| 125 |
+
)
|
| 126 |
+
max_steps: int = Field(
|
| 127 |
+
default=15,
|
| 128 |
+
description="Maximum steps allowed for this episode.",
|
| 129 |
+
)
|
| 130 |
+
incident_resolved: bool = Field(
|
| 131 |
+
default=False,
|
| 132 |
+
description="Whether the incident has been fully resolved.",
|
| 133 |
+
)
|
| 134 |
+
root_cause_found: bool = Field(
|
| 135 |
+
default=False,
|
| 136 |
+
description="Whether the agent has identified the root cause.",
|
| 137 |
+
)
|
| 138 |
+
total_reward: float = Field(
|
| 139 |
+
default=0.0,
|
| 140 |
+
description="Cumulative reward accumulated during the episode.",
|
| 141 |
+
)
|
sre_env/tests/test_environment.py
ADDED
|
@@ -0,0 +1,113 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from server.environment import SREEnvironment
|
| 2 |
+
from sre_env.models import SREAction
|
| 3 |
+
|
| 4 |
+
|
| 5 |
+
def test_easy_episode():
|
| 6 |
+
env = SREEnvironment()
|
| 7 |
+
env.reset(seed=42)
|
| 8 |
+
# Ensure it's easy by forcing it if needed, or by injecting state
|
| 9 |
+
env.state.task_difficulty = "easy"
|
| 10 |
+
|
| 11 |
+
# 1. Diagnose
|
| 12 |
+
obs = env.step(SREAction(command_type="diagnose"))
|
| 13 |
+
assert obs.success is True
|
| 14 |
+
assert "pod-web-3" in obs.message
|
| 15 |
+
|
| 16 |
+
# 2. Restart wrong pod
|
| 17 |
+
obs2 = env.step(SREAction(command_type="restart_pod", target_resource="pod-web-not-real"))
|
| 18 |
+
assert obs2.success is False
|
| 19 |
+
assert obs2.reward < 0
|
| 20 |
+
|
| 21 |
+
# 3. Restart correct pod
|
| 22 |
+
obs3 = env.step(SREAction(command_type="restart_pod", target_resource="pod-web-3"))
|
| 23 |
+
assert obs3.success is True
|
| 24 |
+
assert obs3.done is True
|
| 25 |
+
assert obs3.metadata["grader_score"] > 0.8
|
| 26 |
+
|
| 27 |
+
|
| 28 |
+
def test_medium_episode():
|
| 29 |
+
env = SREEnvironment()
|
| 30 |
+
env.reset(seed=42)
|
| 31 |
+
env.state.task_difficulty = "medium"
|
| 32 |
+
|
| 33 |
+
# 1. Diagnose
|
| 34 |
+
env.step(SREAction(command_type="diagnose"))
|
| 35 |
+
|
| 36 |
+
# 2. Run wrong SQL
|
| 37 |
+
obs = env.step(
|
| 38 |
+
SREAction(
|
| 39 |
+
command_type="run_sql",
|
| 40 |
+
target_resource="users",
|
| 41 |
+
parameters={"sql": "CREATE INDEX on users"},
|
| 42 |
+
)
|
| 43 |
+
)
|
| 44 |
+
assert obs.success is False
|
| 45 |
+
|
| 46 |
+
# 3. Run partial SQL (wrong column)
|
| 47 |
+
obs2 = env.step(
|
| 48 |
+
SREAction(
|
| 49 |
+
command_type="run_sql",
|
| 50 |
+
target_resource="orders_table",
|
| 51 |
+
parameters={"sql": "CREATE INDEX on orders_table(date)"},
|
| 52 |
+
)
|
| 53 |
+
)
|
| 54 |
+
assert obs2.success is True
|
| 55 |
+
assert obs2.done is False
|
| 56 |
+
|
| 57 |
+
# 4. Run correct SQL
|
| 58 |
+
obs3 = env.step(
|
| 59 |
+
SREAction(
|
| 60 |
+
command_type="run_sql",
|
| 61 |
+
target_resource="orders_table",
|
| 62 |
+
parameters={"sql": "CREATE INDEX idx ON orders_table(customer_id)"},
|
| 63 |
+
)
|
| 64 |
+
)
|
| 65 |
+
assert obs3.success is True
|
| 66 |
+
assert obs3.done is True
|
| 67 |
+
assert obs3.metadata["grader_score"] > 0.9
|
| 68 |
+
|
| 69 |
+
|
| 70 |
+
def test_hard_episode():
|
| 71 |
+
env = SREEnvironment()
|
| 72 |
+
env.reset(seed=42)
|
| 73 |
+
env.state.task_difficulty = "hard"
|
| 74 |
+
|
| 75 |
+
env.step(SREAction(command_type="diagnose"))
|
| 76 |
+
|
| 77 |
+
# Scale too many (break budget)
|
| 78 |
+
obs = env.step(
|
| 79 |
+
SREAction(
|
| 80 |
+
command_type="scale_servers",
|
| 81 |
+
target_resource="cluster",
|
| 82 |
+
parameters={"replicas": 20},
|
| 83 |
+
)
|
| 84 |
+
)
|
| 85 |
+
assert obs.success is False
|
| 86 |
+
assert obs.done is True # Out of budget
|
| 87 |
+
assert obs.metadata["grader_score"] < 0.5
|
| 88 |
+
|
| 89 |
+
|
| 90 |
+
def test_extreme_episode():
|
| 91 |
+
env = SREEnvironment()
|
| 92 |
+
env.reset(seed=42)
|
| 93 |
+
env.state.task_difficulty = "extreme"
|
| 94 |
+
|
| 95 |
+
# 1. Diagnose
|
| 96 |
+
env.step(SREAction(command_type="diagnose"))
|
| 97 |
+
|
| 98 |
+
# 2. Check Logs
|
| 99 |
+
obs_logs = env.step(SREAction(command_type="check_logs", target_resource="auth-service"))
|
| 100 |
+
assert obs_logs.success is True
|
| 101 |
+
assert "v1.4.2" in str(obs_logs.logs)
|
| 102 |
+
|
| 103 |
+
# 3. Rollback
|
| 104 |
+
obs_rollback = env.step(
|
| 105 |
+
SREAction(
|
| 106 |
+
command_type="rollback",
|
| 107 |
+
target_resource="auth-service",
|
| 108 |
+
parameters={"revision": "v1.4.2"},
|
| 109 |
+
)
|
| 110 |
+
)
|
| 111 |
+
assert obs_rollback.success is True
|
| 112 |
+
assert obs_rollback.done is True
|
| 113 |
+
assert obs_rollback.metadata["grader_score"] > 0.8
|
sre_env/utils/parser.py
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import json
|
| 2 |
+
import re
|
| 3 |
+
|
| 4 |
+
|
| 5 |
+
def extract_json(text: str) -> dict:
|
| 6 |
+
"""Finds the first { and last } to extract JSON from conversational filler."""
|
| 7 |
+
try:
|
| 8 |
+
match = re.search(r"(\{.*\})", text, re.DOTALL)
|
| 9 |
+
if match:
|
| 10 |
+
clean_content = match.group(1)
|
| 11 |
+
clean_content = clean_content.replace("```json", "").replace("```", "")
|
| 12 |
+
return json.loads(clean_content)
|
| 13 |
+
return json.loads(text)
|
| 14 |
+
except Exception:
|
| 15 |
+
raise ValueError(f"Failed to parse LLM response as JSON. Content: {text[:100]}...")
|
uv.lock
ADDED
|
The diff for this file is too large to render.
See raw diff
|
|
|