OGrohit commited on
Commit
c41eb02
Β·
1 Parent(s): 6ce1392

Phase 1: repo setup and environment port

Browse files
.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