Spaces:
Sleeping
Sleeping
Ajjack4 commited on
Commit ·
095c966
1
Parent(s): 6c700b3
major upgrade: 5 tasks, 7 faults, live HTML dashboard, richer reward shaping
Browse files- api.py +256 -1
- environment.py +70 -58
- tasks.py +123 -45
api.py
CHANGED
|
@@ -4,6 +4,7 @@ FastAPI server — exposes OpenEnv standard endpoints + required hackathon endpo
|
|
| 4 |
|
| 5 |
from fastapi import FastAPI, HTTPException, Request
|
| 6 |
from fastapi.middleware.cors import CORSMiddleware
|
|
|
|
| 7 |
import uvicorn
|
| 8 |
|
| 9 |
from models import Action, Observation
|
|
@@ -219,17 +220,271 @@ async def mcp(request: Request):
|
|
| 219 |
}
|
| 220 |
|
| 221 |
|
| 222 |
-
@app.get("/")
|
| 223 |
def root():
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 224 |
return {
|
| 225 |
"name": "Dev Environment Debugger",
|
| 226 |
"version": "1.0.0",
|
| 227 |
"description": "OpenEnv environment for AI-powered dev environment debugging.",
|
|
|
|
| 228 |
"endpoints": ["/reset", "/step", "/state", "/tasks", "/grader",
|
| 229 |
"/baseline", "/health", "/metadata", "/schema", "/mcp"],
|
| 230 |
"openenv": True,
|
| 231 |
}
|
| 232 |
|
| 233 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 234 |
if __name__ == "__main__":
|
| 235 |
uvicorn.run("api:app", host="0.0.0.0", port=7860, reload=False)
|
|
|
|
| 4 |
|
| 5 |
from fastapi import FastAPI, HTTPException, Request
|
| 6 |
from fastapi.middleware.cors import CORSMiddleware
|
| 7 |
+
from fastapi.responses import HTMLResponse
|
| 8 |
import uvicorn
|
| 9 |
|
| 10 |
from models import Action, Observation
|
|
|
|
| 220 |
}
|
| 221 |
|
| 222 |
|
| 223 |
+
@app.get("/", response_class=HTMLResponse)
|
| 224 |
def root():
|
| 225 |
+
return HTMLResponse(content=DASHBOARD_HTML)
|
| 226 |
+
|
| 227 |
+
|
| 228 |
+
@app.get("/api")
|
| 229 |
+
def api_info():
|
| 230 |
return {
|
| 231 |
"name": "Dev Environment Debugger",
|
| 232 |
"version": "1.0.0",
|
| 233 |
"description": "OpenEnv environment for AI-powered dev environment debugging.",
|
| 234 |
+
"tasks": list(TASKS.keys()),
|
| 235 |
"endpoints": ["/reset", "/step", "/state", "/tasks", "/grader",
|
| 236 |
"/baseline", "/health", "/metadata", "/schema", "/mcp"],
|
| 237 |
"openenv": True,
|
| 238 |
}
|
| 239 |
|
| 240 |
|
| 241 |
+
DASHBOARD_HTML = """<!DOCTYPE html>
|
| 242 |
+
<html lang="en">
|
| 243 |
+
<head>
|
| 244 |
+
<meta charset="UTF-8">
|
| 245 |
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
| 246 |
+
<title>Dev Environment Debugger — OpenEnv</title>
|
| 247 |
+
<style>
|
| 248 |
+
* { box-sizing: border-box; margin: 0; padding: 0; }
|
| 249 |
+
body { font-family: 'Segoe UI', system-ui, sans-serif; background: #0d1117; color: #e6edf3; min-height: 100vh; }
|
| 250 |
+
header { background: linear-gradient(135deg, #161b22 0%, #1f2937 100%); border-bottom: 1px solid #30363d; padding: 24px 32px; display: flex; align-items: center; gap: 16px; }
|
| 251 |
+
header h1 { font-size: 1.5rem; font-weight: 700; }
|
| 252 |
+
header .badge { background: #238636; color: #fff; padding: 4px 10px; border-radius: 12px; font-size: 0.75rem; font-weight: 600; }
|
| 253 |
+
header .subtitle { color: #8b949e; font-size: 0.9rem; margin-top: 4px; }
|
| 254 |
+
main { max-width: 1100px; margin: 0 auto; padding: 32px; }
|
| 255 |
+
.grid { display: grid; grid-template-columns: 1fr 1fr; gap: 24px; margin-bottom: 32px; }
|
| 256 |
+
@media(max-width: 700px) { .grid { grid-template-columns: 1fr; } }
|
| 257 |
+
.card { background: #161b22; border: 1px solid #30363d; border-radius: 12px; padding: 24px; }
|
| 258 |
+
.card h2 { font-size: 1rem; font-weight: 600; color: #58a6ff; margin-bottom: 16px; display: flex; align-items: center; gap: 8px; }
|
| 259 |
+
.service { display: flex; justify-content: space-between; align-items: center; padding: 10px 14px; border-radius: 8px; margin-bottom: 8px; background: #0d1117; border: 1px solid #21262d; }
|
| 260 |
+
.service-name { font-weight: 600; font-size: 0.9rem; }
|
| 261 |
+
.status { padding: 3px 10px; border-radius: 10px; font-size: 0.78rem; font-weight: 700; }
|
| 262 |
+
.healthy { background: #1a4a2a; color: #3fb950; border: 1px solid #238636; }
|
| 263 |
+
.error { background: #4a1a1a; color: #f85149; border: 1px solid #da3633; }
|
| 264 |
+
.task-card { background: #161b22; border: 1px solid #30363d; border-radius: 12px; padding: 20px; margin-bottom: 16px; cursor: pointer; transition: border-color 0.2s; }
|
| 265 |
+
.task-card:hover { border-color: #58a6ff; }
|
| 266 |
+
.task-card.active { border-color: #58a6ff; background: #1c2433; }
|
| 267 |
+
.task-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 8px; }
|
| 268 |
+
.task-name { font-weight: 700; }
|
| 269 |
+
.diff { padding: 3px 10px; border-radius: 10px; font-size: 0.75rem; font-weight: 700; }
|
| 270 |
+
.easy { background: #1a4a2a; color: #3fb950; }
|
| 271 |
+
.medium { background: #4a3a1a; color: #d29922; }
|
| 272 |
+
.hard { background: #4a1a1a; color: #f85149; }
|
| 273 |
+
.expert { background: #2d1a4a; color: #bc8cff; }
|
| 274 |
+
.task-desc { color: #8b949e; font-size: 0.85rem; line-height: 1.5; }
|
| 275 |
+
.actions { display: flex; gap: 10px; flex-wrap: wrap; margin-top: 16px; }
|
| 276 |
+
button { padding: 9px 18px; border-radius: 8px; border: none; cursor: pointer; font-size: 0.85rem; font-weight: 600; transition: opacity 0.2s; }
|
| 277 |
+
button:hover { opacity: 0.85; }
|
| 278 |
+
.btn-primary { background: #238636; color: #fff; }
|
| 279 |
+
.btn-secondary { background: #21262d; color: #e6edf3; border: 1px solid #30363d; }
|
| 280 |
+
.btn-danger { background: #da3633; color: #fff; }
|
| 281 |
+
.log { background: #0d1117; border: 1px solid #21262d; border-radius: 8px; padding: 14px; font-family: 'Cascadia Code', 'Fira Code', monospace; font-size: 0.8rem; line-height: 1.6; max-height: 260px; overflow-y: auto; color: #8b949e; white-space: pre-wrap; word-break: break-all; }
|
| 282 |
+
.log .ok { color: #3fb950; }
|
| 283 |
+
.log .err { color: #f85149; }
|
| 284 |
+
.log .info { color: #58a6ff; }
|
| 285 |
+
.score-bar { height: 8px; border-radius: 4px; background: #21262d; margin-top: 8px; overflow: hidden; }
|
| 286 |
+
.score-fill { height: 100%; border-radius: 4px; background: linear-gradient(90deg, #238636, #3fb950); transition: width 0.5s; }
|
| 287 |
+
.step-counter { font-size: 2rem; font-weight: 800; color: #58a6ff; }
|
| 288 |
+
.stat-label { color: #8b949e; font-size: 0.8rem; margin-top: 4px; }
|
| 289 |
+
.stats { display: grid; grid-template-columns: repeat(3, 1fr); gap: 16px; margin-bottom: 16px; }
|
| 290 |
+
.stat { text-align: center; }
|
| 291 |
+
#action-panel { margin-top: 24px; }
|
| 292 |
+
select, input[type=text] { background: #0d1117; border: 1px solid #30363d; color: #e6edf3; border-radius: 6px; padding: 8px 12px; font-size: 0.85rem; width: 100%; margin-bottom: 8px; }
|
| 293 |
+
.full-width { grid-column: 1 / -1; }
|
| 294 |
+
.arch { display: flex; align-items: center; justify-content: center; gap: 8px; flex-wrap: wrap; padding: 16px 0; }
|
| 295 |
+
.svc-box { padding: 8px 16px; border-radius: 8px; font-size: 0.85rem; font-weight: 600; border: 2px solid; text-align: center; min-width: 80px; }
|
| 296 |
+
.arrow { color: #58a6ff; font-size: 1.2rem; }
|
| 297 |
+
</style>
|
| 298 |
+
</head>
|
| 299 |
+
<body>
|
| 300 |
+
<header>
|
| 301 |
+
<div>
|
| 302 |
+
<div style="display:flex;align-items:center;gap:12px">
|
| 303 |
+
<h1>🔧 Dev Environment Debugger</h1>
|
| 304 |
+
<span class="badge">OpenEnv</span>
|
| 305 |
+
</div>
|
| 306 |
+
<div class="subtitle">AI agent benchmark · 5 tasks · realistic multi-service debugging</div>
|
| 307 |
+
</div>
|
| 308 |
+
</header>
|
| 309 |
+
<main>
|
| 310 |
+
<div class="grid">
|
| 311 |
+
<!-- Services Panel -->
|
| 312 |
+
<div class="card">
|
| 313 |
+
<h2>⚙️ Service Status</h2>
|
| 314 |
+
<div id="arch" class="arch">
|
| 315 |
+
<div class="svc-box" id="box-proxy">proxy</div>
|
| 316 |
+
<div class="arrow">→</div>
|
| 317 |
+
<div class="svc-box" id="box-api">api</div>
|
| 318 |
+
<div class="arrow">→</div>
|
| 319 |
+
<div class="svc-box" id="box-database">database</div>
|
| 320 |
+
</div>
|
| 321 |
+
<div style="text-align:center;margin-bottom:16px">
|
| 322 |
+
<div class="arrow" style="font-size:1rem">↓</div>
|
| 323 |
+
<div class="svc-box" id="box-worker" style="display:inline-block;margin-top:4px">worker</div>
|
| 324 |
+
</div>
|
| 325 |
+
<div id="services-list"></div>
|
| 326 |
+
<div class="stats" style="margin-top:16px">
|
| 327 |
+
<div class="stat"><div class="step-counter" id="step-count">0</div><div class="stat-label">Steps</div></div>
|
| 328 |
+
<div class="stat"><div class="step-counter" id="fixed-count" style="color:#3fb950">0</div><div class="stat-label">Fixed</div></div>
|
| 329 |
+
<div class="stat"><div class="step-counter" id="score-val" style="color:#d29922">—</div><div class="stat-label">Score</div></div>
|
| 330 |
+
</div>
|
| 331 |
+
</div>
|
| 332 |
+
|
| 333 |
+
<!-- Action Panel -->
|
| 334 |
+
<div class="card">
|
| 335 |
+
<h2>🎮 Actions</h2>
|
| 336 |
+
<select id="action-type" onchange="updateActionFields()">
|
| 337 |
+
<option value="read_logs">read_logs</option>
|
| 338 |
+
<option value="inspect_env">inspect_env</option>
|
| 339 |
+
<option value="edit_env">edit_env</option>
|
| 340 |
+
<option value="restart_service">restart_service</option>
|
| 341 |
+
<option value="run_healthcheck">run_healthcheck</option>
|
| 342 |
+
<option value="submit">submit</option>
|
| 343 |
+
</select>
|
| 344 |
+
<div id="service-row">
|
| 345 |
+
<select id="action-service">
|
| 346 |
+
<option value="api">api</option>
|
| 347 |
+
<option value="worker">worker</option>
|
| 348 |
+
<option value="database">database</option>
|
| 349 |
+
<option value="proxy">proxy</option>
|
| 350 |
+
</select>
|
| 351 |
+
</div>
|
| 352 |
+
<div id="kv-row" style="display:none">
|
| 353 |
+
<input type="text" id="action-key" placeholder="env key (e.g. DB_PORT)" />
|
| 354 |
+
<input type="text" id="action-value" placeholder="new value (e.g. 5432)" />
|
| 355 |
+
</div>
|
| 356 |
+
<div class="actions">
|
| 357 |
+
<button class="btn-primary" onclick="doStep()">▶ Execute Action</button>
|
| 358 |
+
<button class="btn-secondary" onclick="doGrade()">📊 Grade</button>
|
| 359 |
+
</div>
|
| 360 |
+
<div class="log" id="action-log" style="margin-top:16px">No actions yet. Start an episode below.</div>
|
| 361 |
+
</div>
|
| 362 |
+
|
| 363 |
+
<!-- Tasks Panel -->
|
| 364 |
+
<div class="card full-width">
|
| 365 |
+
<h2>📋 Tasks</h2>
|
| 366 |
+
<div id="tasks-list"></div>
|
| 367 |
+
</div>
|
| 368 |
+
</div>
|
| 369 |
+
</main>
|
| 370 |
+
|
| 371 |
+
<script>
|
| 372 |
+
let currentTask = 'task1';
|
| 373 |
+
|
| 374 |
+
async function fetchTasks() {
|
| 375 |
+
const r = await fetch('/tasks');
|
| 376 |
+
const d = await r.json();
|
| 377 |
+
const el = document.getElementById('tasks-list');
|
| 378 |
+
el.innerHTML = d.tasks.map(t => `
|
| 379 |
+
<div class="task-card ${t.id === currentTask ? 'active' : ''}" id="tc-${t.id}" onclick="selectTask('${t.id}')">
|
| 380 |
+
<div class="task-header">
|
| 381 |
+
<span class="task-name">${t.id} — ${t.name}</span>
|
| 382 |
+
<span class="diff ${t.difficulty}">${t.difficulty}</span>
|
| 383 |
+
</div>
|
| 384 |
+
<div class="task-desc">${t.description}</div>
|
| 385 |
+
<div class="actions" style="margin-top:12px">
|
| 386 |
+
<button class="btn-primary" onclick="event.stopPropagation(); resetTask('${t.id}')">▶ Start Episode</button>
|
| 387 |
+
<span style="color:#8b949e;font-size:0.8rem;align-self:center">${t.faults_count} fault${t.faults_count>1?'s':''}</span>
|
| 388 |
+
</div>
|
| 389 |
+
</div>
|
| 390 |
+
`).join('');
|
| 391 |
+
}
|
| 392 |
+
|
| 393 |
+
function selectTask(id) {
|
| 394 |
+
currentTask = id;
|
| 395 |
+
document.querySelectorAll('.task-card').forEach(c => c.classList.remove('active'));
|
| 396 |
+
const el = document.getElementById('tc-' + id);
|
| 397 |
+
if (el) el.classList.add('active');
|
| 398 |
+
}
|
| 399 |
+
|
| 400 |
+
async function resetTask(taskId) {
|
| 401 |
+
selectTask(taskId);
|
| 402 |
+
const r = await fetch('/reset?task_id=' + taskId, {method:'POST'});
|
| 403 |
+
const obs = await r.json();
|
| 404 |
+
updateServices(obs.services);
|
| 405 |
+
log('info', 'Episode started: ' + taskId);
|
| 406 |
+
log('ok', obs.last_action_result.message);
|
| 407 |
+
document.getElementById('step-count').textContent = obs.step;
|
| 408 |
+
document.getElementById('fixed-count').textContent = '0';
|
| 409 |
+
document.getElementById('score-val').textContent = '—';
|
| 410 |
+
}
|
| 411 |
+
|
| 412 |
+
async function doStep() {
|
| 413 |
+
const type = document.getElementById('action-type').value;
|
| 414 |
+
const service = document.getElementById('action-service').value;
|
| 415 |
+
const key = document.getElementById('action-key').value;
|
| 416 |
+
const value = document.getElementById('action-value').value;
|
| 417 |
+
const action = {type};
|
| 418 |
+
if (type !== 'submit') action.service = service;
|
| 419 |
+
if (type === 'edit_env') { action.key = key; action.value = value; }
|
| 420 |
+
|
| 421 |
+
const r = await fetch('/step', {method:'POST', headers:{'Content-Type':'application/json'}, body:JSON.stringify(action)});
|
| 422 |
+
const d = await r.json();
|
| 423 |
+
const obs = d.observation;
|
| 424 |
+
updateServices(obs.services);
|
| 425 |
+
document.getElementById('step-count').textContent = obs.step;
|
| 426 |
+
const cls = d.reward > 0 ? 'ok' : d.reward < 0 ? 'err' : 'info';
|
| 427 |
+
log(cls, `[${type}${service && type!=='submit'?' → '+service:''}] reward=${d.reward > 0 ? '+' : ''}${d.reward.toFixed(2)}`);
|
| 428 |
+
log('info', obs.last_action_result.message.replace(/\\n/g, ' '));
|
| 429 |
+
if (d.done) { log('ok', '✓ Episode done!'); doGrade(); }
|
| 430 |
+
}
|
| 431 |
+
|
| 432 |
+
async function doGrade() {
|
| 433 |
+
const r = await fetch('/grader?task_id=' + currentTask);
|
| 434 |
+
const d = await r.json();
|
| 435 |
+
const gr = d.grader_result;
|
| 436 |
+
document.getElementById('score-val').textContent = (gr.score * 100).toFixed(1) + '%';
|
| 437 |
+
document.getElementById('fixed-count').textContent = gr.faults_fixed || 0;
|
| 438 |
+
log('ok', `Score: ${(gr.score*100).toFixed(1)}% — ${gr.reason}`);
|
| 439 |
+
}
|
| 440 |
+
|
| 441 |
+
function updateServices(services) {
|
| 442 |
+
const colors = {healthy: '#3fb950', error: '#f85149'};
|
| 443 |
+
const bg = {healthy: '#1a4a2a', error: '#4a1a1a'};
|
| 444 |
+
const border = {healthy: '#238636', error: '#da3633'};
|
| 445 |
+
const list = document.getElementById('services-list');
|
| 446 |
+
list.innerHTML = Object.entries(services).map(([name, s]) => `
|
| 447 |
+
<div class="service">
|
| 448 |
+
<span class="service-name">${name}</span>
|
| 449 |
+
<div style="display:flex;align-items:center;gap:8px">
|
| 450 |
+
${s.port ? `<span style="color:#8b949e;font-size:0.8rem">:${s.port}</span>` : ''}
|
| 451 |
+
<span class="status ${s.status}">${s.status}</span>
|
| 452 |
+
</div>
|
| 453 |
+
</div>
|
| 454 |
+
`).join('');
|
| 455 |
+
['proxy','api','database','worker'].forEach(name => {
|
| 456 |
+
const box = document.getElementById('box-' + name);
|
| 457 |
+
if (box && services[name]) {
|
| 458 |
+
const st = services[name].status;
|
| 459 |
+
box.style.borderColor = border[st] || '#30363d';
|
| 460 |
+
box.style.color = colors[st] || '#e6edf3';
|
| 461 |
+
box.style.background = bg[st] || '#21262d';
|
| 462 |
+
}
|
| 463 |
+
});
|
| 464 |
+
}
|
| 465 |
+
|
| 466 |
+
function log(cls, msg) {
|
| 467 |
+
const el = document.getElementById('action-log');
|
| 468 |
+
const line = document.createElement('div');
|
| 469 |
+
line.className = cls;
|
| 470 |
+
line.textContent = '› ' + msg;
|
| 471 |
+
el.appendChild(line);
|
| 472 |
+
el.scrollTop = el.scrollHeight;
|
| 473 |
+
if (el.children.length > 100) el.removeChild(el.firstChild);
|
| 474 |
+
}
|
| 475 |
+
|
| 476 |
+
function updateActionFields() {
|
| 477 |
+
const type = document.getElementById('action-type').value;
|
| 478 |
+
document.getElementById('service-row').style.display = type === 'submit' ? 'none' : 'block';
|
| 479 |
+
document.getElementById('kv-row').style.display = type === 'edit_env' ? 'block' : 'none';
|
| 480 |
+
}
|
| 481 |
+
|
| 482 |
+
fetchTasks();
|
| 483 |
+
resetTask('task1');
|
| 484 |
+
</script>
|
| 485 |
+
</body>
|
| 486 |
+
</html>"""
|
| 487 |
+
|
| 488 |
+
|
| 489 |
if __name__ == "__main__":
|
| 490 |
uvicorn.run("api:app", host="0.0.0.0", port=7860, reload=False)
|
environment.py
CHANGED
|
@@ -5,12 +5,11 @@ Agent must diagnose and fix faults to bring all services healthy.
|
|
| 5 |
"""
|
| 6 |
|
| 7 |
import copy
|
| 8 |
-
import random
|
| 9 |
from typing import Any
|
| 10 |
from models import Observation, Action, Reward, ServiceStatus, ActionResult
|
| 11 |
|
| 12 |
# ---------------------------------------------------------------------------
|
| 13 |
-
# Fault
|
| 14 |
# ---------------------------------------------------------------------------
|
| 15 |
|
| 16 |
FAULT_CATALOG = {
|
|
@@ -21,8 +20,8 @@ FAULT_CATALOG = {
|
|
| 21 |
"bad_value": "5433",
|
| 22 |
"fix_key": "DB_PORT",
|
| 23 |
"fix_value": "5432",
|
| 24 |
-
"log": "Error: cannot connect to database on port 5433. Connection refused.",
|
| 25 |
-
"requires_restart": ["api"
|
| 26 |
},
|
| 27 |
"missing_queue_url": {
|
| 28 |
"service": "worker",
|
|
@@ -31,7 +30,7 @@ FAULT_CATALOG = {
|
|
| 31 |
"bad_value": None, # missing
|
| 32 |
"fix_key": "WORKER_QUEUE_URL",
|
| 33 |
"fix_value": "redis://localhost:6379",
|
| 34 |
-
"log": "Error: WORKER_QUEUE_URL is not set. Worker cannot connect to queue.",
|
| 35 |
"requires_restart": ["worker"],
|
| 36 |
},
|
| 37 |
"wrong_api_port": {
|
|
@@ -41,7 +40,7 @@ FAULT_CATALOG = {
|
|
| 41 |
"bad_value": "http://api:9999",
|
| 42 |
"fix_key": "PROXY_UPSTREAM",
|
| 43 |
"fix_value": "http://api:8000",
|
| 44 |
-
"log": "Error: upstream http://api:9999 unreachable. 502 Bad Gateway.",
|
| 45 |
"requires_restart": ["proxy"],
|
| 46 |
},
|
| 47 |
"wrong_db_password": {
|
|
@@ -51,13 +50,43 @@ FAULT_CATALOG = {
|
|
| 51 |
"bad_value": "wrongpassword",
|
| 52 |
"fix_key": "DB_PASSWORD",
|
| 53 |
"fix_value": "secret",
|
| 54 |
-
"log": "FATAL: password authentication failed for user 'app'. Check DB_PASSWORD.",
|
| 55 |
"requires_restart": ["database", "api", "worker"],
|
| 56 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 57 |
}
|
| 58 |
|
| 59 |
# ---------------------------------------------------------------------------
|
| 60 |
-
# Service dependency graph
|
| 61 |
# ---------------------------------------------------------------------------
|
| 62 |
|
| 63 |
DEPENDENCIES = {
|
|
@@ -96,7 +125,7 @@ SERVICE_PORTS = {
|
|
| 96 |
"proxy": 80,
|
| 97 |
}
|
| 98 |
|
| 99 |
-
MAX_STEPS =
|
| 100 |
|
| 101 |
|
| 102 |
# ---------------------------------------------------------------------------
|
|
@@ -124,12 +153,10 @@ class DevEnvEnvironment:
|
|
| 124 |
self._fixed_faults = set()
|
| 125 |
self._log_history = []
|
| 126 |
|
| 127 |
-
# Load task faults
|
| 128 |
from tasks import TASKS
|
| 129 |
task = TASKS[task_id]
|
| 130 |
self._active_faults = list(task["faults"])
|
| 131 |
|
| 132 |
-
# Build initial service env state from defaults
|
| 133 |
self._state = {
|
| 134 |
"envs": copy.deepcopy(DEFAULT_ENV),
|
| 135 |
"services": {},
|
|
@@ -141,16 +168,17 @@ class DevEnvEnvironment:
|
|
| 141 |
svc = fault["service"]
|
| 142 |
key = fault["env_key"]
|
| 143 |
if fault["bad_value"] is None:
|
| 144 |
-
# Remove the key (missing env var)
|
| 145 |
self._state["envs"][svc].pop(key, None)
|
| 146 |
else:
|
| 147 |
self._state["envs"][svc][key] = fault["bad_value"]
|
| 148 |
|
| 149 |
-
# Compute initial service statuses
|
| 150 |
self._recompute_statuses()
|
| 151 |
|
| 152 |
return self._build_observation(
|
| 153 |
-
last_action_result=ActionResult(
|
|
|
|
|
|
|
|
|
|
| 154 |
)
|
| 155 |
|
| 156 |
def step(self, action: Action) -> tuple[Observation, float, bool, dict]:
|
|
@@ -162,9 +190,7 @@ class DevEnvEnvironment:
|
|
| 162 |
|
| 163 |
self._step_count += 1
|
| 164 |
reward = 0.0
|
| 165 |
-
result: ActionResult
|
| 166 |
|
| 167 |
-
# Dispatch action
|
| 168 |
if action.type == "read_logs":
|
| 169 |
result, r = self._handle_read_logs(action)
|
| 170 |
reward += r
|
|
@@ -184,10 +210,9 @@ class DevEnvEnvironment:
|
|
| 184 |
result, r, self._done = self._handle_submit()
|
| 185 |
reward += r
|
| 186 |
else:
|
| 187 |
-
result = ActionResult(success=False, message=f"Unknown action type: {action.type}")
|
| 188 |
reward -= 0.05
|
| 189 |
|
| 190 |
-
# Penalty for exceeding steps
|
| 191 |
if self._step_count >= MAX_STEPS and not self._done:
|
| 192 |
self._done = True
|
| 193 |
reward -= 0.2
|
|
@@ -196,7 +221,11 @@ class DevEnvEnvironment:
|
|
| 196 |
self._log_history.append(f"Step {self._step_count}: {action.type} → {result.message}")
|
| 197 |
|
| 198 |
obs = self._build_observation(result)
|
| 199 |
-
return obs, round(reward, 3), self._done, {
|
|
|
|
|
|
|
|
|
|
|
|
|
| 200 |
|
| 201 |
def state(self) -> dict:
|
| 202 |
return {
|
|
@@ -217,28 +246,27 @@ class DevEnvEnvironment:
|
|
| 217 |
def _handle_read_logs(self, action: Action) -> tuple[ActionResult, float]:
|
| 218 |
svc = action.service
|
| 219 |
if svc not in self._state["services"]:
|
| 220 |
-
return ActionResult(success=False, message=f"Unknown service
|
| 221 |
|
| 222 |
status = self._state["services"][svc]
|
| 223 |
reward = 0.0
|
| 224 |
|
| 225 |
if status.status == "error":
|
| 226 |
-
# Find fault log for this service
|
| 227 |
logs = []
|
| 228 |
for fault_id in self._active_faults:
|
| 229 |
if fault_id not in self._fixed_faults:
|
| 230 |
fault = FAULT_CATALOG[fault_id]
|
| 231 |
if fault["service"] == svc:
|
| 232 |
-
logs.append(fault[
|
| 233 |
-
#
|
| 234 |
for fault_id in self._active_faults:
|
| 235 |
if fault_id not in self._fixed_faults:
|
| 236 |
fault = FAULT_CATALOG[fault_id]
|
| 237 |
if svc in fault.get("requires_restart", []) and fault["service"] != svc:
|
| 238 |
-
logs.append(f"[
|
| 239 |
|
| 240 |
log_text = "\n".join(logs) if logs else "No errors in log."
|
| 241 |
-
reward += 0.05
|
| 242 |
return ActionResult(success=True, message=f"[{svc} logs]\n{log_text}"), reward
|
| 243 |
else:
|
| 244 |
return ActionResult(success=True, message=f"[{svc} logs]\nNo errors. Service running normally."), 0.0
|
|
@@ -246,22 +274,21 @@ class DevEnvEnvironment:
|
|
| 246 |
def _handle_inspect_env(self, action: Action) -> tuple[ActionResult, float]:
|
| 247 |
svc = action.service
|
| 248 |
if svc not in self._state["envs"]:
|
| 249 |
-
return ActionResult(success=False, message=f"Unknown service
|
| 250 |
|
| 251 |
env = self._state["envs"][svc]
|
| 252 |
-
env_text = "\n".join(f" {k}={v}" for k, v in env.items())
|
| 253 |
return ActionResult(success=True, message=f"[{svc} env]\n{env_text}"), 0.02
|
| 254 |
|
| 255 |
def _handle_edit_env(self, action: Action) -> tuple[ActionResult, float]:
|
| 256 |
svc = action.service
|
| 257 |
if not action.key or action.value is None:
|
| 258 |
-
return ActionResult(success=False, message="edit_env requires 'key' and 'value'
|
| 259 |
if svc not in self._state["envs"]:
|
| 260 |
-
return ActionResult(success=False, message=f"Unknown service
|
| 261 |
|
| 262 |
self._state["envs"][svc][action.key] = action.value
|
| 263 |
|
| 264 |
-
# Check if this edit fixes a fault
|
| 265 |
reward = 0.0
|
| 266 |
for fault_id in self._active_faults:
|
| 267 |
if fault_id in self._fixed_faults:
|
|
@@ -270,11 +297,10 @@ class DevEnvEnvironment:
|
|
| 270 |
if (fault["service"] == svc and
|
| 271 |
fault["fix_key"] == action.key and
|
| 272 |
fault["fix_value"] == action.value):
|
| 273 |
-
# Correct fix applied — mark as pending restart
|
| 274 |
reward += 0.1
|
| 275 |
return ActionResult(
|
| 276 |
success=True,
|
| 277 |
-
message=f"Updated {svc}.{action.key}={action.value}.
|
| 278 |
), reward
|
| 279 |
|
| 280 |
return ActionResult(
|
|
@@ -285,12 +311,11 @@ class DevEnvEnvironment:
|
|
| 285 |
def _handle_restart_service(self, action: Action) -> tuple[ActionResult, float]:
|
| 286 |
svc = action.service
|
| 287 |
if svc not in self._state["services"]:
|
| 288 |
-
return ActionResult(success=False, message=f"Unknown service
|
| 289 |
|
| 290 |
reward = 0.0
|
| 291 |
prev_statuses = {s: self._state["services"][s].status for s in self._state["services"]}
|
| 292 |
|
| 293 |
-
# Check if fault is now fixed (env has correct value)
|
| 294 |
for fault_id in self._active_faults:
|
| 295 |
if fault_id in self._fixed_faults:
|
| 296 |
continue
|
|
@@ -302,25 +327,23 @@ class DevEnvEnvironment:
|
|
| 302 |
|
| 303 |
self._recompute_statuses()
|
| 304 |
|
| 305 |
-
# Check improvements
|
| 306 |
for s, status_obj in self._state["services"].items():
|
| 307 |
if prev_statuses[s] != "healthy" and status_obj.status == "healthy":
|
| 308 |
-
reward += 0.3
|
| 309 |
|
| 310 |
-
# Penalty: restarted without fixing root cause
|
| 311 |
if self._state["services"][svc].status == "error":
|
| 312 |
reward -= 0.1
|
| 313 |
return ActionResult(
|
| 314 |
success=True,
|
| 315 |
-
message=f"{svc} restarted but still in error
|
| 316 |
), reward
|
| 317 |
|
| 318 |
-
return ActionResult(success=True, message=f"{svc} restarted successfully."), reward
|
| 319 |
|
| 320 |
def _handle_healthcheck(self, action: Action) -> tuple[ActionResult, float]:
|
| 321 |
svc = action.service
|
| 322 |
if svc not in self._state["services"]:
|
| 323 |
-
return ActionResult(success=False, message=f"Unknown service
|
| 324 |
|
| 325 |
status = self._state["services"][svc]
|
| 326 |
return ActionResult(
|
|
@@ -329,9 +352,7 @@ class DevEnvEnvironment:
|
|
| 329 |
), 0.01
|
| 330 |
|
| 331 |
def _handle_submit(self) -> tuple[ActionResult, float, bool]:
|
| 332 |
-
all_healthy = all(
|
| 333 |
-
s.status == "healthy" for s in self._state["services"].values()
|
| 334 |
-
)
|
| 335 |
if all_healthy:
|
| 336 |
return ActionResult(
|
| 337 |
success=True,
|
|
@@ -341,7 +362,7 @@ class DevEnvEnvironment:
|
|
| 341 |
unhealthy = [k for k, v in self._state["services"].items() if v.status != "healthy"]
|
| 342 |
return ActionResult(
|
| 343 |
success=False,
|
| 344 |
-
message=f"Submit failed. Services still unhealthy: {unhealthy}"
|
| 345 |
), -0.3, True
|
| 346 |
|
| 347 |
# ------------------------------------------------------------------
|
|
@@ -349,34 +370,25 @@ class DevEnvEnvironment:
|
|
| 349 |
# ------------------------------------------------------------------
|
| 350 |
|
| 351 |
def _recompute_statuses(self):
|
| 352 |
-
"""Recompute service statuses based on active unfixed faults and dependencies."""
|
| 353 |
-
# Start all healthy
|
| 354 |
statuses = {svc: "healthy" for svc in DEFAULT_ENV}
|
| 355 |
|
| 356 |
-
# Apply unfixed faults
|
| 357 |
-
remaining = [f for f in self._active_faults if f not in self._fixed_faults]
|
| 358 |
directly_broken = set()
|
| 359 |
-
for fault_id in
|
| 360 |
-
|
| 361 |
-
|
|
|
|
| 362 |
|
| 363 |
-
# Propagate through dependency graph
|
| 364 |
for svc in ["database", "api", "worker", "proxy"]:
|
| 365 |
if svc in directly_broken:
|
| 366 |
statuses[svc] = "error"
|
| 367 |
else:
|
| 368 |
-
# Check if any dependency is broken
|
| 369 |
for dep in DEPENDENCIES.get(svc, []):
|
| 370 |
if statuses.get(dep) == "error":
|
| 371 |
statuses[svc] = "error"
|
| 372 |
break
|
| 373 |
|
| 374 |
self._state["services"] = {
|
| 375 |
-
svc: ServiceStatus(
|
| 376 |
-
name=svc,
|
| 377 |
-
status=statuses[svc],
|
| 378 |
-
port=SERVICE_PORTS[svc]
|
| 379 |
-
)
|
| 380 |
for svc in DEFAULT_ENV
|
| 381 |
}
|
| 382 |
|
|
|
|
| 5 |
"""
|
| 6 |
|
| 7 |
import copy
|
|
|
|
| 8 |
from typing import Any
|
| 9 |
from models import Observation, Action, Reward, ServiceStatus, ActionResult
|
| 10 |
|
| 11 |
# ---------------------------------------------------------------------------
|
| 12 |
+
# Fault catalog — injectable faults with realistic log messages
|
| 13 |
# ---------------------------------------------------------------------------
|
| 14 |
|
| 15 |
FAULT_CATALOG = {
|
|
|
|
| 20 |
"bad_value": "5433",
|
| 21 |
"fix_key": "DB_PORT",
|
| 22 |
"fix_value": "5432",
|
| 23 |
+
"log": "Error: cannot connect to database on port 5433. Connection refused. Is the database running?",
|
| 24 |
+
"requires_restart": ["api"],
|
| 25 |
},
|
| 26 |
"missing_queue_url": {
|
| 27 |
"service": "worker",
|
|
|
|
| 30 |
"bad_value": None, # missing
|
| 31 |
"fix_key": "WORKER_QUEUE_URL",
|
| 32 |
"fix_value": "redis://localhost:6379",
|
| 33 |
+
"log": "Error: WORKER_QUEUE_URL is not set. Worker cannot connect to queue. Set WORKER_QUEUE_URL.",
|
| 34 |
"requires_restart": ["worker"],
|
| 35 |
},
|
| 36 |
"wrong_api_port": {
|
|
|
|
| 40 |
"bad_value": "http://api:9999",
|
| 41 |
"fix_key": "PROXY_UPSTREAM",
|
| 42 |
"fix_value": "http://api:8000",
|
| 43 |
+
"log": "Error: upstream http://api:9999 unreachable. 502 Bad Gateway. Check PROXY_UPSTREAM.",
|
| 44 |
"requires_restart": ["proxy"],
|
| 45 |
},
|
| 46 |
"wrong_db_password": {
|
|
|
|
| 50 |
"bad_value": "wrongpassword",
|
| 51 |
"fix_key": "DB_PASSWORD",
|
| 52 |
"fix_value": "secret",
|
| 53 |
+
"log": "FATAL: password authentication failed for user 'app'. Check DB_PASSWORD env var.",
|
| 54 |
"requires_restart": ["database", "api", "worker"],
|
| 55 |
},
|
| 56 |
+
"wrong_db_host": {
|
| 57 |
+
"service": "api",
|
| 58 |
+
"description": "DB_HOST points to wrong hostname",
|
| 59 |
+
"env_key": "DB_HOST",
|
| 60 |
+
"bad_value": "db-prod-01.internal",
|
| 61 |
+
"fix_key": "DB_HOST",
|
| 62 |
+
"fix_value": "localhost",
|
| 63 |
+
"log": "Error: could not resolve host 'db-prod-01.internal'. Check DB_HOST. Is this a prod config?",
|
| 64 |
+
"requires_restart": ["api"],
|
| 65 |
+
},
|
| 66 |
+
"wrong_api_host": {
|
| 67 |
+
"service": "worker",
|
| 68 |
+
"description": "Worker API_HOST points to wrong address",
|
| 69 |
+
"env_key": "API_HOST",
|
| 70 |
+
"bad_value": "http://api:9000",
|
| 71 |
+
"fix_key": "API_HOST",
|
| 72 |
+
"fix_value": "http://api:8000",
|
| 73 |
+
"log": "Error: worker cannot reach API at http://api:9000. Connection refused. Check API_HOST.",
|
| 74 |
+
"requires_restart": ["worker"],
|
| 75 |
+
},
|
| 76 |
+
"missing_db_password": {
|
| 77 |
+
"service": "api",
|
| 78 |
+
"description": "DB_PASSWORD not set on API service",
|
| 79 |
+
"env_key": "DB_PASSWORD",
|
| 80 |
+
"bad_value": None,
|
| 81 |
+
"fix_key": "DB_PASSWORD",
|
| 82 |
+
"fix_value": "secret",
|
| 83 |
+
"log": "Error: DB_PASSWORD is not set. Database connection rejected (auth requires password).",
|
| 84 |
+
"requires_restart": ["api"],
|
| 85 |
+
},
|
| 86 |
}
|
| 87 |
|
| 88 |
# ---------------------------------------------------------------------------
|
| 89 |
+
# Service dependency graph
|
| 90 |
# ---------------------------------------------------------------------------
|
| 91 |
|
| 92 |
DEPENDENCIES = {
|
|
|
|
| 125 |
"proxy": 80,
|
| 126 |
}
|
| 127 |
|
| 128 |
+
MAX_STEPS = 30
|
| 129 |
|
| 130 |
|
| 131 |
# ---------------------------------------------------------------------------
|
|
|
|
| 153 |
self._fixed_faults = set()
|
| 154 |
self._log_history = []
|
| 155 |
|
|
|
|
| 156 |
from tasks import TASKS
|
| 157 |
task = TASKS[task_id]
|
| 158 |
self._active_faults = list(task["faults"])
|
| 159 |
|
|
|
|
| 160 |
self._state = {
|
| 161 |
"envs": copy.deepcopy(DEFAULT_ENV),
|
| 162 |
"services": {},
|
|
|
|
| 168 |
svc = fault["service"]
|
| 169 |
key = fault["env_key"]
|
| 170 |
if fault["bad_value"] is None:
|
|
|
|
| 171 |
self._state["envs"][svc].pop(key, None)
|
| 172 |
else:
|
| 173 |
self._state["envs"][svc][key] = fault["bad_value"]
|
| 174 |
|
|
|
|
| 175 |
self._recompute_statuses()
|
| 176 |
|
| 177 |
return self._build_observation(
|
| 178 |
+
last_action_result=ActionResult(
|
| 179 |
+
success=True,
|
| 180 |
+
message="Environment initialized. Services started. Check service statuses and read logs."
|
| 181 |
+
)
|
| 182 |
)
|
| 183 |
|
| 184 |
def step(self, action: Action) -> tuple[Observation, float, bool, dict]:
|
|
|
|
| 190 |
|
| 191 |
self._step_count += 1
|
| 192 |
reward = 0.0
|
|
|
|
| 193 |
|
|
|
|
| 194 |
if action.type == "read_logs":
|
| 195 |
result, r = self._handle_read_logs(action)
|
| 196 |
reward += r
|
|
|
|
| 210 |
result, r, self._done = self._handle_submit()
|
| 211 |
reward += r
|
| 212 |
else:
|
| 213 |
+
result = ActionResult(success=False, message=f"Unknown action type: '{action.type}'. Valid: read_logs, inspect_env, edit_env, restart_service, run_healthcheck, submit")
|
| 214 |
reward -= 0.05
|
| 215 |
|
|
|
|
| 216 |
if self._step_count >= MAX_STEPS and not self._done:
|
| 217 |
self._done = True
|
| 218 |
reward -= 0.2
|
|
|
|
| 221 |
self._log_history.append(f"Step {self._step_count}: {action.type} → {result.message}")
|
| 222 |
|
| 223 |
obs = self._build_observation(result)
|
| 224 |
+
return obs, round(reward, 3), self._done, {
|
| 225 |
+
"step": self._step_count,
|
| 226 |
+
"fixed_faults": list(self._fixed_faults),
|
| 227 |
+
"remaining_faults": [f for f in self._active_faults if f not in self._fixed_faults],
|
| 228 |
+
}
|
| 229 |
|
| 230 |
def state(self) -> dict:
|
| 231 |
return {
|
|
|
|
| 246 |
def _handle_read_logs(self, action: Action) -> tuple[ActionResult, float]:
|
| 247 |
svc = action.service
|
| 248 |
if svc not in self._state["services"]:
|
| 249 |
+
return ActionResult(success=False, message=f"Unknown service '{svc}'. Valid: api, worker, database, proxy"), -0.05
|
| 250 |
|
| 251 |
status = self._state["services"][svc]
|
| 252 |
reward = 0.0
|
| 253 |
|
| 254 |
if status.status == "error":
|
|
|
|
| 255 |
logs = []
|
| 256 |
for fault_id in self._active_faults:
|
| 257 |
if fault_id not in self._fixed_faults:
|
| 258 |
fault = FAULT_CATALOG[fault_id]
|
| 259 |
if fault["service"] == svc:
|
| 260 |
+
logs.append(f"[ERROR] {fault['log']}")
|
| 261 |
+
# Add downstream symptom logs for dependent failures
|
| 262 |
for fault_id in self._active_faults:
|
| 263 |
if fault_id not in self._fixed_faults:
|
| 264 |
fault = FAULT_CATALOG[fault_id]
|
| 265 |
if svc in fault.get("requires_restart", []) and fault["service"] != svc:
|
| 266 |
+
logs.append(f"[ERROR] Service unavailable — upstream dependency '{fault['service']}' is failing.")
|
| 267 |
|
| 268 |
log_text = "\n".join(logs) if logs else "No errors in log."
|
| 269 |
+
reward += 0.05
|
| 270 |
return ActionResult(success=True, message=f"[{svc} logs]\n{log_text}"), reward
|
| 271 |
else:
|
| 272 |
return ActionResult(success=True, message=f"[{svc} logs]\nNo errors. Service running normally."), 0.0
|
|
|
|
| 274 |
def _handle_inspect_env(self, action: Action) -> tuple[ActionResult, float]:
|
| 275 |
svc = action.service
|
| 276 |
if svc not in self._state["envs"]:
|
| 277 |
+
return ActionResult(success=False, message=f"Unknown service '{svc}'. Valid: api, worker, database, proxy"), -0.05
|
| 278 |
|
| 279 |
env = self._state["envs"][svc]
|
| 280 |
+
env_text = "\n".join(f" {k}={v}" for k, v in sorted(env.items()))
|
| 281 |
return ActionResult(success=True, message=f"[{svc} env]\n{env_text}"), 0.02
|
| 282 |
|
| 283 |
def _handle_edit_env(self, action: Action) -> tuple[ActionResult, float]:
|
| 284 |
svc = action.service
|
| 285 |
if not action.key or action.value is None:
|
| 286 |
+
return ActionResult(success=False, message="edit_env requires 'key' and 'value'."), -0.05
|
| 287 |
if svc not in self._state["envs"]:
|
| 288 |
+
return ActionResult(success=False, message=f"Unknown service '{svc}'."), -0.05
|
| 289 |
|
| 290 |
self._state["envs"][svc][action.key] = action.value
|
| 291 |
|
|
|
|
| 292 |
reward = 0.0
|
| 293 |
for fault_id in self._active_faults:
|
| 294 |
if fault_id in self._fixed_faults:
|
|
|
|
| 297 |
if (fault["service"] == svc and
|
| 298 |
fault["fix_key"] == action.key and
|
| 299 |
fault["fix_value"] == action.value):
|
|
|
|
| 300 |
reward += 0.1
|
| 301 |
return ActionResult(
|
| 302 |
success=True,
|
| 303 |
+
message=f"Updated {svc}.{action.key}={action.value}. Now restart {svc} to apply."
|
| 304 |
), reward
|
| 305 |
|
| 306 |
return ActionResult(
|
|
|
|
| 311 |
def _handle_restart_service(self, action: Action) -> tuple[ActionResult, float]:
|
| 312 |
svc = action.service
|
| 313 |
if svc not in self._state["services"]:
|
| 314 |
+
return ActionResult(success=False, message=f"Unknown service '{svc}'."), -0.05
|
| 315 |
|
| 316 |
reward = 0.0
|
| 317 |
prev_statuses = {s: self._state["services"][s].status for s in self._state["services"]}
|
| 318 |
|
|
|
|
| 319 |
for fault_id in self._active_faults:
|
| 320 |
if fault_id in self._fixed_faults:
|
| 321 |
continue
|
|
|
|
| 327 |
|
| 328 |
self._recompute_statuses()
|
| 329 |
|
|
|
|
| 330 |
for s, status_obj in self._state["services"].items():
|
| 331 |
if prev_statuses[s] != "healthy" and status_obj.status == "healthy":
|
| 332 |
+
reward += 0.3
|
| 333 |
|
|
|
|
| 334 |
if self._state["services"][svc].status == "error":
|
| 335 |
reward -= 0.1
|
| 336 |
return ActionResult(
|
| 337 |
success=True,
|
| 338 |
+
message=f"{svc} restarted but still in error. Root cause not fixed yet."
|
| 339 |
), reward
|
| 340 |
|
| 341 |
+
return ActionResult(success=True, message=f"{svc} restarted successfully. Status: healthy."), reward
|
| 342 |
|
| 343 |
def _handle_healthcheck(self, action: Action) -> tuple[ActionResult, float]:
|
| 344 |
svc = action.service
|
| 345 |
if svc not in self._state["services"]:
|
| 346 |
+
return ActionResult(success=False, message=f"Unknown service '{svc}'."), -0.05
|
| 347 |
|
| 348 |
status = self._state["services"][svc]
|
| 349 |
return ActionResult(
|
|
|
|
| 352 |
), 0.01
|
| 353 |
|
| 354 |
def _handle_submit(self) -> tuple[ActionResult, float, bool]:
|
| 355 |
+
all_healthy = all(s.status == "healthy" for s in self._state["services"].values())
|
|
|
|
|
|
|
| 356 |
if all_healthy:
|
| 357 |
return ActionResult(
|
| 358 |
success=True,
|
|
|
|
| 362 |
unhealthy = [k for k, v in self._state["services"].items() if v.status != "healthy"]
|
| 363 |
return ActionResult(
|
| 364 |
success=False,
|
| 365 |
+
message=f"Submit failed. Services still unhealthy: {unhealthy}. Fix all services before submitting."
|
| 366 |
), -0.3, True
|
| 367 |
|
| 368 |
# ------------------------------------------------------------------
|
|
|
|
| 370 |
# ------------------------------------------------------------------
|
| 371 |
|
| 372 |
def _recompute_statuses(self):
|
|
|
|
|
|
|
| 373 |
statuses = {svc: "healthy" for svc in DEFAULT_ENV}
|
| 374 |
|
|
|
|
|
|
|
| 375 |
directly_broken = set()
|
| 376 |
+
for fault_id in self._active_faults:
|
| 377 |
+
if fault_id not in self._fixed_faults:
|
| 378 |
+
fault = FAULT_CATALOG[fault_id]
|
| 379 |
+
directly_broken.add(fault["service"])
|
| 380 |
|
|
|
|
| 381 |
for svc in ["database", "api", "worker", "proxy"]:
|
| 382 |
if svc in directly_broken:
|
| 383 |
statuses[svc] = "error"
|
| 384 |
else:
|
|
|
|
| 385 |
for dep in DEPENDENCIES.get(svc, []):
|
| 386 |
if statuses.get(dep) == "error":
|
| 387 |
statuses[svc] = "error"
|
| 388 |
break
|
| 389 |
|
| 390 |
self._state["services"] = {
|
| 391 |
+
svc: ServiceStatus(name=svc, status=statuses[svc], port=SERVICE_PORTS[svc])
|
|
|
|
|
|
|
|
|
|
|
|
|
| 392 |
for svc in DEFAULT_ENV
|
| 393 |
}
|
| 394 |
|
tasks.py
CHANGED
|
@@ -1,6 +1,6 @@
|
|
| 1 |
"""
|
| 2 |
Task definitions and programmatic graders.
|
| 3 |
-
Each task defines faults to inject and a grader that scores
|
| 4 |
"""
|
| 5 |
|
| 6 |
TASKS = {
|
|
@@ -31,13 +31,37 @@ TASKS = {
|
|
| 31 |
"name": "Cascading Failure Debug",
|
| 32 |
"difficulty": "hard",
|
| 33 |
"description": (
|
| 34 |
-
"The database has wrong credentials, causing the API to
|
| 35 |
-
"'connection timeout' error, which
|
| 36 |
-
"Logs are misleading — the agent must trace the root cause (DB_PASSWORD
|
| 37 |
-
"than patching downstream symptoms."
|
| 38 |
),
|
| 39 |
"faults": ["wrong_db_password"],
|
| 40 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 41 |
}
|
| 42 |
|
| 43 |
|
|
@@ -53,12 +77,11 @@ def grade_episode(task_id: str, env_state: dict) -> dict:
|
|
| 53 |
"""
|
| 54 |
task = TASKS.get(task_id)
|
| 55 |
if not task:
|
| 56 |
-
return {"score": _clamp(0.
|
| 57 |
|
| 58 |
services = env_state.get("services", {})
|
| 59 |
fixed_faults = set(env_state.get("fixed_faults", []))
|
| 60 |
active_faults = set(env_state.get("active_faults", []))
|
| 61 |
-
remaining = active_faults - fixed_faults
|
| 62 |
steps = env_state.get("step", 0)
|
| 63 |
|
| 64 |
all_healthy = all(s["status"] == "healthy" for s in services.values())
|
|
@@ -66,13 +89,12 @@ def grade_episode(task_id: str, env_state: dict) -> dict:
|
|
| 66 |
faults_fixed = len(fixed_faults)
|
| 67 |
|
| 68 |
# ---------------------------------------------------------------
|
| 69 |
-
# Task 1
|
| 70 |
# ---------------------------------------------------------------
|
| 71 |
if task_id == "task1":
|
| 72 |
if all_healthy and faults_fixed == total_faults:
|
| 73 |
-
|
| 74 |
-
|
| 75 |
-
score = min(1.0, 0.85 + efficiency * 0.15)
|
| 76 |
return {
|
| 77 |
"score": _clamp(score),
|
| 78 |
"reason": f"All services healthy. Fixed in {steps} steps.",
|
|
@@ -81,55 +103,52 @@ def grade_episode(task_id: str, env_state: dict) -> dict:
|
|
| 81 |
}
|
| 82 |
elif faults_fixed > 0:
|
| 83 |
return {
|
| 84 |
-
"score": _clamp(0.
|
| 85 |
-
"reason": "Fault fixed but
|
| 86 |
"faults_fixed": faults_fixed,
|
| 87 |
"total_faults": total_faults,
|
| 88 |
}
|
| 89 |
-
|
| 90 |
-
|
| 91 |
-
|
| 92 |
-
|
| 93 |
-
|
| 94 |
-
|
| 95 |
-
}
|
| 96 |
|
| 97 |
# ---------------------------------------------------------------
|
| 98 |
-
# Task 2
|
| 99 |
# ---------------------------------------------------------------
|
| 100 |
if task_id == "task2":
|
| 101 |
if all_healthy and faults_fixed == total_faults:
|
| 102 |
-
efficiency = max(0.0, 1.0 - (steps -
|
| 103 |
-
score =
|
| 104 |
return {
|
| 105 |
"score": _clamp(score),
|
| 106 |
-
"reason": f"
|
| 107 |
"faults_fixed": faults_fixed,
|
| 108 |
"total_faults": total_faults,
|
| 109 |
}
|
| 110 |
elif faults_fixed == 1:
|
| 111 |
return {
|
| 112 |
-
"score": _clamp(0.
|
| 113 |
-
"reason":
|
| 114 |
"faults_fixed": faults_fixed,
|
| 115 |
"total_faults": total_faults,
|
| 116 |
}
|
| 117 |
-
|
| 118 |
-
|
| 119 |
-
|
| 120 |
-
|
| 121 |
-
|
| 122 |
-
|
| 123 |
-
}
|
| 124 |
|
| 125 |
# ---------------------------------------------------------------
|
| 126 |
-
# Task 3
|
| 127 |
# ---------------------------------------------------------------
|
| 128 |
if task_id == "task3":
|
| 129 |
if all_healthy and faults_fixed == total_faults:
|
| 130 |
-
|
| 131 |
-
|
| 132 |
-
score = min(1.0, 0.80 + efficiency * 0.20)
|
| 133 |
return {
|
| 134 |
"score": _clamp(score),
|
| 135 |
"reason": f"Root cause found and fixed. All services recovered in {steps} steps.",
|
|
@@ -137,19 +156,78 @@ def grade_episode(task_id: str, env_state: dict) -> dict:
|
|
| 137 |
"total_faults": total_faults,
|
| 138 |
}
|
| 139 |
elif faults_fixed > 0:
|
| 140 |
-
# Fixed something but not all healthy — partial
|
| 141 |
return {
|
| 142 |
-
"score": _clamp(0.
|
| 143 |
-
"reason": "
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 144 |
"faults_fixed": faults_fixed,
|
| 145 |
"total_faults": total_faults,
|
| 146 |
}
|
| 147 |
-
|
| 148 |
return {
|
| 149 |
-
"score": _clamp(0.
|
| 150 |
-
"reason": "
|
| 151 |
-
"faults_fixed":
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 152 |
"total_faults": total_faults,
|
| 153 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 154 |
|
| 155 |
-
return {"score": _clamp(0.
|
|
|
|
| 1 |
"""
|
| 2 |
Task definitions and programmatic graders.
|
| 3 |
+
Each task defines faults to inject and a grader that scores strictly in (0, 1).
|
| 4 |
"""
|
| 5 |
|
| 6 |
TASKS = {
|
|
|
|
| 31 |
"name": "Cascading Failure Debug",
|
| 32 |
"difficulty": "hard",
|
| 33 |
"description": (
|
| 34 |
+
"The database has wrong credentials, causing the API to log a misleading "
|
| 35 |
+
"'connection timeout' error, which cascades to the worker and proxy. "
|
| 36 |
+
"Logs are misleading — the agent must trace the root cause (DB_PASSWORD on the "
|
| 37 |
+
"database service) rather than patching downstream symptoms."
|
| 38 |
),
|
| 39 |
"faults": ["wrong_db_password"],
|
| 40 |
},
|
| 41 |
+
"task4": {
|
| 42 |
+
"id": "task4",
|
| 43 |
+
"name": "Triple Fault Storm",
|
| 44 |
+
"difficulty": "hard",
|
| 45 |
+
"description": (
|
| 46 |
+
"Three independent faults hit at once: wrong DB_HOST on the API, "
|
| 47 |
+
"wrong API_HOST on the worker, and wrong PROXY_UPSTREAM on the proxy. "
|
| 48 |
+
"Each service is broken for a different reason. Agent must diagnose and fix all three "
|
| 49 |
+
"without being misled by cascading errors."
|
| 50 |
+
),
|
| 51 |
+
"faults": ["wrong_db_host", "wrong_api_host", "wrong_api_port"],
|
| 52 |
+
},
|
| 53 |
+
"task5": {
|
| 54 |
+
"id": "task5",
|
| 55 |
+
"name": "Silent Misconfiguration",
|
| 56 |
+
"difficulty": "expert",
|
| 57 |
+
"description": (
|
| 58 |
+
"Two subtle faults with no obvious error messages: DB_PASSWORD is missing on the API "
|
| 59 |
+
"(not wrong — missing entirely), and the worker's API_HOST points to the wrong port. "
|
| 60 |
+
"Services appear to start but immediately fail silently. "
|
| 61 |
+
"The agent must inspect env vars carefully rather than relying on logs alone."
|
| 62 |
+
),
|
| 63 |
+
"faults": ["missing_db_password", "wrong_api_host"],
|
| 64 |
+
},
|
| 65 |
}
|
| 66 |
|
| 67 |
|
|
|
|
| 77 |
"""
|
| 78 |
task = TASKS.get(task_id)
|
| 79 |
if not task:
|
| 80 |
+
return {"score": _clamp(0.001), "reason": f"Unknown task: {task_id}"}
|
| 81 |
|
| 82 |
services = env_state.get("services", {})
|
| 83 |
fixed_faults = set(env_state.get("fixed_faults", []))
|
| 84 |
active_faults = set(env_state.get("active_faults", []))
|
|
|
|
| 85 |
steps = env_state.get("step", 0)
|
| 86 |
|
| 87 |
all_healthy = all(s["status"] == "healthy" for s in services.values())
|
|
|
|
| 89 |
faults_fixed = len(fixed_faults)
|
| 90 |
|
| 91 |
# ---------------------------------------------------------------
|
| 92 |
+
# Task 1 — single fault, efficiency bonus
|
| 93 |
# ---------------------------------------------------------------
|
| 94 |
if task_id == "task1":
|
| 95 |
if all_healthy and faults_fixed == total_faults:
|
| 96 |
+
efficiency = max(0.0, 1.0 - max(0, steps - 4) * 0.04)
|
| 97 |
+
score = 0.75 + efficiency * 0.20
|
|
|
|
| 98 |
return {
|
| 99 |
"score": _clamp(score),
|
| 100 |
"reason": f"All services healthy. Fixed in {steps} steps.",
|
|
|
|
| 103 |
}
|
| 104 |
elif faults_fixed > 0:
|
| 105 |
return {
|
| 106 |
+
"score": _clamp(0.35),
|
| 107 |
+
"reason": "Fault fixed but episode not completed correctly.",
|
| 108 |
"faults_fixed": faults_fixed,
|
| 109 |
"total_faults": total_faults,
|
| 110 |
}
|
| 111 |
+
return {
|
| 112 |
+
"score": _clamp(0.05),
|
| 113 |
+
"reason": "No faults fixed.",
|
| 114 |
+
"faults_fixed": 0,
|
| 115 |
+
"total_faults": total_faults,
|
| 116 |
+
}
|
|
|
|
| 117 |
|
| 118 |
# ---------------------------------------------------------------
|
| 119 |
+
# Task 2 — partial credit per fault fixed
|
| 120 |
# ---------------------------------------------------------------
|
| 121 |
if task_id == "task2":
|
| 122 |
if all_healthy and faults_fixed == total_faults:
|
| 123 |
+
efficiency = max(0.0, 1.0 - max(0, steps - 6) * 0.03)
|
| 124 |
+
score = 0.70 + efficiency * 0.25
|
| 125 |
return {
|
| 126 |
"score": _clamp(score),
|
| 127 |
+
"reason": f"Both faults fixed. All services healthy in {steps} steps.",
|
| 128 |
"faults_fixed": faults_fixed,
|
| 129 |
"total_faults": total_faults,
|
| 130 |
}
|
| 131 |
elif faults_fixed == 1:
|
| 132 |
return {
|
| 133 |
+
"score": _clamp(0.40),
|
| 134 |
+
"reason": "One of two faults fixed. Partial credit.",
|
| 135 |
"faults_fixed": faults_fixed,
|
| 136 |
"total_faults": total_faults,
|
| 137 |
}
|
| 138 |
+
return {
|
| 139 |
+
"score": _clamp(0.05),
|
| 140 |
+
"reason": "No faults fixed.",
|
| 141 |
+
"faults_fixed": 0,
|
| 142 |
+
"total_faults": total_faults,
|
| 143 |
+
}
|
|
|
|
| 144 |
|
| 145 |
# ---------------------------------------------------------------
|
| 146 |
+
# Task 3 — cascading root cause
|
| 147 |
# ---------------------------------------------------------------
|
| 148 |
if task_id == "task3":
|
| 149 |
if all_healthy and faults_fixed == total_faults:
|
| 150 |
+
efficiency = max(0.0, 1.0 - max(0, steps - 5) * 0.04)
|
| 151 |
+
score = 0.70 + efficiency * 0.25
|
|
|
|
| 152 |
return {
|
| 153 |
"score": _clamp(score),
|
| 154 |
"reason": f"Root cause found and fixed. All services recovered in {steps} steps.",
|
|
|
|
| 156 |
"total_faults": total_faults,
|
| 157 |
}
|
| 158 |
elif faults_fixed > 0:
|
|
|
|
| 159 |
return {
|
| 160 |
+
"score": _clamp(0.30),
|
| 161 |
+
"reason": "Root cause addressed but services not fully recovered.",
|
| 162 |
+
"faults_fixed": faults_fixed,
|
| 163 |
+
"total_faults": total_faults,
|
| 164 |
+
}
|
| 165 |
+
return {
|
| 166 |
+
"score": _clamp(0.05),
|
| 167 |
+
"reason": "Root cause not identified.",
|
| 168 |
+
"faults_fixed": 0,
|
| 169 |
+
"total_faults": total_faults,
|
| 170 |
+
}
|
| 171 |
+
|
| 172 |
+
# ---------------------------------------------------------------
|
| 173 |
+
# Task 4 — triple fault storm (partial credit per fault)
|
| 174 |
+
# ---------------------------------------------------------------
|
| 175 |
+
if task_id == "task4":
|
| 176 |
+
if all_healthy and faults_fixed == total_faults:
|
| 177 |
+
efficiency = max(0.0, 1.0 - max(0, steps - 10) * 0.025)
|
| 178 |
+
score = 0.65 + efficiency * 0.30
|
| 179 |
+
return {
|
| 180 |
+
"score": _clamp(score),
|
| 181 |
+
"reason": f"All 3 faults fixed. All services healthy in {steps} steps.",
|
| 182 |
+
"faults_fixed": faults_fixed,
|
| 183 |
+
"total_faults": total_faults,
|
| 184 |
+
}
|
| 185 |
+
elif faults_fixed == 2:
|
| 186 |
+
return {
|
| 187 |
+
"score": _clamp(0.55),
|
| 188 |
+
"reason": "2 of 3 faults fixed.",
|
| 189 |
"faults_fixed": faults_fixed,
|
| 190 |
"total_faults": total_faults,
|
| 191 |
}
|
| 192 |
+
elif faults_fixed == 1:
|
| 193 |
return {
|
| 194 |
+
"score": _clamp(0.28),
|
| 195 |
+
"reason": "1 of 3 faults fixed.",
|
| 196 |
+
"faults_fixed": faults_fixed,
|
| 197 |
+
"total_faults": total_faults,
|
| 198 |
+
}
|
| 199 |
+
return {
|
| 200 |
+
"score": _clamp(0.05),
|
| 201 |
+
"reason": "No faults fixed.",
|
| 202 |
+
"faults_fixed": 0,
|
| 203 |
+
"total_faults": total_faults,
|
| 204 |
+
}
|
| 205 |
+
|
| 206 |
+
# ---------------------------------------------------------------
|
| 207 |
+
# Task 5 — silent misconfiguration (expert)
|
| 208 |
+
# ---------------------------------------------------------------
|
| 209 |
+
if task_id == "task5":
|
| 210 |
+
if all_healthy and faults_fixed == total_faults:
|
| 211 |
+
efficiency = max(0.0, 1.0 - max(0, steps - 8) * 0.03)
|
| 212 |
+
score = 0.65 + efficiency * 0.30
|
| 213 |
+
return {
|
| 214 |
+
"score": _clamp(score),
|
| 215 |
+
"reason": f"Both silent faults found and fixed in {steps} steps. Expert diagnosis.",
|
| 216 |
+
"faults_fixed": faults_fixed,
|
| 217 |
+
"total_faults": total_faults,
|
| 218 |
+
}
|
| 219 |
+
elif faults_fixed == 1:
|
| 220 |
+
return {
|
| 221 |
+
"score": _clamp(0.38),
|
| 222 |
+
"reason": "One silent fault found. Second still undetected.",
|
| 223 |
+
"faults_fixed": faults_fixed,
|
| 224 |
"total_faults": total_faults,
|
| 225 |
}
|
| 226 |
+
return {
|
| 227 |
+
"score": _clamp(0.05),
|
| 228 |
+
"reason": "No faults found. Agent must inspect env vars, not just logs.",
|
| 229 |
+
"faults_fixed": 0,
|
| 230 |
+
"total_faults": total_faults,
|
| 231 |
+
}
|
| 232 |
|
| 233 |
+
return {"score": _clamp(0.05), "reason": "Unhandled task."}
|