Spaces:
Sleeping
Sleeping
Phase 1: repo setup and environment port
Browse files- .env.example +0 -0
- Dockerfile +16 -0
- PHASE_1.md +429 -0
- README.md +249 -0
- agents/__init__.py +0 -0
- amd_client.py +0 -0
- requirements.txt +18 -0
- run_agent.py +0 -0
- server/__init__.py +0 -0
- server/app.py +170 -0
- server/environment.py +341 -0
- server/graders/__init__.py +0 -0
- server/graders/base_grader.py +68 -0
- server/graders/cascade_grader.py +123 -0
- server/graders/crash_grader.py +110 -0
- server/log_generator.py +188 -0
- server/models.py +221 -0
- server/scenarios/__init__.py +0 -0
- server/scenarios/cascading.py +211 -0
- server/scenarios/silent_degrade.py +132 -0
- server/scenarios/single_crash.py +124 -0
.env.example
ADDED
|
File without changes
|
Dockerfile
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
FROM python:3.11-slim
|
| 2 |
+
|
| 3 |
+
WORKDIR /app
|
| 4 |
+
|
| 5 |
+
# Copy requirements first (layer caching)
|
| 6 |
+
COPY requirements.txt .
|
| 7 |
+
RUN pip install --no-cache-dir -r requirements.txt
|
| 8 |
+
|
| 9 |
+
# Copy all source
|
| 10 |
+
COPY . .
|
| 11 |
+
|
| 12 |
+
# Expose port (HF Spaces uses 7860)
|
| 13 |
+
EXPOSE 7860
|
| 14 |
+
|
| 15 |
+
# Start server
|
| 16 |
+
CMD ["uvicorn", "server.app:app", "--host", "0.0.0.0", "--port", "7860"]
|
PHASE_1.md
ADDED
|
@@ -0,0 +1,429 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# PHASE 1 β Repo Setup + Environment Port + AMD LLM Swap
|
| 2 |
+
|
| 3 |
+
> **Goal:** New repo created, old codebase copied cleanly, AMD Developer Cloud LLM working as a drop-in replacement for Groq. Environment boots, `/health` responds, and a test LLM call returns a valid response.
|
| 4 |
+
>
|
| 5 |
+
> **Time budget:** Day 1 (full day)
|
| 6 |
+
>
|
| 7 |
+
> **Success condition:** `curl http://localhost:7860/health` returns ok AND `python amd_client.py` returns a valid LLM response from AMD.
|
| 8 |
+
|
| 9 |
+
---
|
| 10 |
+
|
| 11 |
+
## STEP 1 β Create the New GitHub Repo
|
| 12 |
+
|
| 13 |
+
### 1.1 Create repo on GitHub
|
| 14 |
+
- Go to https://github.com/new
|
| 15 |
+
- Name: `agentic-triage-amd`
|
| 16 |
+
- Visibility: **Public**
|
| 17 |
+
- Initialize with: **nothing** (no README, no .gitignore)
|
| 18 |
+
- Click **Create repository**
|
| 19 |
+
|
| 20 |
+
### 1.2 Clone it locally
|
| 21 |
+
```bash
|
| 22 |
+
cd ~/projects # or wherever you keep your code
|
| 23 |
+
git clone https://github.com/YOUR_USERNAME/agentic-triage-amd.git
|
| 24 |
+
cd agentic-triage-amd
|
| 25 |
+
```
|
| 26 |
+
|
| 27 |
+
### 1.3 Set up base folder structure
|
| 28 |
+
```bash
|
| 29 |
+
mkdir -p server/scenarios server/graders agents
|
| 30 |
+
touch amd_client.py run_agent.py .env.example
|
| 31 |
+
```
|
| 32 |
+
|
| 33 |
+
**Checklist:**
|
| 34 |
+
- [ ] Repo created on GitHub as Public
|
| 35 |
+
- [ ] Cloned locally
|
| 36 |
+
- [ ] Folder structure created
|
| 37 |
+
|
| 38 |
+
---
|
| 39 |
+
|
| 40 |
+
## STEP 2 β Copy Core Files from Old Repo
|
| 41 |
+
|
| 42 |
+
### 2.1 Copy the server directory
|
| 43 |
+
From inside `agentic-triage-amd/`, run:
|
| 44 |
+
|
| 45 |
+
```bash
|
| 46 |
+
cp ../logtriage-env/server/app.py ./server/
|
| 47 |
+
cp ../logtriage-env/server/environment.py ./server/
|
| 48 |
+
cp ../logtriage-env/server/models.py ./server/
|
| 49 |
+
cp ../logtriage-env/server/log_generator.py ./server/
|
| 50 |
+
cp ../logtriage-env/server/scenarios/single_crash.py ./server/scenarios/
|
| 51 |
+
cp ../logtriage-env/server/scenarios/cascading.py ./server/scenarios/
|
| 52 |
+
cp ../logtriage-env/server/scenarios/silent_degrade.py ./server/scenarios/
|
| 53 |
+
cp ../logtriage-env/server/graders/base_grader.py ./server/graders/
|
| 54 |
+
cp ../logtriage-env/server/graders/crash_grader.py ./server/graders/
|
| 55 |
+
cp ../logtriage-env/server/graders/cascade_grader.py ./server/graders/
|
| 56 |
+
cp ../logtriage-env/Dockerfile ./
|
| 57 |
+
cp ../logtriage-env/requirements.txt ./
|
| 58 |
+
```
|
| 59 |
+
|
| 60 |
+
> Adjust the `../logtriage-env/` path to "C:\Users\Rohit\Desktop\logtriage-env" because we need to copy files from here
|
| 61 |
+
### 2.2 Create `__init__.py` files
|
| 62 |
+
```bash
|
| 63 |
+
touch server/__init__.py
|
| 64 |
+
touch server/scenarios/__init__.py
|
| 65 |
+
touch server/graders/__init__.py
|
| 66 |
+
touch agents/__init__.py
|
| 67 |
+
```
|
| 68 |
+
|
| 69 |
+
### 2.3 Verify structure
|
| 70 |
+
```bash
|
| 71 |
+
find . -type f -name "*.py" | sort
|
| 72 |
+
```
|
| 73 |
+
|
| 74 |
+
Expected output:
|
| 75 |
+
```
|
| 76 |
+
./agents/__init__.py
|
| 77 |
+
./amd_client.py
|
| 78 |
+
./run_agent.py
|
| 79 |
+
./server/__init__.py
|
| 80 |
+
./server/app.py
|
| 81 |
+
./server/environment.py
|
| 82 |
+
./server/graders/__init__.py
|
| 83 |
+
./server/graders/base_grader.py
|
| 84 |
+
./server/graders/cascade_grader.py
|
| 85 |
+
./server/graders/crash_grader.py
|
| 86 |
+
./server/log_generator.py
|
| 87 |
+
./server/models.py
|
| 88 |
+
./server/scenarios/__init__.py
|
| 89 |
+
./server/scenarios/cascading.py
|
| 90 |
+
./server/scenarios/single_crash.py
|
| 91 |
+
./server/scenarios/silent_degrade.py
|
| 92 |
+
```
|
| 93 |
+
|
| 94 |
+
**Checklist:**
|
| 95 |
+
- [ ] All server files copied
|
| 96 |
+
- [ ] All grader files copied
|
| 97 |
+
- [ ] All scenario files copied
|
| 98 |
+
- [ ] `__init__.py` files created in all 4 directories
|
| 99 |
+
|
| 100 |
+
---
|
| 101 |
+
|
| 102 |
+
## STEP 3 β Update requirements.txt
|
| 103 |
+
|
| 104 |
+
Replace the entire content of `requirements.txt` with:
|
| 105 |
+
|
| 106 |
+
```txt
|
| 107 |
+
# Environment
|
| 108 |
+
fastapi>=0.104.0
|
| 109 |
+
uvicorn>=0.24.0
|
| 110 |
+
pydantic>=2.0.0
|
| 111 |
+
openenv-core==0.2.3
|
| 112 |
+
|
| 113 |
+
# HTTP
|
| 114 |
+
requests>=2.25.0
|
| 115 |
+
httpx>=0.24.0
|
| 116 |
+
|
| 117 |
+
# LLM + Agents
|
| 118 |
+
openai>=1.0.0
|
| 119 |
+
langgraph>=0.1.0
|
| 120 |
+
langchain>=0.2.0
|
| 121 |
+
langchain-openai>=0.1.0
|
| 122 |
+
|
| 123 |
+
# Utilities
|
| 124 |
+
python-dotenv>=1.0.0
|
| 125 |
+
```
|
| 126 |
+
|
| 127 |
+
Install:
|
| 128 |
+
```bash
|
| 129 |
+
pip install -r requirements.txt
|
| 130 |
+
```
|
| 131 |
+
|
| 132 |
+
Verify:
|
| 133 |
+
```bash
|
| 134 |
+
python -c "import fastapi; import langgraph; import openai; print('All packages OK')"
|
| 135 |
+
```
|
| 136 |
+
|
| 137 |
+
**Checklist:**
|
| 138 |
+
- [ ] `requirements.txt` updated (Groq removed, LangGraph added)
|
| 139 |
+
- [ ] `pip install` completes without errors
|
| 140 |
+
- [ ] Verification print shows `All packages OK`
|
| 141 |
+
|
| 142 |
+
---
|
| 143 |
+
|
| 144 |
+
## STEP 4 β AMD Developer Cloud Setup
|
| 145 |
+
|
| 146 |
+
### 4.1 Get your AMD API key
|
| 147 |
+
- Go to https://www.amd.com/en/developer/resources/ml-software/developer-cloud.html
|
| 148 |
+
- Sign up / log in to AMD Developer Cloud
|
| 149 |
+
- Navigate to API Keys section in the dashboard
|
| 150 |
+
- Generate a new key β copy it immediately
|
| 151 |
+
|
| 152 |
+
### 4.2 Note your model and base URL
|
| 153 |
+
- In the dashboard, find the Inference / API section
|
| 154 |
+
- Note down exactly:
|
| 155 |
+
- The base URL (e.g. `https://api.amd.com/v1`)
|
| 156 |
+
- Available model names (e.g. `mistral-7b-instruct`, `llama-3-8b-instruct`)
|
| 157 |
+
|
| 158 |
+
### 4.3 Create `.env`
|
| 159 |
+
```bash
|
| 160 |
+
cat > .env << EOF
|
| 161 |
+
AMD_API_KEY=your_actual_key_here
|
| 162 |
+
AMD_BASE_URL=https://api.amd.com/v1
|
| 163 |
+
AMD_MODEL=mistral-7b-instruct
|
| 164 |
+
ENV_HOST=0.0.0.0
|
| 165 |
+
ENV_PORT=7860
|
| 166 |
+
EOF
|
| 167 |
+
```
|
| 168 |
+
|
| 169 |
+
### 4.4 Create `.env.example` (safe to commit)
|
| 170 |
+
```bash
|
| 171 |
+
cat > .env.example << EOF
|
| 172 |
+
AMD_API_KEY=your_amd_developer_cloud_api_key
|
| 173 |
+
AMD_BASE_URL=https://api.amd.com/v1
|
| 174 |
+
AMD_MODEL=mistral-7b-instruct
|
| 175 |
+
ENV_HOST=0.0.0.0
|
| 176 |
+
ENV_PORT=7860
|
| 177 |
+
EOF
|
| 178 |
+
```
|
| 179 |
+
|
| 180 |
+
### 4.5 Create `.gitignore`
|
| 181 |
+
```bash
|
| 182 |
+
cat > .gitignore << EOF
|
| 183 |
+
.env
|
| 184 |
+
__pycache__/
|
| 185 |
+
*.pyc
|
| 186 |
+
*.pyo
|
| 187 |
+
.DS_Store
|
| 188 |
+
*.egg-info/
|
| 189 |
+
dist/
|
| 190 |
+
build/
|
| 191 |
+
.venv/
|
| 192 |
+
venv/
|
| 193 |
+
EOF
|
| 194 |
+
```
|
| 195 |
+
|
| 196 |
+
**Checklist:**
|
| 197 |
+
- [ ] AMD Developer Cloud account active
|
| 198 |
+
- [ ] API key obtained and saved
|
| 199 |
+
- [ ] Base URL and model name confirmed from dashboard
|
| 200 |
+
- [ ] `.env` created with real values
|
| 201 |
+
- [ ] `.env.example` created
|
| 202 |
+
- [ ] `.gitignore` created
|
| 203 |
+
|
| 204 |
+
---
|
| 205 |
+
|
| 206 |
+
## STEP 5 β Write amd_client.py
|
| 207 |
+
|
| 208 |
+
Open `amd_client.py` and paste this:
|
| 209 |
+
|
| 210 |
+
```python
|
| 211 |
+
import os
|
| 212 |
+
from openai import OpenAI
|
| 213 |
+
from dotenv import load_dotenv
|
| 214 |
+
|
| 215 |
+
load_dotenv()
|
| 216 |
+
|
| 217 |
+
|
| 218 |
+
def get_amd_client() -> OpenAI:
|
| 219 |
+
"""Returns an OpenAI-compatible client pointed at AMD Developer Cloud."""
|
| 220 |
+
return OpenAI(
|
| 221 |
+
api_key=os.environ["AMD_API_KEY"],
|
| 222 |
+
base_url=os.environ["AMD_BASE_URL"],
|
| 223 |
+
)
|
| 224 |
+
|
| 225 |
+
|
| 226 |
+
def call_amd_llm(
|
| 227 |
+
prompt: str,
|
| 228 |
+
system_prompt: str = None,
|
| 229 |
+
temperature: float = 0.2
|
| 230 |
+
) -> str:
|
| 231 |
+
"""
|
| 232 |
+
Single LLM call to AMD Developer Cloud.
|
| 233 |
+
Returns the response text as a plain string.
|
| 234 |
+
"""
|
| 235 |
+
client = get_amd_client()
|
| 236 |
+
model = os.environ.get("AMD_MODEL", "mistral-7b-instruct")
|
| 237 |
+
|
| 238 |
+
messages = []
|
| 239 |
+
if system_prompt:
|
| 240 |
+
messages.append({"role": "system", "content": system_prompt})
|
| 241 |
+
messages.append({"role": "user", "content": prompt})
|
| 242 |
+
|
| 243 |
+
response = client.chat.completions.create(
|
| 244 |
+
model=model,
|
| 245 |
+
messages=messages,
|
| 246 |
+
temperature=temperature,
|
| 247 |
+
max_tokens=1024,
|
| 248 |
+
)
|
| 249 |
+
|
| 250 |
+
return response.choices[0].message.content.strip()
|
| 251 |
+
|
| 252 |
+
|
| 253 |
+
if __name__ == "__main__":
|
| 254 |
+
# Quick connection test
|
| 255 |
+
print("Testing AMD Developer Cloud connection...")
|
| 256 |
+
result = call_amd_llm(
|
| 257 |
+
prompt=(
|
| 258 |
+
"A payment-service is throwing NullPointerException. "
|
| 259 |
+
"Error rate is 100%. All downstream services are timing out. "
|
| 260 |
+
"What severity is this? Answer with P1, P2, or P3 only."
|
| 261 |
+
),
|
| 262 |
+
system_prompt="You are an expert Site Reliability Engineer. Be concise."
|
| 263 |
+
)
|
| 264 |
+
print(f"\nAMD LLM Response: {result}")
|
| 265 |
+
print("\nConnection test PASSED." if result else "Connection test FAILED.")
|
| 266 |
+
```
|
| 267 |
+
|
| 268 |
+
### Run the test:
|
| 269 |
+
```bash
|
| 270 |
+
python amd_client.py
|
| 271 |
+
```
|
| 272 |
+
|
| 273 |
+
Expected output:
|
| 274 |
+
```
|
| 275 |
+
Testing AMD Developer Cloud connection...
|
| 276 |
+
|
| 277 |
+
AMD LLM Response: P1
|
| 278 |
+
|
| 279 |
+
Connection test PASSED.
|
| 280 |
+
```
|
| 281 |
+
|
| 282 |
+
Any coherent text response means the AMD connection works.
|
| 283 |
+
|
| 284 |
+
**Checklist:**
|
| 285 |
+
- [ ] `amd_client.py` written
|
| 286 |
+
- [ ] `python amd_client.py` runs without errors
|
| 287 |
+
- [ ] AMD LLM returns a valid response
|
| 288 |
+
- [ ] No 401/403/404 errors
|
| 289 |
+
|
| 290 |
+
---
|
| 291 |
+
|
| 292 |
+
## STEP 6 β Verify the Environment Boots
|
| 293 |
+
|
| 294 |
+
### 6.1 Start the FastAPI server
|
| 295 |
+
```bash
|
| 296 |
+
uvicorn server.app:app --host 0.0.0.0 --port 7860 --reload
|
| 297 |
+
```
|
| 298 |
+
|
| 299 |
+
Watch the terminal β it should say `Application startup complete.`
|
| 300 |
+
|
| 301 |
+
### 6.2 Test all endpoints (new terminal)
|
| 302 |
+
```bash
|
| 303 |
+
# Health check
|
| 304 |
+
curl http://localhost:7860/health
|
| 305 |
+
|
| 306 |
+
# List tasks
|
| 307 |
+
curl http://localhost:7860/tasks
|
| 308 |
+
|
| 309 |
+
# Reset to single_crash task
|
| 310 |
+
curl -X POST http://localhost:7860/reset \
|
| 311 |
+
-H "Content-Type: application/json" \
|
| 312 |
+
-d '{"task_id": "single_crash", "seed": 42}'
|
| 313 |
+
```
|
| 314 |
+
|
| 315 |
+
Expected:
|
| 316 |
+
- `/health` β `{"status": "ok"}`
|
| 317 |
+
- `/tasks` β JSON array with 3 task objects
|
| 318 |
+
- `/reset` β Observation JSON with `logs`, `service_state`, `reward`, `done` fields
|
| 319 |
+
|
| 320 |
+
If `/reset` returns a proper observation with logs β the environment is fully working.
|
| 321 |
+
|
| 322 |
+
**Checklist:**
|
| 323 |
+
- [ ] Server starts with no import errors
|
| 324 |
+
- [ ] `/health` returns ok
|
| 325 |
+
- [ ] `/tasks` returns 3 tasks
|
| 326 |
+
- [ ] `/reset` with `single_crash` returns an observation with logs
|
| 327 |
+
|
| 328 |
+
---
|
| 329 |
+
|
| 330 |
+
## STEP 7 β Add README and First Git Push
|
| 331 |
+
|
| 332 |
+
### 7.1 Add the README
|
| 333 |
+
Copy the `README.md` from the files provided into the repo root. Update:
|
| 334 |
+
- Replace `YOUR_USERNAME` with your actual GitHub username
|
| 335 |
+
- Fill in your teammate's name in the Team table
|
| 336 |
+
|
| 337 |
+
### 7.2 Commit and push everything
|
| 338 |
+
```bash
|
| 339 |
+
# Stage all files
|
| 340 |
+
git add .
|
| 341 |
+
|
| 342 |
+
# Verify .env is NOT being staged
|
| 343 |
+
git status
|
| 344 |
+
# You should NOT see .env in the list β if you do, check .gitignore
|
| 345 |
+
|
| 346 |
+
# Initial commit
|
| 347 |
+
git commit -m "Phase 1: Repo setup, environment port, AMD LLM client"
|
| 348 |
+
|
| 349 |
+
# Push to GitHub
|
| 350 |
+
git push origin main
|
| 351 |
+
```
|
| 352 |
+
|
| 353 |
+
### 7.3 Verify on GitHub
|
| 354 |
+
- Open the repo on GitHub
|
| 355 |
+
- Confirm all files are there
|
| 356 |
+
- Confirm `.env` is NOT visible (only `.env.example` should be there)
|
| 357 |
+
|
| 358 |
+
**Checklist:**
|
| 359 |
+
- [ ] README added with correct username
|
| 360 |
+
- [ ] `git status` does NOT show `.env`
|
| 361 |
+
- [ ] Committed with descriptive message
|
| 362 |
+
- [ ] `git push` successful
|
| 363 |
+
- [ ] Repo looks clean on GitHub
|
| 364 |
+
|
| 365 |
+
---
|
| 366 |
+
|
| 367 |
+
## PHASE 1 COMPLETE β Final Verification
|
| 368 |
+
|
| 369 |
+
Run through every item before calling Phase 1 done:
|
| 370 |
+
|
| 371 |
+
- [ ] `agentic-triage-amd` repo exists on GitHub (Public)
|
| 372 |
+
- [ ] All server/ files present and importable
|
| 373 |
+
- [ ] LangGraph and openai installed without errors
|
| 374 |
+
- [ ] `.env` has real AMD credentials
|
| 375 |
+
- [ ] `python amd_client.py` β valid LLM response from AMD
|
| 376 |
+
- [ ] `uvicorn server.app:app` starts cleanly
|
| 377 |
+
- [ ] `/health` β ok
|
| 378 |
+
- [ ] `/tasks` β 3 tasks returned
|
| 379 |
+
- [ ] `/reset` β observation with logs returned
|
| 380 |
+
- [ ] `.env` NOT committed to GitHub
|
| 381 |
+
- [ ] First commit pushed successfully
|
| 382 |
+
|
| 383 |
+
---
|
| 384 |
+
|
| 385 |
+
## Troubleshooting
|
| 386 |
+
|
| 387 |
+
**`ModuleNotFoundError: No module named 'server'`**
|
| 388 |
+
```bash
|
| 389 |
+
# Always run from the project root directory
|
| 390 |
+
cd agentic-triage-amd
|
| 391 |
+
uvicorn server.app:app --host 0.0.0.0 --port 7860
|
| 392 |
+
```
|
| 393 |
+
|
| 394 |
+
**`AMD API returns 401 Unauthorized`**
|
| 395 |
+
- Double check `AMD_API_KEY` in `.env` β no extra spaces or quotes
|
| 396 |
+
- Confirm `python-dotenv` is installed and `load_dotenv()` is called
|
| 397 |
+
- Verify the key is active in the AMD Developer Cloud dashboard
|
| 398 |
+
|
| 399 |
+
**`AMD API returns 404 Not Found`**
|
| 400 |
+
- The model name might be wrong β check exact names in AMD dashboard
|
| 401 |
+
- Update `AMD_MODEL` in `.env` to match the exact available model string
|
| 402 |
+
|
| 403 |
+
**`AMD API returns connection error`**
|
| 404 |
+
- Check `AMD_BASE_URL` β get the exact URL from the dashboard, don't guess
|
| 405 |
+
|
| 406 |
+
**`ImportError` on any scenario or grader file**
|
| 407 |
+
- Check `__init__.py` exists in `server/`, `server/scenarios/`, `server/graders/`, `agents/`
|
| 408 |
+
```bash
|
| 409 |
+
find . -name "__init__.py"
|
| 410 |
+
```
|
| 411 |
+
|
| 412 |
+
**Port 7860 already in use**
|
| 413 |
+
```bash
|
| 414 |
+
# Kill whatever is on that port
|
| 415 |
+
lsof -i :7860
|
| 416 |
+
kill -9 <PID>
|
| 417 |
+
```
|
| 418 |
+
|
| 419 |
+
---
|
| 420 |
+
|
| 421 |
+
## What's Next β Phase 2 Preview
|
| 422 |
+
|
| 423 |
+
Phase 2 builds the three agents:
|
| 424 |
+
- `agents/planner.py` β reads the initial observation, outputs a triage strategy
|
| 425 |
+
- `agents/executor.py` β loops through steps, calls the environment, takes actions
|
| 426 |
+
- `agents/summarizer.py` β takes the completed episode, generates an incident report
|
| 427 |
+
- `agents/pipeline.py` β LangGraph graph wiring all three together
|
| 428 |
+
|
| 429 |
+
**Once all Phase 1 checkboxes are ticked, share your summary and we build Phase 2.**
|
README.md
ADDED
|
@@ -0,0 +1,249 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# π΄ AgentTriage β Agentic SRE Incident Response on AMD Developer Cloud
|
| 2 |
+
|
| 3 |
+
> Multi-agent log triage system that autonomously diagnoses production incidents using AMD-hosted LLMs and a LangGraph-powered agent pipeline.
|
| 4 |
+
|
| 5 |
+
Built for the **AMD Developer Cloud Hackathon β Track 1: AI Agents & Agentic Workflows**
|
| 6 |
+
|
| 7 |
+
---
|
| 8 |
+
|
| 9 |
+
## π What Is This?
|
| 10 |
+
|
| 11 |
+
AgentTriage is a production-grade agentic system where an AI agent pipeline automatically triages software incidents β the same work a human Site Reliability Engineer (SRE) does when production goes down.
|
| 12 |
+
|
| 13 |
+
When something breaks in production (a server crashes, a database causes a cascade failure, or a service silently degrades), engineers need to:
|
| 14 |
+
1. Diagnose the severity (P1/P2/P3)
|
| 15 |
+
2. Identify the root cause (which service/component)
|
| 16 |
+
3. Decide on remediation (restart, kill-query, flush-cache)
|
| 17 |
+
4. Escalate to the right team
|
| 18 |
+
|
| 19 |
+
AgentTriage automates this entire workflow using a **multi-agent pipeline** running on **AMD Developer Cloud**.
|
| 20 |
+
|
| 21 |
+
---
|
| 22 |
+
|
| 23 |
+
## π§ How It Works
|
| 24 |
+
|
| 25 |
+
### The Environment (LogTriageEnv)
|
| 26 |
+
A simulated microservice incident environment with a REST API interface (OpenEnv-compatible). The agent interacts via a reset β step loop, reads logs and service states, takes actions, and gets scored.
|
| 27 |
+
|
| 28 |
+
**Three incident scenarios:**
|
| 29 |
+
|
| 30 |
+
| Task | Difficulty | Noise | Incident Type |
|
| 31 |
+
|---|---|---|---|
|
| 32 |
+
| `single_crash` | Easy | 20% | Payment service NullPointerException |
|
| 33 |
+
| `cascading_failure` | Medium | 30% | user-db slow query β auth β gateway cascade |
|
| 34 |
+
| `silent_degradation` | Hard | 60% | Gradual payment-db latency increase (no crash) |
|
| 35 |
+
|
| 36 |
+
### The Agent Pipeline (New β AMD-Powered)
|
| 37 |
+
|
| 38 |
+
```
|
| 39 |
+
Incoming Logs + Service State
|
| 40 |
+
β
|
| 41 |
+
[PLANNER AGENT]
|
| 42 |
+
Reads logs, decides strategy
|
| 43 |
+
β
|
| 44 |
+
[EXECUTOR AGENT]
|
| 45 |
+
Takes triage actions step-by-step
|
| 46 |
+
(classify_severity β identify_root_cause β remediate β resolve)
|
| 47 |
+
β
|
| 48 |
+
[SUMMARIZER AGENT]
|
| 49 |
+
Produces structured incident report
|
| 50 |
+
β
|
| 51 |
+
Episode Score (0.0 β 1.0)
|
| 52 |
+
```
|
| 53 |
+
|
| 54 |
+
All agents run on **AMD Developer Cloud hosted LLMs** (Mistral/Llama) via their OpenAI-compatible API endpoint.
|
| 55 |
+
|
| 56 |
+
---
|
| 57 |
+
|
| 58 |
+
## ποΈ Architecture
|
| 59 |
+
|
| 60 |
+
```
|
| 61 |
+
βββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 62 |
+
β AgentTriage System β
|
| 63 |
+
β β
|
| 64 |
+
β ββββββββββββββββ ββββββββββββββββββββββββ β
|
| 65 |
+
β β LangGraph ββββββΆβ AMD Developer β β
|
| 66 |
+
β β Agent Loop β β Cloud LLM API β β
|
| 67 |
+
β β βββββββ (Mistral/Llama) β β
|
| 68 |
+
β ββββββββ¬ββββββββ ββββββββββββββββββββββββ β
|
| 69 |
+
β β β
|
| 70 |
+
β ββββββββΌββββββββ β
|
| 71 |
+
β β LogTriage β β
|
| 72 |
+
β β Environment β β
|
| 73 |
+
β β (FastAPI) β β
|
| 74 |
+
β ββββββββ¬ββββββββ β
|
| 75 |
+
β β β
|
| 76 |
+
β ββββββββΌββββββββββββββββββββββββββββββββββββ β
|
| 77 |
+
β β Scenario Engine β β
|
| 78 |
+
β β single_crash | cascading | silent_degradeβ β
|
| 79 |
+
β ββββββββ¬ββββββββββββββββββββββββββββββββββββ β
|
| 80 |
+
β β β
|
| 81 |
+
β ββββββββΌββββββββ β
|
| 82 |
+
β β Grader β β Episode Score (0.0β1.0) β
|
| 83 |
+
β ββββββββββββββββ β
|
| 84 |
+
βββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 85 |
+
```
|
| 86 |
+
|
| 87 |
+
---
|
| 88 |
+
|
| 89 |
+
## π οΈ Tech Stack
|
| 90 |
+
|
| 91 |
+
| Layer | Technology |
|
| 92 |
+
|---|---|
|
| 93 |
+
| Agent Framework | LangGraph |
|
| 94 |
+
| LLM Backend | AMD Developer Cloud (Mistral-7B / Llama-3) |
|
| 95 |
+
| Environment API | FastAPI + Uvicorn |
|
| 96 |
+
| Data Validation | Pydantic v2 |
|
| 97 |
+
| Containerization | Docker |
|
| 98 |
+
| Environment Interface | OpenEnv-compatible (reset/step) |
|
| 99 |
+
| Language | Python 3.11 |
|
| 100 |
+
|
| 101 |
+
---
|
| 102 |
+
|
| 103 |
+
## π Project Structure
|
| 104 |
+
|
| 105 |
+
```
|
| 106 |
+
agentic-triage-amd/
|
| 107 |
+
β
|
| 108 |
+
βββ server/ # LogTriageEnv (environment core)
|
| 109 |
+
β βββ app.py # FastAPI endpoints (/reset, /step, /state, /tasks)
|
| 110 |
+
β βββ environment.py # Core simulator (reset/step/state)
|
| 111 |
+
β βββ models.py # Pydantic schemas (LogLine, TriageAction, etc.)
|
| 112 |
+
β βββ log_generator.py # Log + service state generation
|
| 113 |
+
β βββ scenarios/
|
| 114 |
+
β β βββ single_crash.py # Task 1: Payment service crash
|
| 115 |
+
β β βββ cascading.py # Task 2: user-db β auth β gateway cascade
|
| 116 |
+
β β βββ silent_degrade.py # Task 3: Gradual latency degradation
|
| 117 |
+
β βββ graders/
|
| 118 |
+
β βββ base_grader.py # Abstract grader interface
|
| 119 |
+
β βββ crash_grader.py # Task 1 scoring
|
| 120 |
+
β βββ cascade_grader.py # Task 2 scoring
|
| 121 |
+
β
|
| 122 |
+
βββ agents/ # Multi-agent pipeline (NEW)
|
| 123 |
+
β βββ planner.py # Planner agent β reads logs, sets strategy
|
| 124 |
+
β βββ executor.py # Executor agent β takes step-by-step actions
|
| 125 |
+
β βββ summarizer.py # Summarizer agent β generates incident report
|
| 126 |
+
β βββ pipeline.py # LangGraph graph definition
|
| 127 |
+
β
|
| 128 |
+
βββ amd_client.py # AMD Developer Cloud LLM client
|
| 129 |
+
βββ run_agent.py # Entry point β runs agent on all 3 tasks
|
| 130 |
+
βββ Dockerfile # Container definition
|
| 131 |
+
βββ requirements.txt # Python dependencies
|
| 132 |
+
βββ .env.example # Environment variable template
|
| 133 |
+
βββ README.md # This file
|
| 134 |
+
```
|
| 135 |
+
|
| 136 |
+
---
|
| 137 |
+
|
| 138 |
+
## βοΈ Setup & Installation
|
| 139 |
+
|
| 140 |
+
### Prerequisites
|
| 141 |
+
- Python 3.11+
|
| 142 |
+
- Docker
|
| 143 |
+
- AMD Developer Cloud account + API key
|
| 144 |
+
|
| 145 |
+
### 1. Clone the repo
|
| 146 |
+
```bash
|
| 147 |
+
git clone https://github.com/YOUR_USERNAME/agentic-triage-amd.git
|
| 148 |
+
cd agentic-triage-amd
|
| 149 |
+
```
|
| 150 |
+
|
| 151 |
+
### 2. Set environment variables
|
| 152 |
+
```bash
|
| 153 |
+
cp .env.example .env
|
| 154 |
+
# Edit .env β add your AMD_API_KEY
|
| 155 |
+
```
|
| 156 |
+
|
| 157 |
+
### 3. Run with Docker
|
| 158 |
+
```bash
|
| 159 |
+
docker build -t agentic-triage-amd .
|
| 160 |
+
docker run -p 7860:7860 --env-file .env agentic-triage-amd
|
| 161 |
+
```
|
| 162 |
+
|
| 163 |
+
### 4. Run locally
|
| 164 |
+
```bash
|
| 165 |
+
pip install -r requirements.txt
|
| 166 |
+
# Terminal 1 β start the environment
|
| 167 |
+
uvicorn server.app:app --host 0.0.0.0 --port 7860
|
| 168 |
+
# Terminal 2 β run the agent
|
| 169 |
+
python run_agent.py
|
| 170 |
+
```
|
| 171 |
+
|
| 172 |
+
---
|
| 173 |
+
|
| 174 |
+
## π§ͺ Scoring System
|
| 175 |
+
|
| 176 |
+
Each task is scored 0.0 to 1.0:
|
| 177 |
+
|
| 178 |
+
| Action | Points |
|
| 179 |
+
|---|---|
|
| 180 |
+
| Correct severity classification | +0.30 |
|
| 181 |
+
| Correct root cause identification | +0.35 |
|
| 182 |
+
| Correct remediation command | +0.25 |
|
| 183 |
+
| Speed bonus (within step threshold) | +0.10 |
|
| 184 |
+
| Wrong escalation | -0.10 |
|
| 185 |
+
| Ignoring a P1 incident | -0.50 |
|
| 186 |
+
|
| 187 |
+
**Task 1:** severity=P1, root_cause=payment-service, remediation=restart:payment-service
|
| 188 |
+
**Task 2:** severity=P1, root_cause=user-db, remediation=kill-query:user-db
|
| 189 |
+
**Task 3:** severity=P2, root_cause=payment-db, remediation=flush-cache:payment-db
|
| 190 |
+
|
| 191 |
+
---
|
| 192 |
+
|
| 193 |
+
## π Results
|
| 194 |
+
|
| 195 |
+
| Task | Score |
|
| 196 |
+
|---|---|
|
| 197 |
+
| single_crash | β |
|
| 198 |
+
| cascading_failure | β |
|
| 199 |
+
| silent_degradation | β |
|
| 200 |
+
| **Average** | **β** |
|
| 201 |
+
|
| 202 |
+
*(Updated after final runs)*
|
| 203 |
+
|
| 204 |
+
---
|
| 205 |
+
|
| 206 |
+
## π Environment Variables
|
| 207 |
+
|
| 208 |
+
```env
|
| 209 |
+
AMD_API_KEY=your_amd_developer_cloud_api_key
|
| 210 |
+
AMD_BASE_URL=https://api.amd.com/v1
|
| 211 |
+
AMD_MODEL=mistral-7b-instruct
|
| 212 |
+
ENV_HOST=0.0.0.0
|
| 213 |
+
ENV_PORT=7860
|
| 214 |
+
```
|
| 215 |
+
|
| 216 |
+
---
|
| 217 |
+
|
| 218 |
+
## β
Hackathon Checklist
|
| 219 |
+
|
| 220 |
+
- [ ] AMD Developer Cloud account activated
|
| 221 |
+
- [ ] LLM swap: Groq β AMD hosted model working
|
| 222 |
+
- [ ] Planner agent implemented
|
| 223 |
+
- [ ] Executor agent implemented
|
| 224 |
+
- [ ] Summarizer agent implemented
|
| 225 |
+
- [ ] LangGraph pipeline connecting all three agents
|
| 226 |
+
- [ ] All 3 tasks running end-to-end
|
| 227 |
+
- [ ] Scores recorded and documented
|
| 228 |
+
- [ ] Demo video recorded
|
| 229 |
+
- [ ] Devpost submission written
|
| 230 |
+
- [ ] Repo open-sourced and clean
|
| 231 |
+
|
| 232 |
+
---
|
| 233 |
+
|
| 234 |
+
## π Team
|
| 235 |
+
|
| 236 |
+
| Name | Role |
|
| 237 |
+
|---|---|
|
| 238 |
+
| Rohit Patil (Sonic) | Environment + Agent Pipeline |
|
| 239 |
+
| [Teammate] | [Role TBD] |
|
| 240 |
+
|
| 241 |
+
---
|
| 242 |
+
|
| 243 |
+
## π License
|
| 244 |
+
|
| 245 |
+
MIT License β open source, built for AMD Developer Cloud Hackathon.
|
| 246 |
+
|
| 247 |
+
---
|
| 248 |
+
|
| 249 |
+
> **Note:** This project builds on prior research work in agentic log triage environments, redesigned and upgraded with a multi-agent architecture specifically for AMD Developer Cloud infrastructure.
|
agents/__init__.py
ADDED
|
File without changes
|
amd_client.py
ADDED
|
File without changes
|
requirements.txt
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Environment
|
| 2 |
+
fastapi>=0.104.0
|
| 3 |
+
uvicorn>=0.24.0
|
| 4 |
+
pydantic>=2.0.0
|
| 5 |
+
openenv-core==0.2.3
|
| 6 |
+
|
| 7 |
+
# HTTP
|
| 8 |
+
requests>=2.25.0
|
| 9 |
+
httpx>=0.24.0
|
| 10 |
+
|
| 11 |
+
# LLM + Agents
|
| 12 |
+
openai>=1.0.0
|
| 13 |
+
langgraph>=0.1.0
|
| 14 |
+
langchain>=0.2.0
|
| 15 |
+
langchain-openai>=0.1.0
|
| 16 |
+
|
| 17 |
+
# Utilities
|
| 18 |
+
python-dotenv>=1.0.0
|
run_agent.py
ADDED
|
File without changes
|
server/__init__.py
ADDED
|
File without changes
|
server/app.py
ADDED
|
@@ -0,0 +1,170 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from fastapi import FastAPI, Query
|
| 2 |
+
from fastapi.responses import JSONResponse
|
| 3 |
+
import uvicorn
|
| 4 |
+
import os
|
| 5 |
+
|
| 6 |
+
from server.models import TriageAction
|
| 7 |
+
from server.environment import LogTriageEnvironment
|
| 8 |
+
|
| 9 |
+
app = FastAPI(
|
| 10 |
+
title="LogTriageEnv",
|
| 11 |
+
description="OpenEnv environment for SRE incident triage",
|
| 12 |
+
version="1.0.0",
|
| 13 |
+
)
|
| 14 |
+
|
| 15 |
+
# One environment instance per server process
|
| 16 |
+
env = LogTriageEnvironment()
|
| 17 |
+
|
| 18 |
+
|
| 19 |
+
@app.get("/health")
|
| 20 |
+
def health():
|
| 21 |
+
return {"status": "ok", "environment": "logtriage-env", "version": "1.0.0"}
|
| 22 |
+
|
| 23 |
+
|
| 24 |
+
@app.post("/reset")
|
| 25 |
+
def reset(
|
| 26 |
+
task: str = Query(default="single_crash", description="Task ID to run"),
|
| 27 |
+
seed: int = Query(default=None, description="Random seed for reproducibility"),
|
| 28 |
+
):
|
| 29 |
+
try:
|
| 30 |
+
obs = env.reset(task_id=task, seed=seed)
|
| 31 |
+
return obs.model_dump()
|
| 32 |
+
except ValueError as e:
|
| 33 |
+
return JSONResponse(status_code=400, content={"error": str(e)})
|
| 34 |
+
|
| 35 |
+
|
| 36 |
+
@app.post("/step")
|
| 37 |
+
def step(action: TriageAction):
|
| 38 |
+
valid, err = action.is_valid()
|
| 39 |
+
if not valid:
|
| 40 |
+
return JSONResponse(status_code=422, content={"error": err})
|
| 41 |
+
try:
|
| 42 |
+
obs = env.step(action)
|
| 43 |
+
return obs.model_dump()
|
| 44 |
+
except RuntimeError as e:
|
| 45 |
+
return JSONResponse(status_code=400, content={"error": str(e)})
|
| 46 |
+
|
| 47 |
+
|
| 48 |
+
@app.get("/state")
|
| 49 |
+
def state():
|
| 50 |
+
try:
|
| 51 |
+
return env.state.model_dump()
|
| 52 |
+
except RuntimeError as e:
|
| 53 |
+
return JSONResponse(status_code=400, content={"error": str(e)})
|
| 54 |
+
|
| 55 |
+
|
| 56 |
+
@app.get("/tasks")
|
| 57 |
+
def get_tasks():
|
| 58 |
+
return {
|
| 59 |
+
"tasks": [
|
| 60 |
+
{
|
| 61 |
+
"id": "single_crash",
|
| 62 |
+
"name": "Single Service Crash",
|
| 63 |
+
"difficulty": "easy",
|
| 64 |
+
"max_steps": 8,
|
| 65 |
+
"description": "One service crashes. Classify severity, find root cause, remediate.",
|
| 66 |
+
"action_schema": {
|
| 67 |
+
"action_type": "classify_severity | identify_root_cause | escalate | remediate | request_more_logs | resolve | ignore",
|
| 68 |
+
"value": "string (depends on action_type β see README)",
|
| 69 |
+
"confidence": "float [0.0, 1.0]",
|
| 70 |
+
"reasoning": "string (optional)",
|
| 71 |
+
},
|
| 72 |
+
},
|
| 73 |
+
{
|
| 74 |
+
"id": "cascading_failure",
|
| 75 |
+
"name": "Cascading Failure",
|
| 76 |
+
"difficulty": "medium",
|
| 77 |
+
"max_steps": 12,
|
| 78 |
+
"description": "DB slowdown cascades upstream. Find the true root cause, not symptoms.",
|
| 79 |
+
"action_schema": {
|
| 80 |
+
"action_type": "classify_severity | identify_root_cause | escalate | remediate | request_more_logs | resolve | ignore",
|
| 81 |
+
"value": "string (depends on action_type β see README)",
|
| 82 |
+
"confidence": "float [0.0, 1.0]",
|
| 83 |
+
"reasoning": "string (optional)",
|
| 84 |
+
},
|
| 85 |
+
},
|
| 86 |
+
{
|
| 87 |
+
"id": "silent_degradation",
|
| 88 |
+
"name": "Silent Degradation with Noise",
|
| 89 |
+
"difficulty": "hard",
|
| 90 |
+
"max_steps": 15,
|
| 91 |
+
"description": "Slow degradation hidden in 60% noise. Nuanced P2 severity judgment.",
|
| 92 |
+
"action_schema": {
|
| 93 |
+
"action_type": "classify_severity | identify_root_cause | escalate | remediate | request_more_logs | resolve | ignore",
|
| 94 |
+
"value": "string (depends on action_type β see README)",
|
| 95 |
+
"confidence": "float [0.0, 1.0]",
|
| 96 |
+
"reasoning": "string (optional)",
|
| 97 |
+
},
|
| 98 |
+
},
|
| 99 |
+
]
|
| 100 |
+
}
|
| 101 |
+
|
| 102 |
+
|
| 103 |
+
@app.post("/grader")
|
| 104 |
+
def grader():
|
| 105 |
+
try:
|
| 106 |
+
from server.graders import score_episode
|
| 107 |
+
state = env.state
|
| 108 |
+
result = score_episode(state.task_id, state)
|
| 109 |
+
return result
|
| 110 |
+
except RuntimeError as e:
|
| 111 |
+
return JSONResponse(status_code=400, content={"error": str(e)})
|
| 112 |
+
except ValueError as e:
|
| 113 |
+
return JSONResponse(status_code=400, content={"error": str(e)})
|
| 114 |
+
|
| 115 |
+
|
| 116 |
+
@app.post("/baseline")
|
| 117 |
+
def baseline():
|
| 118 |
+
"""
|
| 119 |
+
Run the baseline inference script against all 3 tasks.
|
| 120 |
+
Returns scores for each task produced by the LLM agent.
|
| 121 |
+
Note: Requires HF_TOKEN (or GROQ_API_KEY) to be set.
|
| 122 |
+
"""
|
| 123 |
+
import subprocess
|
| 124 |
+
import sys
|
| 125 |
+
import json as json_lib
|
| 126 |
+
|
| 127 |
+
try:
|
| 128 |
+
result = subprocess.run(
|
| 129 |
+
[sys.executable, "inference.py"],
|
| 130 |
+
capture_output=True,
|
| 131 |
+
text=True,
|
| 132 |
+
timeout=1200, # 20 minute timeout (matches spec)
|
| 133 |
+
cwd=os.path.dirname(os.path.dirname(os.path.abspath(__file__))),
|
| 134 |
+
)
|
| 135 |
+
|
| 136 |
+
if result.returncode != 0:
|
| 137 |
+
return JSONResponse(
|
| 138 |
+
status_code=500,
|
| 139 |
+
content={
|
| 140 |
+
"error": "Inference script failed",
|
| 141 |
+
"stderr": result.stderr[-500:] if result.stderr else "",
|
| 142 |
+
}
|
| 143 |
+
)
|
| 144 |
+
|
| 145 |
+
# Extract JSON from output
|
| 146 |
+
output_lines = result.stdout.strip().split("\n")
|
| 147 |
+
json_start = None
|
| 148 |
+
for i, line in enumerate(output_lines):
|
| 149 |
+
if line.strip() == "JSON Output:":
|
| 150 |
+
json_start = i + 1
|
| 151 |
+
break
|
| 152 |
+
|
| 153 |
+
if json_start and json_start < len(output_lines):
|
| 154 |
+
json_str = "\n".join(output_lines[json_start:])
|
| 155 |
+
return json_lib.loads(json_str)
|
| 156 |
+
else:
|
| 157 |
+
return {"message": "Baseline completed", "output": result.stdout[-1000:]}
|
| 158 |
+
|
| 159 |
+
except subprocess.TimeoutExpired:
|
| 160 |
+
return JSONResponse(status_code=504, content={"error": "Inference timed out (20min limit)"})
|
| 161 |
+
except Exception as e:
|
| 162 |
+
return JSONResponse(status_code=500, content={"error": str(e)})
|
| 163 |
+
|
| 164 |
+
|
| 165 |
+
def main():
|
| 166 |
+
uvicorn.run("server.app:app", host="0.0.0.0", port=7860, reload=False)
|
| 167 |
+
|
| 168 |
+
|
| 169 |
+
if __name__ == "__main__":
|
| 170 |
+
main()
|
server/environment.py
ADDED
|
@@ -0,0 +1,341 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Core LogTriageEnvironment class.
|
| 3 |
+
Implements OpenEnv interface: reset(), step(), state property.
|
| 4 |
+
"""
|
| 5 |
+
from __future__ import annotations
|
| 6 |
+
import random
|
| 7 |
+
from datetime import datetime
|
| 8 |
+
from uuid import uuid4
|
| 9 |
+
|
| 10 |
+
from server.models import (
|
| 11 |
+
TriageAction,
|
| 12 |
+
TriageObservation,
|
| 13 |
+
EpisodeState,
|
| 14 |
+
LogLine,
|
| 15 |
+
ServiceStatus,
|
| 16 |
+
)
|
| 17 |
+
from server.scenarios import single_crash
|
| 18 |
+
from server.scenarios import cascading
|
| 19 |
+
from server.scenarios import silent_degrade
|
| 20 |
+
from server.log_generator import generate_healthy_system_state, _make_timestamp
|
| 21 |
+
|
| 22 |
+
# βββ TASK REGISTRY βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 23 |
+
|
| 24 |
+
TASK_MAX_STEPS = {
|
| 25 |
+
"single_crash": 8,
|
| 26 |
+
"cascading_failure": 12,
|
| 27 |
+
"silent_degradation": 15,
|
| 28 |
+
}
|
| 29 |
+
|
| 30 |
+
# βββ REWARD CONSTANTS ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 31 |
+
|
| 32 |
+
R_CORRECT_SEVERITY = 0.30
|
| 33 |
+
R_CORRECT_ROOT_CAUSE = 0.35
|
| 34 |
+
R_CORRECT_REMEDIATION = 0.25
|
| 35 |
+
R_CORRECT_ESCALATION = 0.10
|
| 36 |
+
R_SPEED_BONUS = 0.10
|
| 37 |
+
R_PARTIAL_SERVICE_FAM = 0.10
|
| 38 |
+
R_PARTIAL_SEVERITY_ADJ = 0.10
|
| 39 |
+
|
| 40 |
+
P_WRONG_ESCALATION = -0.10
|
| 41 |
+
P_IGNORE_P1 = -0.50
|
| 42 |
+
P_REDUNDANT_ACTION = -0.05
|
| 43 |
+
P_EXCEEDED_BUDGET = -0.20
|
| 44 |
+
P_OVERESCALATE_P3_P1 = -0.15
|
| 45 |
+
|
| 46 |
+
|
| 47 |
+
class LogTriageEnvironment:
|
| 48 |
+
"""
|
| 49 |
+
OpenEnv-compatible environment for SRE incident triage.
|
| 50 |
+
|
| 51 |
+
Usage:
|
| 52 |
+
env = LogTriageEnvironment()
|
| 53 |
+
obs = env.reset(task_id="single_crash", seed=42)
|
| 54 |
+
while not obs.done:
|
| 55 |
+
action = agent.act(obs)
|
| 56 |
+
obs = env.step(action)
|
| 57 |
+
score = env.get_grader_score()
|
| 58 |
+
"""
|
| 59 |
+
|
| 60 |
+
def __init__(self):
|
| 61 |
+
self._state: EpisodeState | None = None
|
| 62 |
+
self._rng: random.Random = random.Random()
|
| 63 |
+
self._base_time: datetime = datetime.utcnow()
|
| 64 |
+
self._task_id: str = "single_crash"
|
| 65 |
+
self._ground_truth: dict = {}
|
| 66 |
+
self._current_obs: TriageObservation | None = None
|
| 67 |
+
|
| 68 |
+
# βββ OPENENV INTERFACE βββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 69 |
+
|
| 70 |
+
def reset(self, task_id: str = "single_crash", seed: int | None = None) -> TriageObservation:
|
| 71 |
+
"""Start a fresh episode. Returns initial observation."""
|
| 72 |
+
if task_id not in TASK_MAX_STEPS:
|
| 73 |
+
raise ValueError(f"Unknown task_id '{task_id}'. Valid: {list(TASK_MAX_STEPS.keys())}")
|
| 74 |
+
|
| 75 |
+
self._task_id = task_id
|
| 76 |
+
self._rng = random.Random(seed)
|
| 77 |
+
self._base_time = datetime.utcnow()
|
| 78 |
+
|
| 79 |
+
# Load ground truth for this task
|
| 80 |
+
if task_id == "single_crash":
|
| 81 |
+
self._ground_truth = single_crash.GROUND_TRUTH
|
| 82 |
+
elif task_id == "cascading_failure":
|
| 83 |
+
self._ground_truth = cascading.GROUND_TRUTH
|
| 84 |
+
elif task_id == "silent_degradation":
|
| 85 |
+
self._ground_truth = silent_degrade.GROUND_TRUTH
|
| 86 |
+
|
| 87 |
+
# Initialize episode state
|
| 88 |
+
self._state = EpisodeState(
|
| 89 |
+
episode_id=str(uuid4()),
|
| 90 |
+
task_id=task_id,
|
| 91 |
+
step_count=0,
|
| 92 |
+
max_steps=TASK_MAX_STEPS[task_id],
|
| 93 |
+
done=False,
|
| 94 |
+
cumulative_score=0.0,
|
| 95 |
+
actions_taken=[],
|
| 96 |
+
correct_severity=None,
|
| 97 |
+
correct_root_cause=None,
|
| 98 |
+
correct_remediation=False,
|
| 99 |
+
)
|
| 100 |
+
|
| 101 |
+
# Get initial observation (step 0)
|
| 102 |
+
logs, system_state = self._get_step_data(0)
|
| 103 |
+
alerts = self._get_alerts(0)
|
| 104 |
+
|
| 105 |
+
obs = TriageObservation(
|
| 106 |
+
logs=logs,
|
| 107 |
+
system_state=system_state,
|
| 108 |
+
incident_id=self._state.episode_id,
|
| 109 |
+
task_id=task_id,
|
| 110 |
+
step_count=0,
|
| 111 |
+
time_elapsed_seconds=0,
|
| 112 |
+
active_alerts=alerts,
|
| 113 |
+
reward=0.0,
|
| 114 |
+
cumulative_score=0.0,
|
| 115 |
+
done=False,
|
| 116 |
+
last_action_feedback="Incident detected. Analyze the logs and take action.",
|
| 117 |
+
invalid_action_error=None,
|
| 118 |
+
)
|
| 119 |
+
self._current_obs = obs
|
| 120 |
+
return obs
|
| 121 |
+
|
| 122 |
+
def step(self, action: TriageAction) -> TriageObservation:
|
| 123 |
+
"""Take one action. Returns next observation + reward."""
|
| 124 |
+
if self._state is None:
|
| 125 |
+
raise RuntimeError("Call reset() before step()")
|
| 126 |
+
if self._state.done:
|
| 127 |
+
raise RuntimeError("Episode is done. Call reset() to start a new episode.")
|
| 128 |
+
|
| 129 |
+
# Validate action
|
| 130 |
+
valid, err = action.is_valid()
|
| 131 |
+
if not valid:
|
| 132 |
+
return self._make_obs(
|
| 133 |
+
reward=0.0,
|
| 134 |
+
feedback=f"Invalid action: {err}",
|
| 135 |
+
invalid_action_error=err,
|
| 136 |
+
advance_step=False,
|
| 137 |
+
)
|
| 138 |
+
|
| 139 |
+
# Calculate reward for this action
|
| 140 |
+
reward, feedback = self._evaluate_action(action)
|
| 141 |
+
|
| 142 |
+
# Update state
|
| 143 |
+
self._state.cumulative_score = round(
|
| 144 |
+
self._state.cumulative_score + reward, 4
|
| 145 |
+
)
|
| 146 |
+
self._state.actions_taken.append(action.action_type)
|
| 147 |
+
self._state.action_history.append(action.model_dump())
|
| 148 |
+
self._state.step_count += 1
|
| 149 |
+
|
| 150 |
+
# Check if episode should end
|
| 151 |
+
done = self._check_done(action)
|
| 152 |
+
self._state.done = done
|
| 153 |
+
|
| 154 |
+
# If done due to budget exceeded, apply penalty
|
| 155 |
+
if self._state.step_count >= self._state.max_steps and not done:
|
| 156 |
+
self._state.cumulative_score = round(
|
| 157 |
+
self._state.cumulative_score + P_EXCEEDED_BUDGET, 4
|
| 158 |
+
)
|
| 159 |
+
self._state.done = True
|
| 160 |
+
feedback += f" Step budget exceeded ({self._state.max_steps} steps). Penalty applied."
|
| 161 |
+
|
| 162 |
+
return self._make_obs(reward=reward, feedback=feedback, advance_step=True)
|
| 163 |
+
|
| 164 |
+
@property
|
| 165 |
+
def state(self) -> EpisodeState:
|
| 166 |
+
"""Return current episode state."""
|
| 167 |
+
if self._state is None:
|
| 168 |
+
raise RuntimeError("Call reset() first.")
|
| 169 |
+
return self._state
|
| 170 |
+
|
| 171 |
+
def get_grader_score(self) -> float:
|
| 172 |
+
"""
|
| 173 |
+
Return final grader score for the completed episode.
|
| 174 |
+
Score is normalized to [0.0, 1.0].
|
| 175 |
+
"""
|
| 176 |
+
if self._state is None:
|
| 177 |
+
return 0.0
|
| 178 |
+
# Clamp score to [0.0, 1.0]
|
| 179 |
+
raw = self._state.cumulative_score
|
| 180 |
+
return round(max(0.0, min(1.0, raw)), 4)
|
| 181 |
+
|
| 182 |
+
# βββ INTERNAL HELPERS ββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 183 |
+
|
| 184 |
+
def _evaluate_action(self, action: TriageAction) -> tuple[float, str]:
|
| 185 |
+
"""
|
| 186 |
+
Evaluate the action against ground truth.
|
| 187 |
+
Returns (reward: float, feedback: str).
|
| 188 |
+
"""
|
| 189 |
+
gt = self._ground_truth
|
| 190 |
+
reward = 0.0
|
| 191 |
+
feedback_parts = []
|
| 192 |
+
|
| 193 |
+
# Penalize redundant actions
|
| 194 |
+
if action.action_type in self._state.actions_taken:
|
| 195 |
+
reward += P_REDUNDANT_ACTION
|
| 196 |
+
feedback_parts.append("Redundant action β you've already done this.")
|
| 197 |
+
|
| 198 |
+
# ββ classify_severity ββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 199 |
+
if action.action_type == "classify_severity":
|
| 200 |
+
correct_sev = gt.get("severity", "")
|
| 201 |
+
if action.value == correct_sev:
|
| 202 |
+
if self._state.correct_severity is None: # only reward first time
|
| 203 |
+
reward += R_CORRECT_SEVERITY
|
| 204 |
+
feedback_parts.append(f"Correct severity: {action.value}. +{R_CORRECT_SEVERITY}")
|
| 205 |
+
self._state.correct_severity = action.value
|
| 206 |
+
else:
|
| 207 |
+
# Partial credit: P1 vs P2 is close, P1 vs P3 is not
|
| 208 |
+
if correct_sev == "P1" and action.value == "P3":
|
| 209 |
+
reward += P_OVERESCALATE_P3_P1 # wrong direction
|
| 210 |
+
feedback_parts.append(f"Incorrect severity: {action.value}. P1 expected. This is a customer-impacting incident.")
|
| 211 |
+
elif correct_sev == "P1" and action.value == "P2":
|
| 212 |
+
reward += R_PARTIAL_SEVERITY_ADJ
|
| 213 |
+
feedback_parts.append(f"Close β {action.value} given, P1 expected. Partial credit.")
|
| 214 |
+
else:
|
| 215 |
+
feedback_parts.append(f"Incorrect severity: {action.value}. Reassess.")
|
| 216 |
+
|
| 217 |
+
# ββ identify_root_cause ββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 218 |
+
elif action.action_type == "identify_root_cause":
|
| 219 |
+
correct_rc = gt.get("root_cause", "")
|
| 220 |
+
if action.value == correct_rc:
|
| 221 |
+
if self._state.correct_root_cause is None:
|
| 222 |
+
reward += R_CORRECT_ROOT_CAUSE
|
| 223 |
+
feedback_parts.append(f"Correct root cause: {action.value}. +{R_CORRECT_ROOT_CAUSE}")
|
| 224 |
+
self._state.correct_root_cause = action.value
|
| 225 |
+
else:
|
| 226 |
+
# Partial credit: same tier (e.g. payment-db instead of payment-service)
|
| 227 |
+
if correct_rc.split("-")[0] == action.value.split("-")[0]:
|
| 228 |
+
reward += R_PARTIAL_SERVICE_FAM
|
| 229 |
+
feedback_parts.append(f"Close β {action.value} is in the right service family. Check more carefully.")
|
| 230 |
+
else:
|
| 231 |
+
feedback_parts.append(f"Incorrect root cause: {action.value}. Look at which service is actually failing.")
|
| 232 |
+
|
| 233 |
+
# ββ escalate ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 234 |
+
elif action.action_type == "escalate":
|
| 235 |
+
correct_teams = gt.get("correct_teams", set())
|
| 236 |
+
if action.value in correct_teams:
|
| 237 |
+
reward += R_CORRECT_ESCALATION
|
| 238 |
+
feedback_parts.append(f"Correct escalation to {action.value}. +{R_CORRECT_ESCALATION}")
|
| 239 |
+
else:
|
| 240 |
+
reward += P_WRONG_ESCALATION
|
| 241 |
+
feedback_parts.append(f"Wrong team escalated: {action.value}. Penalty applied.")
|
| 242 |
+
|
| 243 |
+
# ββ remediate βββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 244 |
+
elif action.action_type == "remediate":
|
| 245 |
+
prefix = action.value.split(":")[0]
|
| 246 |
+
service = action.value.split(":")[1] if ":" in action.value else ""
|
| 247 |
+
correct_prefixes = gt.get("remediation_prefixes", set())
|
| 248 |
+
correct_service = gt.get("remediation_service", "")
|
| 249 |
+
|
| 250 |
+
if prefix in correct_prefixes and service == correct_service:
|
| 251 |
+
if not self._state.correct_remediation:
|
| 252 |
+
reward += R_CORRECT_REMEDIATION
|
| 253 |
+
feedback_parts.append(f"Correct remediation: {action.value}. +{R_CORRECT_REMEDIATION}")
|
| 254 |
+
self._state.correct_remediation = True
|
| 255 |
+
elif service == correct_service and prefix not in correct_prefixes:
|
| 256 |
+
reward += 0.05 # right service, wrong action
|
| 257 |
+
feedback_parts.append(f"Right service, but '{prefix}' may not fix this. Try another remediation type.")
|
| 258 |
+
else:
|
| 259 |
+
feedback_parts.append(f"Incorrect remediation: {action.value}. Reconsider which service needs fixing.")
|
| 260 |
+
|
| 261 |
+
# ββ ignore ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 262 |
+
elif action.action_type == "ignore":
|
| 263 |
+
correct_sev = gt.get("severity", "")
|
| 264 |
+
if correct_sev == "P1":
|
| 265 |
+
reward += P_IGNORE_P1
|
| 266 |
+
feedback_parts.append(f"CRITICAL ERROR: Ignored a P1 incident! Major penalty applied.")
|
| 267 |
+
else:
|
| 268 |
+
feedback_parts.append("Marked as noise.")
|
| 269 |
+
|
| 270 |
+
# ββ request_more_logs βββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 271 |
+
elif action.action_type == "request_more_logs":
|
| 272 |
+
feedback_parts.append(f"Fetching more logs for {action.value}...")
|
| 273 |
+
|
| 274 |
+
# ββ resolve βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 275 |
+
elif action.action_type == "resolve":
|
| 276 |
+
# Speed bonus if resolved within 60% of step budget
|
| 277 |
+
step_budget = self._state.max_steps
|
| 278 |
+
if self._state.step_count <= int(step_budget * 0.6):
|
| 279 |
+
reward += R_SPEED_BONUS
|
| 280 |
+
feedback_parts.append(f"Incident resolved efficiently. Speed bonus: +{R_SPEED_BONUS}")
|
| 281 |
+
else:
|
| 282 |
+
feedback_parts.append("Incident resolved.")
|
| 283 |
+
|
| 284 |
+
return round(reward, 4), " | ".join(feedback_parts) or "Action processed."
|
| 285 |
+
|
| 286 |
+
def _check_done(self, action: TriageAction) -> bool:
|
| 287 |
+
"""Episode ends on resolve, ignore (with P1), or step budget exhausted."""
|
| 288 |
+
if action.action_type == "resolve":
|
| 289 |
+
return True
|
| 290 |
+
if action.action_type == "ignore" and self._ground_truth.get("severity") == "P1":
|
| 291 |
+
return True # Catastrophic β episode ends immediately
|
| 292 |
+
if self._state.step_count >= self._state.max_steps:
|
| 293 |
+
return True
|
| 294 |
+
return False
|
| 295 |
+
|
| 296 |
+
def _get_step_data(self, step: int):
|
| 297 |
+
"""Get logs and system state for the current step."""
|
| 298 |
+
if self._task_id == "single_crash":
|
| 299 |
+
return single_crash.get_step_data(step, self._base_time, self._rng)
|
| 300 |
+
elif self._task_id == "cascading_failure":
|
| 301 |
+
return cascading.get_step_data(step, self._base_time, self._rng)
|
| 302 |
+
elif self._task_id == "silent_degradation":
|
| 303 |
+
return silent_degrade.get_step_data(step, self._base_time, self._rng)
|
| 304 |
+
return [], generate_healthy_system_state(self._base_time)
|
| 305 |
+
|
| 306 |
+
def _get_alerts(self, step: int) -> list[str]:
|
| 307 |
+
"""Get active alerts for the current step."""
|
| 308 |
+
if self._task_id == "single_crash":
|
| 309 |
+
return single_crash.get_active_alerts(step)
|
| 310 |
+
elif self._task_id == "cascading_failure":
|
| 311 |
+
return cascading.get_active_alerts(step)
|
| 312 |
+
elif self._task_id == "silent_degradation":
|
| 313 |
+
return silent_degrade.get_active_alerts(step)
|
| 314 |
+
return []
|
| 315 |
+
|
| 316 |
+
def _make_obs(
|
| 317 |
+
self,
|
| 318 |
+
reward: float,
|
| 319 |
+
feedback: str,
|
| 320 |
+
invalid_action_error: str | None = None,
|
| 321 |
+
advance_step: bool = True,
|
| 322 |
+
) -> TriageObservation:
|
| 323 |
+
"""Build a TriageObservation for the current state."""
|
| 324 |
+
step = self._state.step_count
|
| 325 |
+
logs, system_state = self._get_step_data(step)
|
| 326 |
+
alerts = self._get_alerts(step)
|
| 327 |
+
|
| 328 |
+
return TriageObservation(
|
| 329 |
+
logs=logs,
|
| 330 |
+
system_state=system_state,
|
| 331 |
+
incident_id=self._state.episode_id,
|
| 332 |
+
task_id=self._state.task_id,
|
| 333 |
+
step_count=step,
|
| 334 |
+
time_elapsed_seconds=step * 30,
|
| 335 |
+
active_alerts=alerts,
|
| 336 |
+
reward=reward,
|
| 337 |
+
cumulative_score=self._state.cumulative_score,
|
| 338 |
+
done=self._state.done,
|
| 339 |
+
last_action_feedback=feedback,
|
| 340 |
+
invalid_action_error=invalid_action_error,
|
| 341 |
+
)
|
server/graders/__init__.py
ADDED
|
File without changes
|
server/graders/base_grader.py
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Abstract base grader interface.
|
| 3 |
+
All task graders must inherit from this and implement score().
|
| 4 |
+
"""
|
| 5 |
+
from __future__ import annotations
|
| 6 |
+
from abc import ABC, abstractmethod
|
| 7 |
+
from server.models import EpisodeState
|
| 8 |
+
|
| 9 |
+
|
| 10 |
+
class BaseGrader(ABC):
|
| 11 |
+
"""
|
| 12 |
+
Abstract grader base class.
|
| 13 |
+
|
| 14 |
+
A grader evaluates the complete episode history and produces
|
| 15 |
+
a final score in [0.0, 1.0].
|
| 16 |
+
|
| 17 |
+
Unlike the reward function (which fires after every step),
|
| 18 |
+
the grader fires once at episode end and produces the
|
| 19 |
+
official score used by judges.
|
| 20 |
+
"""
|
| 21 |
+
|
| 22 |
+
@abstractmethod
|
| 23 |
+
def score(self, state: EpisodeState) -> float:
|
| 24 |
+
"""
|
| 25 |
+
Score the completed episode.
|
| 26 |
+
|
| 27 |
+
Args:
|
| 28 |
+
state: Final EpisodeState including full action_history
|
| 29 |
+
|
| 30 |
+
Returns:
|
| 31 |
+
float in [0.0, 1.0] β the official episode score
|
| 32 |
+
"""
|
| 33 |
+
raise NotImplementedError
|
| 34 |
+
|
| 35 |
+
def _clamp(self, value: float) -> float:
|
| 36 |
+
"""Clamp score to valid range (0.0, 1.0) β strictly between 0 and 1."""
|
| 37 |
+
return round(max(0.0001, min(0.9999, value)), 4)
|
| 38 |
+
|
| 39 |
+
def _get_actions_of_type(
|
| 40 |
+
self, state: EpisodeState, action_type: str
|
| 41 |
+
) -> list[dict]:
|
| 42 |
+
"""Return all actions of a given type from episode history."""
|
| 43 |
+
return [
|
| 44 |
+
a for a in state.action_history
|
| 45 |
+
if a.get("action_type") == action_type
|
| 46 |
+
]
|
| 47 |
+
|
| 48 |
+
def _was_action_taken(self, state: EpisodeState, action_type: str) -> bool:
|
| 49 |
+
"""Check if an action type was taken at any point in the episode."""
|
| 50 |
+
return any(
|
| 51 |
+
a.get("action_type") == action_type
|
| 52 |
+
for a in state.action_history
|
| 53 |
+
)
|
| 54 |
+
|
| 55 |
+
def _get_first_value(
|
| 56 |
+
self, state: EpisodeState, action_type: str
|
| 57 |
+
) -> str | None:
|
| 58 |
+
"""Get the value of the first action of a given type."""
|
| 59 |
+
actions = self._get_actions_of_type(state, action_type)
|
| 60 |
+
return actions[0].get("value") if actions else None
|
| 61 |
+
|
| 62 |
+
def _episode_resolved(self, state: EpisodeState) -> bool:
|
| 63 |
+
"""Check if agent explicitly resolved the episode."""
|
| 64 |
+
return self._was_action_taken(state, "resolve")
|
| 65 |
+
|
| 66 |
+
def _steps_used(self, state: EpisodeState) -> int:
|
| 67 |
+
"""Return number of steps taken."""
|
| 68 |
+
return state.step_count
|
server/graders/cascade_grader.py
ADDED
|
@@ -0,0 +1,123 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Grader for Task 2 β Cascading Failure (Medium)
|
| 3 |
+
|
| 4 |
+
Scoring breakdown:
|
| 5 |
+
Correct severity (P1) β +0.20
|
| 6 |
+
Correct root cause (user-db) β +0.35
|
| 7 |
+
Correct remediation (kill-query/restart) β +0.25
|
| 8 |
+
Ordering bonus (no symptom fix first) β +0.10
|
| 9 |
+
Speed bonus (resolved β€ 8 steps) β +0.10
|
| 10 |
+
βββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 11 |
+
Maximum possible score β 1.00
|
| 12 |
+
|
| 13 |
+
Penalties:
|
| 14 |
+
Identified symptom as root cause β 0.00 (no credit)
|
| 15 |
+
Remediated symptom service first β -0.10 (ordering penalty)
|
| 16 |
+
Never resolved β -0.10
|
| 17 |
+
"""
|
| 18 |
+
from __future__ import annotations
|
| 19 |
+
from server.models import EpisodeState
|
| 20 |
+
from server.graders.base_grader import BaseGrader
|
| 21 |
+
|
| 22 |
+
|
| 23 |
+
class CascadeGrader(BaseGrader):
|
| 24 |
+
"""Official grader for Task 2 β Cascading Failure."""
|
| 25 |
+
|
| 26 |
+
CORRECT_SEVERITY = "P1"
|
| 27 |
+
CORRECT_ROOT_CAUSE = "user-db"
|
| 28 |
+
CORRECT_REMEDIATION_PREFIXES = {"kill-query", "restart"}
|
| 29 |
+
CORRECT_REMEDIATION_SERVICE = "user-db"
|
| 30 |
+
SYMPTOM_SERVICES = {"api-gateway", "auth-service"} # wrong answers
|
| 31 |
+
MAX_STEPS = 12
|
| 32 |
+
SPEED_THRESHOLD = 8
|
| 33 |
+
|
| 34 |
+
def score(self, state: EpisodeState) -> float:
|
| 35 |
+
"""
|
| 36 |
+
Score the completed Task 2 episode.
|
| 37 |
+
Penalizes agents that treat symptoms instead of root cause.
|
| 38 |
+
"""
|
| 39 |
+
total = 0.0
|
| 40 |
+
breakdown = {}
|
| 41 |
+
|
| 42 |
+
# ββ 1. Severity classification βββββββββββββββββββββββββββββββββββββββββ
|
| 43 |
+
severity_value = self._get_first_value(state, "classify_severity")
|
| 44 |
+
if severity_value == self.CORRECT_SEVERITY:
|
| 45 |
+
total += 0.20
|
| 46 |
+
breakdown["severity"] = "+0.20 (correct: P1)"
|
| 47 |
+
elif severity_value == "P2":
|
| 48 |
+
total += 0.08
|
| 49 |
+
breakdown["severity"] = "+0.08 (partial: P2 given, P1 expected)"
|
| 50 |
+
elif severity_value is None:
|
| 51 |
+
breakdown["severity"] = "+0.00 (never classified)"
|
| 52 |
+
else:
|
| 53 |
+
breakdown["severity"] = f"+0.00 (wrong: {severity_value})"
|
| 54 |
+
|
| 55 |
+
# ββ 2. Root cause identification βββββββββββββββββββββββββββββββββββββββ
|
| 56 |
+
root_cause_value = self._get_first_value(state, "identify_root_cause")
|
| 57 |
+
if root_cause_value == self.CORRECT_ROOT_CAUSE:
|
| 58 |
+
total += 0.35
|
| 59 |
+
breakdown["root_cause"] = "+0.35 (correct: user-db)"
|
| 60 |
+
elif root_cause_value in self.SYMPTOM_SERVICES:
|
| 61 |
+
# Identified a symptom, not root cause β no credit
|
| 62 |
+
breakdown["root_cause"] = f"+0.00 (wrong: {root_cause_value} is a symptom, not root cause)"
|
| 63 |
+
elif root_cause_value and "db" in root_cause_value:
|
| 64 |
+
total += 0.10 # right tier (database), wrong specific service
|
| 65 |
+
breakdown["root_cause"] = f"+0.10 (partial: {root_cause_value}, right tier)"
|
| 66 |
+
elif root_cause_value is None:
|
| 67 |
+
breakdown["root_cause"] = "+0.00 (never identified)"
|
| 68 |
+
else:
|
| 69 |
+
breakdown["root_cause"] = f"+0.00 (wrong: {root_cause_value})"
|
| 70 |
+
|
| 71 |
+
# ββ 3. Remediation + Ordering ββββββββββββββββββββββββββββββββββββββββββ
|
| 72 |
+
remediation_actions = self._get_actions_of_type(state, "remediate")
|
| 73 |
+
remediation_scored = False
|
| 74 |
+
symptom_remediated_first = False
|
| 75 |
+
|
| 76 |
+
for i, action in enumerate(remediation_actions):
|
| 77 |
+
value = action.get("value", "")
|
| 78 |
+
parts = value.split(":")
|
| 79 |
+
if len(parts) != 2:
|
| 80 |
+
continue
|
| 81 |
+
prefix, service = parts
|
| 82 |
+
|
| 83 |
+
# Check if agent remediated a symptom service before root cause
|
| 84 |
+
if service in self.SYMPTOM_SERVICES and not remediation_scored:
|
| 85 |
+
symptom_remediated_first = True
|
| 86 |
+
|
| 87 |
+
# Check for correct remediation
|
| 88 |
+
if (
|
| 89 |
+
prefix in self.CORRECT_REMEDIATION_PREFIXES
|
| 90 |
+
and service == self.CORRECT_REMEDIATION_SERVICE
|
| 91 |
+
and not remediation_scored
|
| 92 |
+
):
|
| 93 |
+
total += 0.25
|
| 94 |
+
breakdown["remediation"] = f"+0.25 (correct: {value})"
|
| 95 |
+
remediation_scored = True
|
| 96 |
+
|
| 97 |
+
if not remediation_scored:
|
| 98 |
+
breakdown["remediation"] = "+0.00 (no correct remediation)"
|
| 99 |
+
|
| 100 |
+
# ββ 4. Ordering bonus ββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 101 |
+
if not symptom_remediated_first and remediation_scored:
|
| 102 |
+
total += 0.10
|
| 103 |
+
breakdown["ordering"] = "+0.10 (correctly targeted root cause, not symptoms)"
|
| 104 |
+
elif symptom_remediated_first:
|
| 105 |
+
total -= 0.10
|
| 106 |
+
breakdown["ordering"] = "-0.10 (remediated symptom service before root cause)"
|
| 107 |
+
|
| 108 |
+
# ββ 5. Speed bonus βββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 109 |
+
if self._episode_resolved(state):
|
| 110 |
+
if self._steps_used(state) <= self.SPEED_THRESHOLD:
|
| 111 |
+
total += 0.10
|
| 112 |
+
breakdown["speed"] = f"+0.10 (resolved in {self._steps_used(state)} steps)"
|
| 113 |
+
else:
|
| 114 |
+
breakdown["speed"] = f"+0.00 (resolved but used {self._steps_used(state)} steps)"
|
| 115 |
+
else:
|
| 116 |
+
total -= 0.10
|
| 117 |
+
breakdown["resolution"] = "-0.10 (never resolved)"
|
| 118 |
+
|
| 119 |
+
self._breakdown = breakdown
|
| 120 |
+
return self._clamp(total)
|
| 121 |
+
|
| 122 |
+
def get_breakdown(self) -> dict:
|
| 123 |
+
return getattr(self, "_breakdown", {})
|
server/graders/crash_grader.py
ADDED
|
@@ -0,0 +1,110 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Grader for Task 1 β Single Service Crash (Easy)
|
| 3 |
+
|
| 4 |
+
Scoring breakdown:
|
| 5 |
+
Correct severity (P1) β +0.30
|
| 6 |
+
Correct root cause (payment-service) β +0.35
|
| 7 |
+
Correct remediation (restart:payment-*) β +0.25
|
| 8 |
+
Speed bonus (resolved β€ 5 steps) β +0.10
|
| 9 |
+
βββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 10 |
+
Maximum possible score β 1.00
|
| 11 |
+
|
| 12 |
+
Penalties:
|
| 13 |
+
Ignored P1 incident β -0.30 (from base)
|
| 14 |
+
Wrong root cause identified β 0.00 (no credit)
|
| 15 |
+
Never resolved β -0.10
|
| 16 |
+
"""
|
| 17 |
+
from __future__ import annotations
|
| 18 |
+
from server.models import EpisodeState
|
| 19 |
+
from server.graders.base_grader import BaseGrader
|
| 20 |
+
|
| 21 |
+
|
| 22 |
+
class CrashGrader(BaseGrader):
|
| 23 |
+
"""Official grader for Task 1 β Single Service Crash."""
|
| 24 |
+
|
| 25 |
+
# Ground truth constants
|
| 26 |
+
CORRECT_SEVERITY = "P1"
|
| 27 |
+
CORRECT_ROOT_CAUSE = "payment-service"
|
| 28 |
+
CORRECT_REMEDIATION_PREFIX = "restart"
|
| 29 |
+
CORRECT_REMEDIATION_SERVICE = "payment-service"
|
| 30 |
+
MAX_STEPS = 8
|
| 31 |
+
SPEED_THRESHOLD = 5 # must resolve within this many steps for speed bonus
|
| 32 |
+
|
| 33 |
+
def score(self, state: EpisodeState) -> float:
|
| 34 |
+
"""
|
| 35 |
+
Score the completed Task 1 episode.
|
| 36 |
+
Deterministic β same action history always produces same score.
|
| 37 |
+
"""
|
| 38 |
+
total = 0.0
|
| 39 |
+
breakdown = {}
|
| 40 |
+
|
| 41 |
+
# ββ 1. Severity classification βββββββββββββββββββββββββββββββββββββββββ
|
| 42 |
+
severity_value = self._get_first_value(state, "classify_severity")
|
| 43 |
+
if severity_value == self.CORRECT_SEVERITY:
|
| 44 |
+
total += 0.30
|
| 45 |
+
breakdown["severity"] = "+0.30 (correct: P1)"
|
| 46 |
+
elif severity_value == "P2":
|
| 47 |
+
total += 0.10 # partial credit β close but not right
|
| 48 |
+
breakdown["severity"] = "+0.10 (partial: P2 given, P1 expected)"
|
| 49 |
+
elif severity_value is None:
|
| 50 |
+
breakdown["severity"] = "+0.00 (never classified)"
|
| 51 |
+
else:
|
| 52 |
+
breakdown["severity"] = f"+0.00 (wrong: {severity_value})"
|
| 53 |
+
|
| 54 |
+
# ββ 2. Root cause identification βββββββββββββββββββββββββββββββββββββββ
|
| 55 |
+
root_cause_value = self._get_first_value(state, "identify_root_cause")
|
| 56 |
+
if root_cause_value == self.CORRECT_ROOT_CAUSE:
|
| 57 |
+
total += 0.35
|
| 58 |
+
breakdown["root_cause"] = "+0.35 (correct: payment-service)"
|
| 59 |
+
elif root_cause_value and root_cause_value.startswith("payment"):
|
| 60 |
+
total += 0.10 # partial β right service family
|
| 61 |
+
breakdown["root_cause"] = f"+0.10 (partial: {root_cause_value}, right family)"
|
| 62 |
+
elif root_cause_value is None:
|
| 63 |
+
breakdown["root_cause"] = "+0.00 (never identified)"
|
| 64 |
+
else:
|
| 65 |
+
breakdown["root_cause"] = f"+0.00 (wrong: {root_cause_value})"
|
| 66 |
+
|
| 67 |
+
# ββ 3. Remediation βββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 68 |
+
remediation_actions = self._get_actions_of_type(state, "remediate")
|
| 69 |
+
remediation_scored = False
|
| 70 |
+
for action in remediation_actions:
|
| 71 |
+
value = action.get("value", "")
|
| 72 |
+
parts = value.split(":")
|
| 73 |
+
if len(parts) == 2:
|
| 74 |
+
prefix, service = parts
|
| 75 |
+
if prefix == self.CORRECT_REMEDIATION_PREFIX and service == self.CORRECT_REMEDIATION_SERVICE:
|
| 76 |
+
total += 0.25
|
| 77 |
+
breakdown["remediation"] = f"+0.25 (correct: {value})"
|
| 78 |
+
remediation_scored = True
|
| 79 |
+
break
|
| 80 |
+
elif service == self.CORRECT_REMEDIATION_SERVICE:
|
| 81 |
+
total += 0.08 # right service, wrong action type
|
| 82 |
+
breakdown["remediation"] = f"+0.08 (partial: right service, wrong action)"
|
| 83 |
+
remediation_scored = True
|
| 84 |
+
break
|
| 85 |
+
|
| 86 |
+
if not remediation_scored:
|
| 87 |
+
breakdown["remediation"] = "+0.00 (no correct remediation)"
|
| 88 |
+
|
| 89 |
+
# ββ 4. Speed bonus βββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 90 |
+
if self._episode_resolved(state):
|
| 91 |
+
if self._steps_used(state) <= self.SPEED_THRESHOLD:
|
| 92 |
+
total += 0.10
|
| 93 |
+
breakdown["speed"] = f"+0.10 (resolved in {self._steps_used(state)} steps)"
|
| 94 |
+
else:
|
| 95 |
+
breakdown["speed"] = f"+0.00 (resolved but slow: {self._steps_used(state)} steps)"
|
| 96 |
+
else:
|
| 97 |
+
total -= 0.10 # penalty for not resolving
|
| 98 |
+
breakdown["resolution"] = "-0.10 (never resolved)"
|
| 99 |
+
|
| 100 |
+
# ββ 5. Ignore penalty βββββββββββββββββββββββββββββοΏ½οΏ½ββββββββββββββββββββ
|
| 101 |
+
if self._was_action_taken(state, "ignore"):
|
| 102 |
+
total -= 0.30
|
| 103 |
+
breakdown["ignore_penalty"] = "-0.30 (ignored P1 incident)"
|
| 104 |
+
|
| 105 |
+
self._breakdown = breakdown
|
| 106 |
+
return self._clamp(total)
|
| 107 |
+
|
| 108 |
+
def get_breakdown(self) -> dict:
|
| 109 |
+
"""Return scoring breakdown from last score() call."""
|
| 110 |
+
return getattr(self, "_breakdown", {})
|
server/log_generator.py
ADDED
|
@@ -0,0 +1,188 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Log generator for LogTriageEnv.
|
| 3 |
+
Produces realistic-looking log lines for the simulated microservice cluster.
|
| 4 |
+
"""
|
| 5 |
+
from __future__ import annotations
|
| 6 |
+
import random
|
| 7 |
+
from datetime import datetime, timedelta
|
| 8 |
+
from server.models import LogLine, ServiceStatus
|
| 9 |
+
|
| 10 |
+
# βββ SERVICES βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 11 |
+
|
| 12 |
+
SERVICES = [
|
| 13 |
+
"api-gateway",
|
| 14 |
+
"auth-service",
|
| 15 |
+
"user-db",
|
| 16 |
+
"payment-service",
|
| 17 |
+
"payment-db",
|
| 18 |
+
"notification-service",
|
| 19 |
+
"email-queue",
|
| 20 |
+
]
|
| 21 |
+
|
| 22 |
+
# βββ LOG TEMPLATES ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 23 |
+
|
| 24 |
+
# Noise logs β realistic but irrelevant to the incident
|
| 25 |
+
NOISE_TEMPLATES = {
|
| 26 |
+
"api-gateway": [
|
| 27 |
+
("INFO", "health check passed β all upstream services reachable"),
|
| 28 |
+
("INFO", "request completed: GET /api/v1/users/profile [200] 45ms"),
|
| 29 |
+
("INFO", "rate limiter: 1240/5000 requests this minute"),
|
| 30 |
+
("DEBUG", "connection pool: 12/100 active connections"),
|
| 31 |
+
("INFO", "TLS certificate valid for 87 more days"),
|
| 32 |
+
],
|
| 33 |
+
"auth-service": [
|
| 34 |
+
("INFO", "JWT token issued for user_id=88142 [expires: 3600s]"),
|
| 35 |
+
("INFO", "OAuth2 flow completed successfully"),
|
| 36 |
+
("DEBUG", "session cache hit ratio: 94.2%"),
|
| 37 |
+
("INFO", "password reset email queued for user_id=23019"),
|
| 38 |
+
],
|
| 39 |
+
"user-db": [
|
| 40 |
+
("INFO", "daily vacuum completed: 0 dead tuples removed"),
|
| 41 |
+
("INFO", "checkpoint complete: wrote 142 buffers"),
|
| 42 |
+
("DEBUG", "autovacuum: processing table 'sessions'"),
|
| 43 |
+
("INFO", "replication lag: 12ms (within threshold)"),
|
| 44 |
+
],
|
| 45 |
+
"payment-service": [
|
| 46 |
+
("INFO", "payment processed: txn_id=TXN-8812 amount=299.00 INR [success]"),
|
| 47 |
+
("INFO", "webhook delivered: stripe event=payment.succeeded"),
|
| 48 |
+
("DEBUG", "idempotency key cache: 2341 keys active"),
|
| 49 |
+
],
|
| 50 |
+
"payment-db": [
|
| 51 |
+
("INFO", "connection pool: 8/50 active"),
|
| 52 |
+
("DEBUG", "query plan cache: 88% hit ratio"),
|
| 53 |
+
("INFO", "index usage: 99.1% queries using indexed scans"),
|
| 54 |
+
],
|
| 55 |
+
"notification-service": [
|
| 56 |
+
("INFO", "email dispatched: template=welcome_email to=user@example.com"),
|
| 57 |
+
("INFO", "SMS delivered: +91XXXXXXXXXX [provider=twilio]"),
|
| 58 |
+
("WARN", "email bounce rate: 1.2% (threshold: 5%)"),
|
| 59 |
+
("INFO", "push notification sent: device_tokens=1240"),
|
| 60 |
+
],
|
| 61 |
+
"email-queue": [
|
| 62 |
+
("INFO", "queue depth: 42 messages pending"),
|
| 63 |
+
("INFO", "consumer lag: 0.3s (healthy)"),
|
| 64 |
+
("DEBUG", "partition rebalance completed in 120ms"),
|
| 65 |
+
],
|
| 66 |
+
}
|
| 67 |
+
|
| 68 |
+
# Signal logs β actual incident indicators
|
| 69 |
+
SIGNAL_TEMPLATES = {
|
| 70 |
+
# Single service crash signals (Task 1 β payment-service crash)
|
| 71 |
+
"single_crash_payment": [
|
| 72 |
+
("ERROR", "NullPointerException: Cannot invoke method processPayment() on null object β PaymentProcessor.java:142"),
|
| 73 |
+
("ERROR", "HTTP 500 Internal Server Error: payment gateway returned null response"),
|
| 74 |
+
("ERROR", "NullPointerException in PaymentService.execute() β retrying (attempt 1/3)"),
|
| 75 |
+
("ERROR", "NullPointerException in PaymentService.execute() β retrying (attempt 2/3)"),
|
| 76 |
+
("FATAL", "NullPointerException in PaymentService.execute() β all retries exhausted, request failed"),
|
| 77 |
+
("ERROR", "health check FAILED: payment-service returned 500 (was 200)"),
|
| 78 |
+
("ERROR", "circuit breaker OPEN: payment-service error rate 98.2% (threshold: 10%)"),
|
| 79 |
+
],
|
| 80 |
+
# Cascading failure signals (Task 2 β user-db β auth-service β api-gateway)
|
| 81 |
+
"cascading_userdb": [
|
| 82 |
+
("WARN", "slow query detected: SELECT * FROM sessions WHERE user_id=? [latency: 2847ms, threshold: 200ms]"),
|
| 83 |
+
("ERROR", "slow query detected: SELECT * FROM sessions WHERE user_id=? [latency: 4120ms]"),
|
| 84 |
+
("ERROR", "query timeout: SELECT * FROM active_sessions [timeout after 5000ms]"),
|
| 85 |
+
],
|
| 86 |
+
"cascading_auth": [
|
| 87 |
+
("WARN", "db connection pool: 42/50 active connections (84% utilization)"),
|
| 88 |
+
("ERROR", "db connection pool exhausted: 50/50 connections in use β requests queuing"),
|
| 89 |
+
("ERROR", "authentication request timed out waiting for db connection [5200ms]"),
|
| 90 |
+
],
|
| 91 |
+
"cascading_gateway": [
|
| 92 |
+
("ERROR", "upstream timeout: auth-service failed to respond within 5000ms [req-id: {req_id}]"),
|
| 93 |
+
("ERROR", "upstream timeout: auth-service [req-id: {req_id}] β returning 504 to client"),
|
| 94 |
+
("WARN", "error rate spike: 34.2% of requests failing (threshold: 5%)"),
|
| 95 |
+
],
|
| 96 |
+
# Silent degradation signals (Task 3 β payment-db slow)
|
| 97 |
+
"silent_paymentdb": [
|
| 98 |
+
("WARN", "query latency elevated: avg=450ms (normal: 80ms) β monitoring"),
|
| 99 |
+
("WARN", "query latency elevated: avg=620ms β possible memory pressure"),
|
| 100 |
+
("WARN", "query latency elevated: avg=890ms β recommend investigation"),
|
| 101 |
+
("WARN", "query latency elevated: avg=1200ms β approaching timeout threshold"),
|
| 102 |
+
("WARN", "buffer cache hit ratio degraded: 87% (normal: 98%) β possible memory issue"),
|
| 103 |
+
],
|
| 104 |
+
}
|
| 105 |
+
|
| 106 |
+
|
| 107 |
+
def _make_timestamp(base_time: datetime, offset_seconds: int = 0) -> str:
|
| 108 |
+
t = base_time + timedelta(seconds=offset_seconds)
|
| 109 |
+
return t.strftime("%Y-%m-%dT%H:%M:%SZ")
|
| 110 |
+
|
| 111 |
+
|
| 112 |
+
def _noise_log(service: str, base_time: datetime, offset: int) -> LogLine:
|
| 113 |
+
templates = NOISE_TEMPLATES.get(service, [("INFO", "routine operation completed")])
|
| 114 |
+
level, message = random.choice(templates)
|
| 115 |
+
return LogLine(
|
| 116 |
+
timestamp=_make_timestamp(base_time, offset),
|
| 117 |
+
level=level,
|
| 118 |
+
service=service,
|
| 119 |
+
request_id=None,
|
| 120 |
+
message=message,
|
| 121 |
+
latency_ms=None,
|
| 122 |
+
)
|
| 123 |
+
|
| 124 |
+
|
| 125 |
+
def generate_log_batch(
|
| 126 |
+
scenario_signals: list[tuple[str, str, str]], # [(service, level, message), ...]
|
| 127 |
+
step: int,
|
| 128 |
+
base_time: datetime,
|
| 129 |
+
noise_ratio: float = 0.3,
|
| 130 |
+
batch_size: int = 8,
|
| 131 |
+
rng: random.Random = None,
|
| 132 |
+
) -> list[LogLine]:
|
| 133 |
+
"""
|
| 134 |
+
Generate a mixed batch of signal + noise log lines.
|
| 135 |
+
|
| 136 |
+
Args:
|
| 137 |
+
scenario_signals: List of (service, level, message) tuples β the actual signals for this step
|
| 138 |
+
step: Current step number (used for timestamp offset)
|
| 139 |
+
base_time: Episode start time (used for timestamps)
|
| 140 |
+
noise_ratio: Fraction of logs that are noise (0.0 = all signal, 1.0 = all noise)
|
| 141 |
+
batch_size: Total number of log lines to return
|
| 142 |
+
rng: Optional seeded Random for reproducibility
|
| 143 |
+
|
| 144 |
+
Returns:
|
| 145 |
+
List of LogLine objects, shuffled (signal mixed into noise)
|
| 146 |
+
"""
|
| 147 |
+
if rng is None:
|
| 148 |
+
rng = random.Random()
|
| 149 |
+
|
| 150 |
+
logs = []
|
| 151 |
+
base_offset = step * 30 # 30 simulated seconds per step
|
| 152 |
+
|
| 153 |
+
# Add signal logs
|
| 154 |
+
for i, (service, level, message) in enumerate(scenario_signals):
|
| 155 |
+
req_id = f"req-{rng.randint(1000, 9999)}" if level in ("ERROR", "WARN") else None
|
| 156 |
+
logs.append(LogLine(
|
| 157 |
+
timestamp=_make_timestamp(base_time, base_offset + i),
|
| 158 |
+
level=level,
|
| 159 |
+
service=service,
|
| 160 |
+
request_id=req_id,
|
| 161 |
+
message=message,
|
| 162 |
+
latency_ms=rng.randint(200, 5000) if "timeout" in message.lower() or "latency" in message.lower() else None,
|
| 163 |
+
))
|
| 164 |
+
|
| 165 |
+
# Fill remaining slots with noise logs
|
| 166 |
+
noise_count = max(0, batch_size - len(logs))
|
| 167 |
+
noise_services = rng.choices(SERVICES, k=noise_count)
|
| 168 |
+
for i, svc in enumerate(noise_services):
|
| 169 |
+
logs.append(_noise_log(svc, base_time, base_offset + len(scenario_signals) + i))
|
| 170 |
+
|
| 171 |
+
# Shuffle β signal should not always be first
|
| 172 |
+
rng.shuffle(logs)
|
| 173 |
+
return logs[:batch_size]
|
| 174 |
+
|
| 175 |
+
|
| 176 |
+
def generate_healthy_system_state(base_time: datetime) -> dict[str, ServiceStatus]:
|
| 177 |
+
"""Generate a fully healthy system state snapshot."""
|
| 178 |
+
now = _make_timestamp(base_time)
|
| 179 |
+
return {
|
| 180 |
+
svc: ServiceStatus(
|
| 181 |
+
name=svc,
|
| 182 |
+
status="up",
|
| 183 |
+
error_rate=round(random.uniform(0.001, 0.01), 4),
|
| 184 |
+
latency_p99_ms=random.randint(20, 80),
|
| 185 |
+
last_updated=now,
|
| 186 |
+
)
|
| 187 |
+
for svc in SERVICES
|
| 188 |
+
}
|
server/models.py
ADDED
|
@@ -0,0 +1,221 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
from typing import Literal, Optional, ClassVar
|
| 3 |
+
from pydantic import BaseModel, Field
|
| 4 |
+
|
| 5 |
+
|
| 6 |
+
# βββ LOG LINE βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 7 |
+
|
| 8 |
+
class LogLine(BaseModel):
|
| 9 |
+
"""A single log line from the simulated microservice cluster."""
|
| 10 |
+
timestamp: str = Field(..., description="ISO 8601 timestamp")
|
| 11 |
+
level: Literal["DEBUG", "INFO", "WARN", "ERROR", "FATAL"]
|
| 12 |
+
service: str = Field(..., description="Service that emitted the log")
|
| 13 |
+
request_id: Optional[str] = Field(None, description="Request trace ID if present")
|
| 14 |
+
message: str = Field(..., description="Log message content")
|
| 15 |
+
latency_ms: Optional[int] = Field(None, description="Latency if relevant")
|
| 16 |
+
|
| 17 |
+
|
| 18 |
+
# βββ SERVICE STATUS ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 19 |
+
|
| 20 |
+
class ServiceStatus(BaseModel):
|
| 21 |
+
"""Current health snapshot of one microservice."""
|
| 22 |
+
name: str
|
| 23 |
+
status: Literal["up", "degraded", "down"]
|
| 24 |
+
error_rate: float = Field(..., ge=0.0, le=1.0, description="Error rate 0.0-1.0")
|
| 25 |
+
latency_p99_ms: int = Field(..., description="99th percentile latency in ms")
|
| 26 |
+
last_updated: str = Field(..., description="ISO 8601 timestamp of last update")
|
| 27 |
+
|
| 28 |
+
|
| 29 |
+
# βββ ACTION βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 30 |
+
|
| 31 |
+
class TriageAction(BaseModel):
|
| 32 |
+
"""
|
| 33 |
+
Action taken by the agent in one step.
|
| 34 |
+
|
| 35 |
+
action_type options:
|
| 36 |
+
- classify_severity : value must be "P1", "P2", or "P3"
|
| 37 |
+
- identify_root_cause: value must be a valid service name
|
| 38 |
+
- escalate : value must be a valid team name
|
| 39 |
+
- remediate : value must be "restart:<svc>", "rollback:<svc>",
|
| 40 |
+
"scale:<svc>", "flush-cache:<svc>", "kill-query:<svc>"
|
| 41 |
+
- request_more_logs : value must be a service name or "all"
|
| 42 |
+
- resolve : value must be "resolved"
|
| 43 |
+
- ignore : value must be "noise"
|
| 44 |
+
"""
|
| 45 |
+
action_type: Literal[
|
| 46 |
+
"classify_severity",
|
| 47 |
+
"identify_root_cause",
|
| 48 |
+
"escalate",
|
| 49 |
+
"remediate",
|
| 50 |
+
"request_more_logs",
|
| 51 |
+
"resolve",
|
| 52 |
+
"ignore",
|
| 53 |
+
] = Field(..., description="Type of triage action to perform")
|
| 54 |
+
|
| 55 |
+
value: str = Field(
|
| 56 |
+
...,
|
| 57 |
+
description="Action value β depends on action_type (see docstring)"
|
| 58 |
+
)
|
| 59 |
+
|
| 60 |
+
confidence: float = Field(
|
| 61 |
+
default=1.0,
|
| 62 |
+
ge=0.0,
|
| 63 |
+
le=1.0,
|
| 64 |
+
description="Agent self-reported confidence in this action (0.0-1.0)"
|
| 65 |
+
)
|
| 66 |
+
|
| 67 |
+
reasoning: str = Field(
|
| 68 |
+
default="",
|
| 69 |
+
description="Optional free-text reasoning (used for interpretability)"
|
| 70 |
+
)
|
| 71 |
+
|
| 72 |
+
# ββ Valid value constants ββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 73 |
+
VALID_SEVERITIES: ClassVar = {"P1", "P2", "P3"}
|
| 74 |
+
VALID_SERVICES: ClassVar = {
|
| 75 |
+
"api-gateway",
|
| 76 |
+
"auth-service",
|
| 77 |
+
"user-db",
|
| 78 |
+
"payment-service",
|
| 79 |
+
"payment-db",
|
| 80 |
+
"notification-service",
|
| 81 |
+
"email-queue",
|
| 82 |
+
}
|
| 83 |
+
VALID_TEAMS: ClassVar = {
|
| 84 |
+
"sre-team",
|
| 85 |
+
"backend-team",
|
| 86 |
+
"dba-team",
|
| 87 |
+
"security-team",
|
| 88 |
+
}
|
| 89 |
+
VALID_REMEDIATION_PREFIXES: ClassVar = {
|
| 90 |
+
"restart",
|
| 91 |
+
"rollback",
|
| 92 |
+
"scale",
|
| 93 |
+
"flush-cache",
|
| 94 |
+
"kill-query",
|
| 95 |
+
}
|
| 96 |
+
|
| 97 |
+
def is_valid(self) -> tuple[bool, str]:
|
| 98 |
+
"""
|
| 99 |
+
Validate the action value against its action_type.
|
| 100 |
+
Returns (is_valid: bool, error_message: str).
|
| 101 |
+
"""
|
| 102 |
+
if self.action_type == "classify_severity":
|
| 103 |
+
if self.value not in self.VALID_SEVERITIES:
|
| 104 |
+
return False, f"classify_severity value must be one of {self.VALID_SEVERITIES}"
|
| 105 |
+
|
| 106 |
+
elif self.action_type == "identify_root_cause":
|
| 107 |
+
if self.value not in self.VALID_SERVICES:
|
| 108 |
+
return False, f"identify_root_cause value must be one of {self.VALID_SERVICES}"
|
| 109 |
+
|
| 110 |
+
elif self.action_type == "escalate":
|
| 111 |
+
if self.value not in self.VALID_TEAMS:
|
| 112 |
+
return False, f"escalate value must be one of {self.VALID_TEAMS}"
|
| 113 |
+
|
| 114 |
+
elif self.action_type == "remediate":
|
| 115 |
+
prefix = self.value.split(":")[0]
|
| 116 |
+
if prefix not in self.VALID_REMEDIATION_PREFIXES:
|
| 117 |
+
return False, f"remediate prefix must be one of {self.VALID_REMEDIATION_PREFIXES}"
|
| 118 |
+
parts = self.value.split(":")
|
| 119 |
+
if len(parts) != 2 or parts[1] not in self.VALID_SERVICES:
|
| 120 |
+
return False, f"remediate format must be '<action>:<service>'"
|
| 121 |
+
|
| 122 |
+
elif self.action_type == "request_more_logs":
|
| 123 |
+
if self.value != "all" and self.value not in self.VALID_SERVICES:
|
| 124 |
+
return False, f"request_more_logs value must be 'all' or a valid service name"
|
| 125 |
+
|
| 126 |
+
elif self.action_type == "resolve":
|
| 127 |
+
if self.value != "resolved":
|
| 128 |
+
return False, "resolve value must be 'resolved'"
|
| 129 |
+
|
| 130 |
+
elif self.action_type == "ignore":
|
| 131 |
+
if self.value != "noise":
|
| 132 |
+
return False, "ignore value must be 'noise'"
|
| 133 |
+
|
| 134 |
+
return True, ""
|
| 135 |
+
|
| 136 |
+
|
| 137 |
+
# βββ OBSERVATION ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 138 |
+
|
| 139 |
+
class TriageObservation(BaseModel):
|
| 140 |
+
"""
|
| 141 |
+
Observation returned to the agent after each step (and after reset).
|
| 142 |
+
Contains the current log batch, system state, incident metadata,
|
| 143 |
+
and reward signals.
|
| 144 |
+
"""
|
| 145 |
+
# Log batch for this step
|
| 146 |
+
logs: list[LogLine] = Field(
|
| 147 |
+
...,
|
| 148 |
+
description="Current batch of log lines (5-15 lines)"
|
| 149 |
+
)
|
| 150 |
+
|
| 151 |
+
# System state snapshot
|
| 152 |
+
system_state: dict[str, ServiceStatus] = Field(
|
| 153 |
+
...,
|
| 154 |
+
description="Per-service health snapshot keyed by service name"
|
| 155 |
+
)
|
| 156 |
+
|
| 157 |
+
# Incident metadata
|
| 158 |
+
incident_id: str = Field(..., description="Unique ID for this episode")
|
| 159 |
+
task_id: str = Field(..., description="Which task is being run")
|
| 160 |
+
step_count: int = Field(..., description="Current step number (0-indexed)")
|
| 161 |
+
time_elapsed_seconds: int = Field(
|
| 162 |
+
...,
|
| 163 |
+
description="Simulated incident time elapsed in seconds"
|
| 164 |
+
)
|
| 165 |
+
active_alerts: list[str] = Field(
|
| 166 |
+
default_factory=list,
|
| 167 |
+
description="Currently firing alert names"
|
| 168 |
+
)
|
| 169 |
+
|
| 170 |
+
# Reward signals
|
| 171 |
+
reward: float = Field(
|
| 172 |
+
default=0.0,
|
| 173 |
+
description="Reward received for the last action"
|
| 174 |
+
)
|
| 175 |
+
cumulative_score: float = Field(
|
| 176 |
+
default=0.0,
|
| 177 |
+
description="Running total score for this episode"
|
| 178 |
+
)
|
| 179 |
+
done: bool = Field(
|
| 180 |
+
default=False,
|
| 181 |
+
description="Whether the episode has ended"
|
| 182 |
+
)
|
| 183 |
+
|
| 184 |
+
# Feedback
|
| 185 |
+
last_action_feedback: str = Field(
|
| 186 |
+
default="",
|
| 187 |
+
description="Natural language feedback on the previous action"
|
| 188 |
+
)
|
| 189 |
+
invalid_action_error: Optional[str] = Field(
|
| 190 |
+
default=None,
|
| 191 |
+
description="Set if the last action was invalid (wrong format/value)"
|
| 192 |
+
)
|
| 193 |
+
|
| 194 |
+
|
| 195 |
+
# βββ EPISODE STATE ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 196 |
+
|
| 197 |
+
class EpisodeState(BaseModel):
|
| 198 |
+
"""Internal state of the current episode (returned by state() endpoint)."""
|
| 199 |
+
episode_id: str
|
| 200 |
+
task_id: str
|
| 201 |
+
step_count: int
|
| 202 |
+
max_steps: int
|
| 203 |
+
done: bool
|
| 204 |
+
cumulative_score: float
|
| 205 |
+
actions_taken: list[str] = Field(
|
| 206 |
+
default_factory=list,
|
| 207 |
+
description="List of action_type values taken so far this episode"
|
| 208 |
+
)
|
| 209 |
+
action_history: list[dict] = Field(
|
| 210 |
+
default_factory=list,
|
| 211 |
+
description="Full action objects taken this episode (for grader evaluation)"
|
| 212 |
+
)
|
| 213 |
+
correct_severity: Optional[str] = Field(
|
| 214 |
+
None,
|
| 215 |
+
description="Whether agent has correctly classified severity yet"
|
| 216 |
+
)
|
| 217 |
+
correct_root_cause: Optional[str] = Field(
|
| 218 |
+
None,
|
| 219 |
+
description="Whether agent has correctly identified root cause yet"
|
| 220 |
+
)
|
| 221 |
+
correct_remediation: bool = False
|
server/scenarios/__init__.py
ADDED
|
File without changes
|
server/scenarios/cascading.py
ADDED
|
@@ -0,0 +1,211 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Task 2 β Cascading Failure (Medium)
|
| 3 |
+
|
| 4 |
+
Scenario: user-db develops a slow query that exhausts the auth-service connection pool,
|
| 5 |
+
which then causes the api-gateway to return timeouts to all users.
|
| 6 |
+
|
| 7 |
+
Surface logs show gateway errors most loudly (symptom), but root cause is hidden (user-db).
|
| 8 |
+
Agent must trace backward through the cascade chain β NOT treat symptoms as root cause.
|
| 9 |
+
|
| 10 |
+
Ground truth:
|
| 11 |
+
- severity: P1
|
| 12 |
+
- root_cause: user-db
|
| 13 |
+
- remediation: kill-query:user-db OR restart:user-db
|
| 14 |
+
- correct_teams: dba-team, sre-team
|
| 15 |
+
- noise_ratio: 30%
|
| 16 |
+
"""
|
| 17 |
+
from __future__ import annotations
|
| 18 |
+
import random
|
| 19 |
+
from datetime import datetime
|
| 20 |
+
from server.models import LogLine, ServiceStatus
|
| 21 |
+
from server.log_generator import (
|
| 22 |
+
generate_log_batch,
|
| 23 |
+
generate_healthy_system_state,
|
| 24 |
+
_make_timestamp,
|
| 25 |
+
)
|
| 26 |
+
|
| 27 |
+
# βββ GROUND TRUTH βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 28 |
+
|
| 29 |
+
GROUND_TRUTH = {
|
| 30 |
+
"severity": "P1",
|
| 31 |
+
"root_cause": "user-db",
|
| 32 |
+
"remediation_prefixes": {"kill-query", "restart"},
|
| 33 |
+
"remediation_service": "user-db",
|
| 34 |
+
"correct_teams": {"dba-team", "sre-team"},
|
| 35 |
+
"max_steps": 12,
|
| 36 |
+
"noise_ratio": 0.30,
|
| 37 |
+
}
|
| 38 |
+
|
| 39 |
+
# βββ STEP-BY-STEP SIGNAL PLAN βββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 40 |
+
# Cascade chain: user-db slow query β auth-service pool exhausted β api-gateway timeouts
|
| 41 |
+
# Steps 0-1: Gateway errors surface (symptoms only β most visible)
|
| 42 |
+
# Steps 2-3: Auth-service DB pressure becomes visible
|
| 43 |
+
# Steps 4-5: user-db slow queries exposed; circuit breaker opens
|
| 44 |
+
# Steps 6-7: Full cascade β all 3 services degraded/down
|
| 45 |
+
# Steps 8-11: Escalating alerts; root cause becomes unmistakable
|
| 46 |
+
|
| 47 |
+
STEP_SIGNALS = [
|
| 48 |
+
# Step 0: Gateway errors first to appear (surface symptom)
|
| 49 |
+
[
|
| 50 |
+
("api-gateway", "ERROR", "upstream timeout from auth-service: 5002ms"),
|
| 51 |
+
("api-gateway", "WARN", "error rate: 8.3% on /auth/* routes"),
|
| 52 |
+
],
|
| 53 |
+
# Step 1: More gateway errors; first hints of auth-service pressure
|
| 54 |
+
[
|
| 55 |
+
("api-gateway", "ERROR", "upstream timeout from auth-service: 30007ms"),
|
| 56 |
+
("api-gateway", "WARN", "error rate: 15.7% β auth-service latency climbing"),
|
| 57 |
+
],
|
| 58 |
+
# Step 2: Auth-service connection pool pressure visible
|
| 59 |
+
[
|
| 60 |
+
("auth-service", "WARN", "db connection pool at 42/50 β pressure building"),
|
| 61 |
+
("api-gateway", "ERROR", "upstream timeout from auth-service: 30005ms"),
|
| 62 |
+
("auth-service", "ERROR", "db query timeout: SELECT session WHERE user_id=? [5001ms]"),
|
| 63 |
+
],
|
| 64 |
+
# Step 3: Auth-service pool nearly exhausted
|
| 65 |
+
[
|
| 66 |
+
("auth-service", "ERROR", "db connection pool EXHAUSTED (50/50) β blocking new requests"),
|
| 67 |
+
("api-gateway", "ERROR", "auth-service unavailable: connection pool full"),
|
| 68 |
+
("auth-service", "WARN", "request queue depth: 127 β approaching overflow"),
|
| 69 |
+
],
|
| 70 |
+
# Step 4: user-db slow query finally exposed
|
| 71 |
+
[
|
| 72 |
+
("user-db", "WARN", "slow query detected: SELECT * FROM sessions WHERE user_id=? [2847ms]"),
|
| 73 |
+
("auth-service", "ERROR", "db connection timeout after 5000ms β query hanging"),
|
| 74 |
+
("user-db", "ERROR", "lock wait timeout: session table β blocking reads"),
|
| 75 |
+
],
|
| 76 |
+
# Step 5: user-db circuit breaker opens; auth-service starts failing fast
|
| 77 |
+
[
|
| 78 |
+
("user-db", "WARN", "slow query: 4500ms β circuit breaker approaching threshold"),
|
| 79 |
+
("auth-service", "ERROR", "circuit breaker OPEN for user-db: latency exceeded 5000ms"),
|
| 80 |
+
("api-gateway", "ERROR", "all /auth/* requests failing β upstream unavailable"),
|
| 81 |
+
],
|
| 82 |
+
# Step 6: Full cascade β all 3 services degraded
|
| 83 |
+
[
|
| 84 |
+
("api-gateway", "ERROR", "error rate: 67.4% β multiple upstreams timing out"),
|
| 85 |
+
("auth-service", "ERROR", "health check FAILED: cannot reach user-db"),
|
| 86 |
+
("user-db", "ERROR", "connection pool saturated: 95/100 connections in use"),
|
| 87 |
+
],
|
| 88 |
+
# Step 7: api-gateway now fully symptomatic
|
| 89 |
+
[
|
| 90 |
+
("api-gateway", "FATAL", "SLA breach: /auth endpoint availability < 95%"),
|
| 91 |
+
("auth-service", "ERROR", "auth-service DOWN: 3/3 health checks failed"),
|
| 92 |
+
("user-db", "WARN", "slow query count: 847 in last 60s β severe degradation"),
|
| 93 |
+
],
|
| 94 |
+
# Step 8: Database fully exposed as root cause
|
| 95 |
+
[
|
| 96 |
+
("user-db", "ERROR", "CRITICAL: user-db query latency 8000ms+ β active sessions timing out"),
|
| 97 |
+
("auth-service", "ERROR", "rejected: user-db connection pool exhausted"),
|
| 98 |
+
("api-gateway", "ERROR", "user-auth endpoint returning 503 β cascade failure"),
|
| 99 |
+
],
|
| 100 |
+
# Step 9: Escalating
|
| 101 |
+
[
|
| 102 |
+
("user-db", "FATAL", "user-db DOWN: connection pool 100/100 β no connections available"),
|
| 103 |
+
("api-gateway", "ERROR", "error rate: 89.2% β auth-service and user-db both unreachable"),
|
| 104 |
+
],
|
| 105 |
+
# Step 10: Critical
|
| 106 |
+
[
|
| 107 |
+
("api-gateway", "FATAL", "CRITICAL: auth-service DOWN for 90s β 100% of login attempts failing"),
|
| 108 |
+
("user-db", "ERROR", "lock contention: session table fully locked β queries timing out"),
|
| 109 |
+
],
|
| 110 |
+
# Step 11: Maximum severity
|
| 111 |
+
[
|
| 112 |
+
("user-db", "FATAL", "user-db unresponsive for 180s β database crisis"),
|
| 113 |
+
("api-gateway", "FATAL", "SLA_BREACH: auth availability 0% β complete user-auth outage"),
|
| 114 |
+
],
|
| 115 |
+
]
|
| 116 |
+
|
| 117 |
+
|
| 118 |
+
def get_system_state(step: int, base_time: datetime) -> dict[str, ServiceStatus]:
|
| 119 |
+
"""Return system state for this step. Cascade: user-db β auth-service β api-gateway."""
|
| 120 |
+
now = _make_timestamp(base_time, step * 30)
|
| 121 |
+
state = generate_healthy_system_state(base_time)
|
| 122 |
+
|
| 123 |
+
# Escalating degradation based on step
|
| 124 |
+
if step <= 1:
|
| 125 |
+
# Gateway just starting to see issues
|
| 126 |
+
state["api-gateway"] = ServiceStatus(
|
| 127 |
+
name="api-gateway", status="degraded", error_rate=0.083, latency_p99_ms=2500, last_updated=now
|
| 128 |
+
)
|
| 129 |
+
elif step <= 3:
|
| 130 |
+
# Auth-service pool pressure
|
| 131 |
+
state["api-gateway"] = ServiceStatus(
|
| 132 |
+
name="api-gateway", status="degraded", error_rate=0.157, latency_p99_ms=5000, last_updated=now
|
| 133 |
+
)
|
| 134 |
+
state["auth-service"] = ServiceStatus(
|
| 135 |
+
name="auth-service", status="degraded", error_rate=0.15, latency_p99_ms=5000, last_updated=now
|
| 136 |
+
)
|
| 137 |
+
elif step <= 5:
|
| 138 |
+
# user-db slow queries exposed
|
| 139 |
+
state["api-gateway"] = ServiceStatus(
|
| 140 |
+
name="api-gateway", status="degraded", error_rate=0.45, latency_p99_ms=8000, last_updated=now
|
| 141 |
+
)
|
| 142 |
+
state["auth-service"] = ServiceStatus(
|
| 143 |
+
name="auth-service", status="down", error_rate=0.85, latency_p99_ms=10000, last_updated=now
|
| 144 |
+
)
|
| 145 |
+
state["user-db"] = ServiceStatus(
|
| 146 |
+
name="user-db", status="degraded", error_rate=0.30, latency_p99_ms=4500, last_updated=now
|
| 147 |
+
)
|
| 148 |
+
elif step <= 7:
|
| 149 |
+
# Full cascade
|
| 150 |
+
state["api-gateway"] = ServiceStatus(
|
| 151 |
+
name="api-gateway", status="down", error_rate=0.89, latency_p99_ms=10000, last_updated=now
|
| 152 |
+
)
|
| 153 |
+
state["auth-service"] = ServiceStatus(
|
| 154 |
+
name="auth-service", status="down", error_rate=0.95, latency_p99_ms=10000, last_updated=now
|
| 155 |
+
)
|
| 156 |
+
state["user-db"] = ServiceStatus(
|
| 157 |
+
name="user-db", status="down", error_rate=0.50, latency_p99_ms=8000, last_updated=now
|
| 158 |
+
)
|
| 159 |
+
else:
|
| 160 |
+
# Maximum severity
|
| 161 |
+
state["api-gateway"] = ServiceStatus(
|
| 162 |
+
name="api-gateway", status="down", error_rate=0.99, latency_p99_ms=10000, last_updated=now
|
| 163 |
+
)
|
| 164 |
+
state["auth-service"] = ServiceStatus(
|
| 165 |
+
name="auth-service", status="down", error_rate=1.0, latency_p99_ms=10000, last_updated=now
|
| 166 |
+
)
|
| 167 |
+
state["user-db"] = ServiceStatus(
|
| 168 |
+
name="user-db", status="down", error_rate=0.75, latency_p99_ms=10000, last_updated=now
|
| 169 |
+
)
|
| 170 |
+
|
| 171 |
+
return state
|
| 172 |
+
|
| 173 |
+
|
| 174 |
+
def get_step_data(step: int, base_time: datetime, rng: random.Random) -> tuple[list[LogLine], dict[str, ServiceStatus]]:
|
| 175 |
+
"""
|
| 176 |
+
Returns (logs, system_state) for the given step.
|
| 177 |
+
Signal gets louder over time if agent hasn't acted.
|
| 178 |
+
"""
|
| 179 |
+
signal_idx = min(step, len(STEP_SIGNALS) - 1)
|
| 180 |
+
signals = STEP_SIGNALS[signal_idx]
|
| 181 |
+
|
| 182 |
+
logs = generate_log_batch(
|
| 183 |
+
scenario_signals=signals,
|
| 184 |
+
step=step,
|
| 185 |
+
base_time=base_time,
|
| 186 |
+
noise_ratio=GROUND_TRUTH["noise_ratio"],
|
| 187 |
+
batch_size=10,
|
| 188 |
+
rng=rng,
|
| 189 |
+
)
|
| 190 |
+
system_state = get_system_state(step, base_time)
|
| 191 |
+
return logs, system_state
|
| 192 |
+
|
| 193 |
+
|
| 194 |
+
def get_active_alerts(step: int) -> list[str]:
|
| 195 |
+
"""Return active alerts for this step."""
|
| 196 |
+
alerts = []
|
| 197 |
+
if step >= 0:
|
| 198 |
+
alerts.append("api-gateway: elevated error rate on /auth/* routes")
|
| 199 |
+
if step >= 2:
|
| 200 |
+
alerts.append("auth-service: db connection pool pressure")
|
| 201 |
+
if step >= 4:
|
| 202 |
+
alerts.append("user-db: slow queries detected β latency 2000ms+")
|
| 203 |
+
if step >= 5:
|
| 204 |
+
alerts.append("auth-service: circuit breaker OPEN for user-db")
|
| 205 |
+
if step >= 6:
|
| 206 |
+
alerts.append("SLA_BREACH: /auth availability < 90%")
|
| 207 |
+
if step >= 8:
|
| 208 |
+
alerts.append("CRITICAL: user-db connection pool saturated")
|
| 209 |
+
if step >= 10:
|
| 210 |
+
alerts.append("CRITICAL: full auth cascade failure β P1 incident")
|
| 211 |
+
return alerts
|
server/scenarios/silent_degrade.py
ADDED
|
@@ -0,0 +1,132 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Task 3 β Silent Degradation with Noise (Hard)
|
| 3 |
+
|
| 4 |
+
Scenario: payment-db query latency slowly increases over time.
|
| 5 |
+
No service crashes. Error rate stays below P1 threshold (5%).
|
| 6 |
+
60% of logs are irrelevant noise from unrelated services.
|
| 7 |
+
Agent must filter noise, identify subtle signal, classify as P2 (NOT P1, NOT P3).
|
| 8 |
+
|
| 9 |
+
Ground truth:
|
| 10 |
+
- severity: P2 (nuanced β trending toward breach but no hard outage yet)
|
| 11 |
+
- root_cause: payment-db
|
| 12 |
+
- remediation: flush-cache:payment-db OR kill-query:payment-db
|
| 13 |
+
- correct_teams: dba-team
|
| 14 |
+
- noise_ratio: 60% (hardest noise ratio of all tasks)
|
| 15 |
+
"""
|
| 16 |
+
from __future__ import annotations
|
| 17 |
+
import random
|
| 18 |
+
from datetime import datetime
|
| 19 |
+
from server.models import LogLine, ServiceStatus
|
| 20 |
+
from server.log_generator import (
|
| 21 |
+
generate_log_batch,
|
| 22 |
+
generate_healthy_system_state,
|
| 23 |
+
_make_timestamp,
|
| 24 |
+
)
|
| 25 |
+
|
| 26 |
+
# Ground Truth
|
| 27 |
+
|
| 28 |
+
GROUND_TRUTH = {
|
| 29 |
+
"severity": "P2",
|
| 30 |
+
"root_cause": "payment-db",
|
| 31 |
+
"remediation_prefixes": {"flush-cache", "kill-query"},
|
| 32 |
+
"remediation_service": "payment-db",
|
| 33 |
+
"correct_teams": {"dba-team"},
|
| 34 |
+
"max_steps": 15,
|
| 35 |
+
"noise_ratio": 0.60,
|
| 36 |
+
}
|
| 37 |
+
|
| 38 |
+
# Step signals: (service, level, message)
|
| 39 |
+
STEP_SIGNALS = [
|
| 40 |
+
# Step 0: Very subtle
|
| 41 |
+
[("payment-db", "WARN", "payment-db: query latency elevated 450ms (baseline: 12ms)")],
|
| 42 |
+
# Step 1
|
| 43 |
+
[("payment-db", "WARN", "payment-db: query latency 620ms")],
|
| 44 |
+
# Step 2
|
| 45 |
+
[("payment-db", "WARN", "payment-db: slow query: SELECT * FROM transactions WHERE user_id=? [890ms]")],
|
| 46 |
+
# Step 3
|
| 47 |
+
[("payment-db", "WARN", "payment-db: buffer cache hit ratio dropping: 89% to 71%")],
|
| 48 |
+
# Step 4
|
| 49 |
+
[("payment-db", "WARN", "payment-db: query latency 1200ms"), ("payment-service", "WARN", "payment-service: error rate 2.1%")],
|
| 50 |
+
# Step 5
|
| 51 |
+
[("payment-db", "WARN", "payment-db: buffer cache hit ratio 54% β cache thrashing")],
|
| 52 |
+
# Step 6
|
| 53 |
+
[("payment-db", "WARN", "payment-db: slow query: SELECT * FROM transactions [2200ms]")],
|
| 54 |
+
# Step 7
|
| 55 |
+
[("payment-db", "WARN", "payment-db: query latency 2800ms"), ("payment-service", "ERROR", "payment-service: 3.4% error rate")],
|
| 56 |
+
# Step 8
|
| 57 |
+
[("payment-db", "ERROR", "payment-db: slow query timeout: [3100ms] β query cancelled")],
|
| 58 |
+
# Step 9
|
| 59 |
+
[("payment-db", "WARN", "payment-db: query latency 4200ms β cache fully cold")],
|
| 60 |
+
# Step 10
|
| 61 |
+
[("payment-db", "ERROR", "payment-db: query latency 4500ms")],
|
| 62 |
+
# Step 11
|
| 63 |
+
[("payment-db", "WARN", "payment-db: buffer pool pages: 94% dirty")],
|
| 64 |
+
# Step 12
|
| 65 |
+
[("payment-db", "ERROR", "payment-db: query latency 4600ms β timeouts beginning"), ("payment-service", "ERROR", "payment-service: error rate 4.9%")],
|
| 66 |
+
# Step 13: P1 breached
|
| 67 |
+
[("payment-db", "ERROR", "payment-db: CRITICAL query latency 4950ms β P1 breached"), ("payment-service", "ERROR", "payment-service: error rate 5.1% β P1 exceeded")],
|
| 68 |
+
# Step 14: Worst case
|
| 69 |
+
[("payment-db", "FATAL", "payment-db: query latency 5000ms+ β connection pool exhausted"), ("payment-service", "FATAL", "payment-service: P1 CRITICAL β 6.2% error rate")],
|
| 70 |
+
]
|
| 71 |
+
|
| 72 |
+
|
| 73 |
+
def get_system_state(step: int, base_time: datetime) -> dict[str, ServiceStatus]:
|
| 74 |
+
now = _make_timestamp(base_time, step * 30)
|
| 75 |
+
state = generate_healthy_system_state(base_time)
|
| 76 |
+
|
| 77 |
+
latencies = [450, 620, 890, 1200, 1400, 1800, 2200, 2800, 3100, 4200, 4500, 4600, 4600, 4950, 5000]
|
| 78 |
+
error_rates = [0.0, 0.005, 0.01, 0.021, 0.021, 0.025, 0.028, 0.034, 0.038, 0.042, 0.047, 0.049, 0.049, 0.051, 0.062]
|
| 79 |
+
|
| 80 |
+
step_idx = min(step, len(latencies) - 1)
|
| 81 |
+
db_latency = latencies[step_idx]
|
| 82 |
+
db_error = error_rates[step_idx]
|
| 83 |
+
|
| 84 |
+
psvc_latency = min(5000, 340 + db_latency // 2)
|
| 85 |
+
psvc_error = min(0.10, db_error * 0.8)
|
| 86 |
+
|
| 87 |
+
state["payment-db"] = ServiceStatus(
|
| 88 |
+
name="payment-db",
|
| 89 |
+
status="up" if step < 3 else "degraded",
|
| 90 |
+
error_rate=db_error,
|
| 91 |
+
latency_p99_ms=db_latency,
|
| 92 |
+
last_updated=now,
|
| 93 |
+
)
|
| 94 |
+
state["payment-service"] = ServiceStatus(
|
| 95 |
+
name="payment-service",
|
| 96 |
+
status="degraded" if step >= 4 else "up",
|
| 97 |
+
error_rate=psvc_error,
|
| 98 |
+
latency_p99_ms=psvc_latency,
|
| 99 |
+
last_updated=now,
|
| 100 |
+
)
|
| 101 |
+
return state
|
| 102 |
+
|
| 103 |
+
|
| 104 |
+
def get_step_data(step: int, base_time: datetime, rng: random.Random) -> tuple[list[LogLine], dict[str, ServiceStatus]]:
|
| 105 |
+
signal_idx = min(step, len(STEP_SIGNALS) - 1)
|
| 106 |
+
signals = STEP_SIGNALS[signal_idx]
|
| 107 |
+
|
| 108 |
+
logs = generate_log_batch(
|
| 109 |
+
scenario_signals=signals,
|
| 110 |
+
step=step,
|
| 111 |
+
base_time=base_time,
|
| 112 |
+
noise_ratio=GROUND_TRUTH["noise_ratio"],
|
| 113 |
+
batch_size=12,
|
| 114 |
+
rng=rng,
|
| 115 |
+
)
|
| 116 |
+
system_state = get_system_state(step, base_time)
|
| 117 |
+
return logs, system_state
|
| 118 |
+
|
| 119 |
+
|
| 120 |
+
def get_active_alerts(step: int) -> list[str]:
|
| 121 |
+
alerts = []
|
| 122 |
+
if step >= 4:
|
| 123 |
+
alerts.append("payment-service: error rate 2%+ β watching")
|
| 124 |
+
if step >= 6:
|
| 125 |
+
alerts.append("payment-service: p99 latency above threshold")
|
| 126 |
+
if step >= 9:
|
| 127 |
+
alerts.append("payment-db: query latency 4000ms+ β approaching P1 threshold")
|
| 128 |
+
if step >= 12:
|
| 129 |
+
alerts.append("WARNING: payment error rate approaching 5% P1 threshold")
|
| 130 |
+
if step >= 13:
|
| 131 |
+
alerts.append("ALERT: P1 threshold BREACHED for payment-service")
|
| 132 |
+
return alerts
|
server/scenarios/single_crash.py
ADDED
|
@@ -0,0 +1,124 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Task 1 β Single Service Crash (Easy)
|
| 3 |
+
|
| 4 |
+
Scenario: payment-service crashes with NullPointerException on every request.
|
| 5 |
+
All other services are healthy. Logs are mostly unambiguous.
|
| 6 |
+
Noise ratio: ~20%.
|
| 7 |
+
|
| 8 |
+
Ground truth:
|
| 9 |
+
- severity: P1
|
| 10 |
+
- root_cause: payment-service
|
| 11 |
+
- remediation: restart:payment-service
|
| 12 |
+
- correct_team: backend-team
|
| 13 |
+
"""
|
| 14 |
+
from __future__ import annotations
|
| 15 |
+
import random
|
| 16 |
+
from datetime import datetime
|
| 17 |
+
from server.models import LogLine, ServiceStatus
|
| 18 |
+
from server.log_generator import (
|
| 19 |
+
generate_log_batch,
|
| 20 |
+
generate_healthy_system_state,
|
| 21 |
+
SIGNAL_TEMPLATES,
|
| 22 |
+
_make_timestamp,
|
| 23 |
+
)
|
| 24 |
+
|
| 25 |
+
# βββ GROUND TRUTH βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 26 |
+
|
| 27 |
+
GROUND_TRUTH = {
|
| 28 |
+
"severity": "P1",
|
| 29 |
+
"root_cause": "payment-service",
|
| 30 |
+
"remediation_prefixes": {"restart"}, # restart:payment-service is correct
|
| 31 |
+
"remediation_service": "payment-service",
|
| 32 |
+
"correct_teams": {"backend-team", "sre-team"},
|
| 33 |
+
"max_steps": 8,
|
| 34 |
+
"noise_ratio": 0.20,
|
| 35 |
+
}
|
| 36 |
+
|
| 37 |
+
# βββ STEP-BY-STEP SIGNAL PLAN βββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 38 |
+
# Each list = signals injected at that step index.
|
| 39 |
+
# Step 0 = after reset (first observation), Step 7 = last possible step.
|
| 40 |
+
|
| 41 |
+
STEP_SIGNALS = [
|
| 42 |
+
# Step 0: first signs β circuit breaker opens, error rate spike
|
| 43 |
+
[
|
| 44 |
+
("payment-service", "ERROR", "NullPointerException: Cannot invoke processPayment() on null β PaymentProcessor.java:142"),
|
| 45 |
+
("api-gateway", "WARN", "error rate spike: 28.4% of /payment requests failing"),
|
| 46 |
+
],
|
| 47 |
+
# Step 1: escalating β more errors, health check fails
|
| 48 |
+
[
|
| 49 |
+
("payment-service", "FATAL", "NullPointerException in PaymentService.execute() β all retries (3/3) exhausted"),
|
| 50 |
+
("payment-service", "ERROR", "health check FAILED: payment-service returned HTTP 500"),
|
| 51 |
+
],
|
| 52 |
+
# Step 2: circuit breaker fully open
|
| 53 |
+
[
|
| 54 |
+
("api-gateway", "ERROR", "circuit breaker OPEN: payment-service error rate 98.2% (threshold: 10%)"),
|
| 55 |
+
("payment-service", "ERROR", "NullPointerException: Cannot invoke processPayment() on null β PaymentProcessor.java:142"),
|
| 56 |
+
],
|
| 57 |
+
# Step 3+: same signals repeat β incident ongoing until agent acts
|
| 58 |
+
[
|
| 59 |
+
("payment-service", "ERROR", "NullPointerException in PaymentService.execute() β retrying (1/3)"),
|
| 60 |
+
("api-gateway", "ERROR", "upstream failure: payment-service unavailable [circuit breaker: OPEN]"),
|
| 61 |
+
],
|
| 62 |
+
[
|
| 63 |
+
("payment-service", "FATAL", "payment-service health check FAILED for 90s β marking as DOWN"),
|
| 64 |
+
("api-gateway", "WARN", "payment endpoint degraded β all requests returning 503"),
|
| 65 |
+
],
|
| 66 |
+
[
|
| 67 |
+
("payment-service", "ERROR", "NullPointerException: Cannot invoke processPayment() on null β PaymentProcessor.java:142"),
|
| 68 |
+
("api-gateway", "ERROR", "error rate: 99.1% on /payment/* routes"),
|
| 69 |
+
],
|
| 70 |
+
[
|
| 71 |
+
("payment-service", "FATAL", "NullPointerException β service unresponsive for 180s"),
|
| 72 |
+
("api-gateway", "ERROR", "SLA breach: payment service uptime < 99.9%"),
|
| 73 |
+
],
|
| 74 |
+
[
|
| 75 |
+
("payment-service", "FATAL", "CRITICAL: payment-service has been DOWN for 210s β immediate action required"),
|
| 76 |
+
("api-gateway", "ERROR", "all payment transactions failing β revenue impact ongoing"),
|
| 77 |
+
],
|
| 78 |
+
]
|
| 79 |
+
|
| 80 |
+
|
| 81 |
+
def get_system_state(step: int, base_time: datetime) -> dict[str, ServiceStatus]:
|
| 82 |
+
"""Return system state for this step. payment-service is down; others are healthy."""
|
| 83 |
+
now = _make_timestamp(base_time, step * 30)
|
| 84 |
+
state = generate_healthy_system_state(base_time)
|
| 85 |
+
|
| 86 |
+
# Override payment-service to be DOWN
|
| 87 |
+
state["payment-service"] = ServiceStatus(
|
| 88 |
+
name="payment-service",
|
| 89 |
+
status="down",
|
| 90 |
+
error_rate=0.982,
|
| 91 |
+
latency_p99_ms=5000,
|
| 92 |
+
last_updated=now,
|
| 93 |
+
)
|
| 94 |
+
return state
|
| 95 |
+
|
| 96 |
+
|
| 97 |
+
def get_step_data(step: int, base_time: datetime, rng: random.Random) -> tuple[list[LogLine], dict[str, ServiceStatus]]:
|
| 98 |
+
"""
|
| 99 |
+
Returns (logs, system_state) for the given step.
|
| 100 |
+
Signals get louder over time if agent hasn't acted.
|
| 101 |
+
"""
|
| 102 |
+
signal_idx = min(step, len(STEP_SIGNALS) - 1)
|
| 103 |
+
signals = STEP_SIGNALS[signal_idx]
|
| 104 |
+
|
| 105 |
+
logs = generate_log_batch(
|
| 106 |
+
scenario_signals=signals,
|
| 107 |
+
step=step,
|
| 108 |
+
base_time=base_time,
|
| 109 |
+
noise_ratio=GROUND_TRUTH["noise_ratio"],
|
| 110 |
+
batch_size=8,
|
| 111 |
+
rng=rng,
|
| 112 |
+
)
|
| 113 |
+
system_state = get_system_state(step, base_time)
|
| 114 |
+
return logs, system_state
|
| 115 |
+
|
| 116 |
+
|
| 117 |
+
def get_active_alerts(step: int) -> list[str]:
|
| 118 |
+
"""Return active alerts for this step."""
|
| 119 |
+
alerts = ["payment-service: circuit breaker OPEN", "payment-service: health check FAILING"]
|
| 120 |
+
if step >= 2:
|
| 121 |
+
alerts.append("SLA_BREACH: payment availability < 99.9%")
|
| 122 |
+
if step >= 5:
|
| 123 |
+
alerts.append("CRITICAL: payment-service DOWN > 150s")
|
| 124 |
+
return alerts
|