feat: replay system, human challenge, progress viz, 2 new generator failure modes
Browse files- Feature 1: Episode Replay System
- replay_store dict + replay_counter track completed episodes
- /step endpoint accumulates per-step data (action, reward, obs summary)
- On done=True, full replay saved with timeline steps
- GET /replays β JSON list of replays, newest first
- GET /replay/{id} β full replay data with steps
- GET /replay/{id}/html β dark-theme HTML timeline visualization
- GET /replays/html β HTML index page
- Feature 2: Human vs Agent Challenge
- GET /challenge β fully interactive HTML page to play incidents
- Auto-resets to easy task seed 1234 on load
- Action form with all 14 action types, context-sensitive fields
- Live reward feedback per action, running score display
- End-of-episode comparison panel: human score vs agent avg from /metrics
- Feature 3: Progress Visualization
- GET /progress β dark-theme HTML progress dashboard
- Section 1: Live task vs random baseline bar chart (CSS-only, no JS)
- Section 2: Hardcoded training results table (baseline β fine-tuned)
- Section 3: Live episode score timeline via canvas + 30s auto-refresh
- Feature 4: Two new IncidentFactory failure modes
- network_latency: latency-only degradation, difficulty=0.55
distractor noise: maintenance window INFO alert
- config_drift: healthy-looking service with wrong config, difficulty=0.75
distractor noise: unrelated high-memory alert
- Both use rollback as optimal fix
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- generator/incident_factory.py +30 -6
- server/app.py +738 -0
|
@@ -1,7 +1,7 @@
|
|
| 1 |
import random
|
| 2 |
|
| 3 |
class IncidentFactory:
|
| 4 |
-
FAILURE_MODES = ["oom", "cascade", "corruption", "security", "database", "network_partition"]
|
| 5 |
SERVICES = ["payment-service", "order-service", "user-service",
|
| 6 |
"inventory-service", "api-gateway", "notification-service",
|
| 7 |
"data-pipeline", "ml-inference-service"]
|
|
@@ -13,6 +13,15 @@ class IncidentFactory:
|
|
| 13 |
"CDN cache warming after deployment",
|
| 14 |
"Routine health check latency spike"
|
| 15 |
]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 16 |
|
| 17 |
def generate(self, seed: int) -> dict:
|
| 18 |
rng = random.Random(seed)
|
|
@@ -20,7 +29,15 @@ class IncidentFactory:
|
|
| 20 |
affected_service = rng.choice(self.SERVICES)
|
| 21 |
severity = rng.choice(self.SEVERITIES)
|
| 22 |
noise_count = rng.randint(0, 3)
|
| 23 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 24 |
|
| 25 |
# Templates for description
|
| 26 |
descriptions = {
|
|
@@ -29,7 +46,9 @@ class IncidentFactory:
|
|
| 29 |
"corruption": f"Silent data corruption in {affected_service}. Invalid records being written. No error-rate alerts β signal buried in business metrics.",
|
| 30 |
"security": f"Credential stuffing botnet targeting {affected_service} from IP range 185.220.x.x.",
|
| 31 |
"database": f"Missing index on {affected_service} database after migration. Full table scans degrading all queries.",
|
| 32 |
-
"network_partition": f"Network partition isolating {affected_service} in us-east-1. Failover decision required."
|
|
|
|
|
|
|
| 33 |
}
|
| 34 |
description = descriptions[failure_mode]
|
| 35 |
|
|
@@ -40,7 +59,9 @@ class IncidentFactory:
|
|
| 40 |
"corruption": f"Data corruption in {affected_service} writing invalid records",
|
| 41 |
"security": f"DDoS credential stuffing from 185.220.0.0/16 targeting {affected_service}",
|
| 42 |
"database": f"Missing index causing sequential scans on {affected_service}",
|
| 43 |
-
"network_partition": f"Network partition in us-east-1 affecting {affected_service}"
|
|
|
|
|
|
|
| 44 |
}
|
| 45 |
ground_truth_root_cause = root_causes[failure_mode]
|
| 46 |
|
|
@@ -51,14 +72,17 @@ class IncidentFactory:
|
|
| 51 |
"corruption": "rollback",
|
| 52 |
"security": "block_ip_range",
|
| 53 |
"database": "create_index",
|
| 54 |
-
"network_partition": "failover"
|
|
|
|
|
|
|
| 55 |
}
|
| 56 |
ground_truth_fix = fixes[failure_mode]
|
| 57 |
|
| 58 |
# Difficulty score
|
| 59 |
base_scores = {
|
| 60 |
"oom": 0.2, "cascade": 0.5, "corruption": 0.8,
|
| 61 |
-
"security": 0.6, "database": 0.6, "network_partition": 0.7
|
|
|
|
| 62 |
}
|
| 63 |
score = base_scores[failure_mode] + (noise_count * 0.05)
|
| 64 |
score = min(score, 1.0)
|
|
|
|
| 1 |
import random
|
| 2 |
|
| 3 |
class IncidentFactory:
|
| 4 |
+
FAILURE_MODES = ["oom", "cascade", "corruption", "security", "database", "network_partition", "network_latency", "config_drift"]
|
| 5 |
SERVICES = ["payment-service", "order-service", "user-service",
|
| 6 |
"inventory-service", "api-gateway", "notification-service",
|
| 7 |
"data-pipeline", "ml-inference-service"]
|
|
|
|
| 13 |
"CDN cache warming after deployment",
|
| 14 |
"Routine health check latency spike"
|
| 15 |
]
|
| 16 |
+
NOISE_ALERTS_NETWORK_LATENCY = [
|
| 17 |
+
"INFO: Scheduled maintenance window active β some latency variation expected",
|
| 18 |
+
"CDN edge node rotation in progress",
|
| 19 |
+
"Routine health check latency spike",
|
| 20 |
+
]
|
| 21 |
+
NOISE_ALERTS_CONFIG_DRIFT = [
|
| 22 |
+
"WARNING: data-pipeline memory usage elevated (85%) β not yet critical",
|
| 23 |
+
"INFO: Scheduled batch job running β high CPU expected",
|
| 24 |
+
]
|
| 25 |
|
| 26 |
def generate(self, seed: int) -> dict:
|
| 27 |
rng = random.Random(seed)
|
|
|
|
| 29 |
affected_service = rng.choice(self.SERVICES)
|
| 30 |
severity = rng.choice(self.SEVERITIES)
|
| 31 |
noise_count = rng.randint(0, 3)
|
| 32 |
+
if failure_mode == "network_latency":
|
| 33 |
+
noise_pool = self.NOISE_ALERTS_NETWORK_LATENCY
|
| 34 |
+
noise_count = min(noise_count + 1, len(noise_pool))
|
| 35 |
+
elif failure_mode == "config_drift":
|
| 36 |
+
noise_pool = self.NOISE_ALERTS_CONFIG_DRIFT
|
| 37 |
+
noise_count = min(noise_count + 1, len(noise_pool))
|
| 38 |
+
else:
|
| 39 |
+
noise_pool = self.NOISE_ALERTS
|
| 40 |
+
noise_alerts = rng.sample(noise_pool, min(noise_count, len(noise_pool)))
|
| 41 |
|
| 42 |
# Templates for description
|
| 43 |
descriptions = {
|
|
|
|
| 46 |
"corruption": f"Silent data corruption in {affected_service}. Invalid records being written. No error-rate alerts β signal buried in business metrics.",
|
| 47 |
"security": f"Credential stuffing botnet targeting {affected_service} from IP range 185.220.x.x.",
|
| 48 |
"database": f"Missing index on {affected_service} database after migration. Full table scans degrading all queries.",
|
| 49 |
+
"network_partition": f"Network partition isolating {affected_service} in us-east-1. Failover decision required.",
|
| 50 |
+
"network_latency": f"Gradual latency degradation in {affected_service}. All services remain running but P99 latency is 10β50x normal. No crashes, no OOM β signal is purely in latency metrics.",
|
| 51 |
+
"config_drift": f"{affected_service} deployed with incorrect environment variables. Service appears healthy β error rate is low β but outputs are wrong. Signal buried in CONFIG_ERROR log warnings and downstream data validation failures.",
|
| 52 |
}
|
| 53 |
description = descriptions[failure_mode]
|
| 54 |
|
|
|
|
| 59 |
"corruption": f"Data corruption in {affected_service} writing invalid records",
|
| 60 |
"security": f"DDoS credential stuffing from 185.220.0.0/16 targeting {affected_service}",
|
| 61 |
"database": f"Missing index causing sequential scans on {affected_service}",
|
| 62 |
+
"network_partition": f"Network partition in us-east-1 affecting {affected_service}",
|
| 63 |
+
"network_latency": f"Network latency degradation in {affected_service}",
|
| 64 |
+
"config_drift": f"Config drift in {affected_service} writing incorrect output",
|
| 65 |
}
|
| 66 |
ground_truth_root_cause = root_causes[failure_mode]
|
| 67 |
|
|
|
|
| 72 |
"corruption": "rollback",
|
| 73 |
"security": "block_ip_range",
|
| 74 |
"database": "create_index",
|
| 75 |
+
"network_partition": "failover",
|
| 76 |
+
"network_latency": "rollback",
|
| 77 |
+
"config_drift": "rollback",
|
| 78 |
}
|
| 79 |
ground_truth_fix = fixes[failure_mode]
|
| 80 |
|
| 81 |
# Difficulty score
|
| 82 |
base_scores = {
|
| 83 |
"oom": 0.2, "cascade": 0.5, "corruption": 0.8,
|
| 84 |
+
"security": 0.6, "database": 0.6, "network_partition": 0.7,
|
| 85 |
+
"network_latency": 0.55, "config_drift": 0.75,
|
| 86 |
}
|
| 87 |
score = base_scores[failure_mode] + (noise_count * 0.05)
|
| 88 |
score = min(score, 1.0)
|
|
@@ -20,6 +20,10 @@ multi_agent_sessions: dict = {}
|
|
| 20 |
|
| 21 |
episode_history = deque(maxlen=1000)
|
| 22 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 23 |
def track_episode(state_obj: State):
|
| 24 |
from graders.grader import grade_episode
|
| 25 |
score = grade_episode(
|
|
@@ -1436,6 +1440,7 @@ async def reset(req: Optional[ResetRequest] = None):
|
|
| 1436 |
status_code=400,
|
| 1437 |
detail=f"task_id must be one of {VALID_TASKS} or 'generated'. Got: {req.task_id}",
|
| 1438 |
)
|
|
|
|
| 1439 |
return await _env.reset(seed=req.seed, task_id=req.task_id)
|
| 1440 |
|
| 1441 |
|
|
@@ -1469,11 +1474,41 @@ async def step(action: Action):
|
|
| 1469 |
Side effects:
|
| 1470 |
On done=True, records the episode in the leaderboard and metrics history.
|
| 1471 |
"""
|
|
|
|
| 1472 |
if _env._logic is None:
|
| 1473 |
raise HTTPException(status_code=400, detail="Call /reset before /step")
|
| 1474 |
res = await _env.step(action)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1475 |
if res.done:
|
| 1476 |
track_episode(_env.state)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1477 |
return res
|
| 1478 |
|
| 1479 |
|
|
@@ -2048,3 +2083,706 @@ def get_curriculum_hint(task_id: str):
|
|
| 2048 |
}
|
| 2049 |
except ValueError as exc:
|
| 2050 |
raise HTTPException(status_code=400, detail=str(exc))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 20 |
|
| 21 |
episode_history = deque(maxlen=1000)
|
| 22 |
|
| 23 |
+
replay_store: dict = {}
|
| 24 |
+
replay_counter: int = 0
|
| 25 |
+
current_episode_steps: list = []
|
| 26 |
+
|
| 27 |
def track_episode(state_obj: State):
|
| 28 |
from graders.grader import grade_episode
|
| 29 |
score = grade_episode(
|
|
|
|
| 1440 |
status_code=400,
|
| 1441 |
detail=f"task_id must be one of {VALID_TASKS} or 'generated'. Got: {req.task_id}",
|
| 1442 |
)
|
| 1443 |
+
current_episode_steps.clear()
|
| 1444 |
return await _env.reset(seed=req.seed, task_id=req.task_id)
|
| 1445 |
|
| 1446 |
|
|
|
|
| 1474 |
Side effects:
|
| 1475 |
On done=True, records the episode in the leaderboard and metrics history.
|
| 1476 |
"""
|
| 1477 |
+
global replay_counter
|
| 1478 |
if _env._logic is None:
|
| 1479 |
raise HTTPException(status_code=400, detail="Call /reset before /step")
|
| 1480 |
res = await _env.step(action)
|
| 1481 |
+
|
| 1482 |
+
step_data = {
|
| 1483 |
+
"step": len(current_episode_steps),
|
| 1484 |
+
"action": action.dict(),
|
| 1485 |
+
"reward": res.reward,
|
| 1486 |
+
"observation_summary": {
|
| 1487 |
+
"failing_services": [s.name for s in res.observation.services if s.status in ("down", "degraded")],
|
| 1488 |
+
"alert_count": len(res.observation.active_alerts),
|
| 1489 |
+
"evidence_count": len(res.observation.evidence_log),
|
| 1490 |
+
},
|
| 1491 |
+
}
|
| 1492 |
+
current_episode_steps.append(step_data)
|
| 1493 |
+
|
| 1494 |
if res.done:
|
| 1495 |
track_episode(_env.state)
|
| 1496 |
+
state = _env.state
|
| 1497 |
+
replay_store[str(replay_counter)] = {
|
| 1498 |
+
"episode_id": str(replay_counter),
|
| 1499 |
+
"task_id": state.task_id,
|
| 1500 |
+
"seed": state.info.get("seed", 0),
|
| 1501 |
+
"final_score": round(state.total_reward, 3),
|
| 1502 |
+
"resolved": state.incident_resolved,
|
| 1503 |
+
"total_steps": state.step,
|
| 1504 |
+
"timestamp": datetime.utcnow().isoformat(),
|
| 1505 |
+
"steps": list(current_episode_steps),
|
| 1506 |
+
}
|
| 1507 |
+
replay_counter += 1
|
| 1508 |
+
if len(replay_store) > 20:
|
| 1509 |
+
oldest = min(replay_store.keys(), key=int)
|
| 1510 |
+
del replay_store[oldest]
|
| 1511 |
+
|
| 1512 |
return res
|
| 1513 |
|
| 1514 |
|
|
|
|
| 2083 |
}
|
| 2084 |
except ValueError as exc:
|
| 2085 |
raise HTTPException(status_code=400, detail=str(exc))
|
| 2086 |
+
|
| 2087 |
+
|
| 2088 |
+
# βββ Feature 1: Episode Replay System ββββββββββββββββββββββββββββββββββββββββ
|
| 2089 |
+
|
| 2090 |
+
@app.get("/replays")
|
| 2091 |
+
def list_replays():
|
| 2092 |
+
"""List available episode replays, newest first."""
|
| 2093 |
+
items = []
|
| 2094 |
+
for ep_id, r in replay_store.items():
|
| 2095 |
+
items.append({
|
| 2096 |
+
"episode_id": r["episode_id"],
|
| 2097 |
+
"task_id": r["task_id"],
|
| 2098 |
+
"score": r["final_score"],
|
| 2099 |
+
"resolved": r["resolved"],
|
| 2100 |
+
"total_steps": r["total_steps"],
|
| 2101 |
+
"timestamp": r["timestamp"],
|
| 2102 |
+
})
|
| 2103 |
+
items.sort(key=lambda x: int(x["episode_id"]), reverse=True)
|
| 2104 |
+
return items
|
| 2105 |
+
|
| 2106 |
+
|
| 2107 |
+
@app.get("/replay/{episode_id}")
|
| 2108 |
+
def get_replay(episode_id: str):
|
| 2109 |
+
"""Return full replay data for an episode."""
|
| 2110 |
+
if episode_id not in replay_store:
|
| 2111 |
+
raise HTTPException(status_code=404, detail="Episode not found")
|
| 2112 |
+
return replay_store[episode_id]
|
| 2113 |
+
|
| 2114 |
+
|
| 2115 |
+
@app.get("/replay/{episode_id}/html", response_class=HTMLResponse)
|
| 2116 |
+
def get_replay_html(episode_id: str):
|
| 2117 |
+
"""Return an HTML timeline visualization of an episode replay."""
|
| 2118 |
+
if episode_id not in replay_store:
|
| 2119 |
+
raise HTTPException(status_code=404, detail="Episode not found")
|
| 2120 |
+
r = replay_store[episode_id]
|
| 2121 |
+
|
| 2122 |
+
def reward_color(reward: float) -> str:
|
| 2123 |
+
if reward > 0.2:
|
| 2124 |
+
return "#00ff88"
|
| 2125 |
+
if reward > 0:
|
| 2126 |
+
return "#ffaa00"
|
| 2127 |
+
if reward == 0:
|
| 2128 |
+
return "#ff3355"
|
| 2129 |
+
return "#ff3355"
|
| 2130 |
+
|
| 2131 |
+
def reward_bg(reward: float) -> str:
|
| 2132 |
+
if reward > 0.2:
|
| 2133 |
+
return "rgba(0,255,136,0.08)"
|
| 2134 |
+
if reward > 0:
|
| 2135 |
+
return "rgba(255,170,0,0.08)"
|
| 2136 |
+
return "rgba(255,51,85,0.08)"
|
| 2137 |
+
|
| 2138 |
+
steps_html = ""
|
| 2139 |
+
running_score = 0.0
|
| 2140 |
+
for s in r["steps"]:
|
| 2141 |
+
running_score += s["reward"]
|
| 2142 |
+
act = s["action"]
|
| 2143 |
+
act_type = act.get("action_type", "unknown")
|
| 2144 |
+
service = act.get("service") or ""
|
| 2145 |
+
rc = act.get("root_cause") or ""
|
| 2146 |
+
obs = s["observation_summary"]
|
| 2147 |
+
col = reward_color(s["reward"])
|
| 2148 |
+
bg = reward_bg(s["reward"])
|
| 2149 |
+
reward_sign = "+" if s["reward"] > 0 else ""
|
| 2150 |
+
failing = ", ".join(obs["failing_services"]) if obs["failing_services"] else "none"
|
| 2151 |
+
steps_html += f"""
|
| 2152 |
+
<div class="step-card" style="border-left-color:{col}; background:{bg}">
|
| 2153 |
+
<div class="step-header">
|
| 2154 |
+
<span class="step-num mono">STEP {s['step'] + 1}</span>
|
| 2155 |
+
<span class="step-action mono">{act_type} {service}</span>
|
| 2156 |
+
<span class="step-reward mono" style="color:{col}">{reward_sign}{s['reward']:.3f}</span>
|
| 2157 |
+
<span class="step-running mono" style="color:#4d9fff">∑ {running_score:.3f}</span>
|
| 2158 |
+
</div>
|
| 2159 |
+
{"<div class='step-rc'>→ " + rc + "</div>" if rc and act_type == "diagnose" else ""}
|
| 2160 |
+
<div class="step-obs mono">
|
| 2161 |
+
failing: {failing} |
|
| 2162 |
+
alerts: {obs['alert_count']} |
|
| 2163 |
+
evidence: {obs['evidence_count']}
|
| 2164 |
+
</div>
|
| 2165 |
+
</div>"""
|
| 2166 |
+
|
| 2167 |
+
resolved_color = "#00ff88" if r["resolved"] else "#ff3355"
|
| 2168 |
+
resolved_text = "INCIDENT RESOLVED" if r["resolved"] else "INCIDENT ESCALATED"
|
| 2169 |
+
resolved_icon = "✓" if r["resolved"] else "✗"
|
| 2170 |
+
score_col = "#00ff88" if r["final_score"] >= 0.7 else "#ffaa00" if r["final_score"] >= 0.4 else "#ff3355"
|
| 2171 |
+
|
| 2172 |
+
html = f"""<!DOCTYPE html>
|
| 2173 |
+
<html lang="en">
|
| 2174 |
+
<head>
|
| 2175 |
+
<meta charset="UTF-8">
|
| 2176 |
+
<title>ARIA Replay #{episode_id}</title>
|
| 2177 |
+
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;600;700&family=Share+Tech+Mono&display=swap" rel="stylesheet">
|
| 2178 |
+
<style>
|
| 2179 |
+
:root {{--bg:#060914;--surface:#0a0f1e;--surface2:#0d1628;--border:#1a2744;--blue:#4d9fff;--cyan:#00d4ff;--green:#00ff88;--yellow:#ffaa00;--red:#ff3355;--text:#c8d8f0;--text-dim:#4a6080;}}
|
| 2180 |
+
*{{box-sizing:border-box;margin:0;padding:0;}}
|
| 2181 |
+
body{{background:var(--bg);color:var(--text);font-family:'Inter',sans-serif;padding:24px;max-width:900px;margin:0 auto;}}
|
| 2182 |
+
.mono{{font-family:'Share Tech Mono',monospace;}}
|
| 2183 |
+
.header{{background:var(--surface);border:1px solid var(--border);padding:24px;margin-bottom:24px;}}
|
| 2184 |
+
.header-title{{font-size:22px;color:var(--blue);font-weight:700;margin-bottom:8px;}}
|
| 2185 |
+
.header-meta{{display:flex;gap:24px;flex-wrap:wrap;}}
|
| 2186 |
+
.meta-item{{font-size:13px;color:var(--text-dim);}}
|
| 2187 |
+
.meta-value{{color:var(--text);font-weight:600;}}
|
| 2188 |
+
.step-card{{border-left:3px solid;padding:12px 16px;margin-bottom:8px;border-radius:0 4px 4px 0;}}
|
| 2189 |
+
.step-header{{display:flex;align-items:center;gap:16px;flex-wrap:wrap;margin-bottom:4px;}}
|
| 2190 |
+
.step-num{{font-size:11px;color:var(--text-dim);min-width:60px;}}
|
| 2191 |
+
.step-action{{font-size:13px;color:var(--cyan);flex:1;}}
|
| 2192 |
+
.step-reward{{font-size:14px;font-weight:700;}}
|
| 2193 |
+
.step-running{{font-size:11px;}}
|
| 2194 |
+
.step-rc{{font-size:12px;color:var(--yellow);padding:4px 0;}}
|
| 2195 |
+
.step-obs{{font-size:11px;color:var(--text-dim);margin-top:4px;}}
|
| 2196 |
+
.resolution-banner{{text-align:center;padding:32px;margin-top:24px;border:2px solid;font-size:24px;font-weight:700;}}
|
| 2197 |
+
.nav-links{{display:flex;gap:16px;margin-bottom:24px;font-size:13px;}}
|
| 2198 |
+
.nav-links a{{color:var(--blue);text-decoration:none;}}
|
| 2199 |
+
.nav-links a:hover{{color:var(--cyan);}}
|
| 2200 |
+
h2{{font-size:14px;color:var(--text-dim);text-transform:uppercase;letter-spacing:0.1em;margin-bottom:16px;}}
|
| 2201 |
+
</style>
|
| 2202 |
+
</head>
|
| 2203 |
+
<body>
|
| 2204 |
+
<div class="nav-links">
|
| 2205 |
+
<a href="/replays/html">← All Replays</a>
|
| 2206 |
+
<a href="/live">Live NOC</a>
|
| 2207 |
+
<a href="/progress">Progress</a>
|
| 2208 |
+
<a href="/docs">API Docs</a>
|
| 2209 |
+
</div>
|
| 2210 |
+
<div class="header">
|
| 2211 |
+
<div class="header-title">▣ Episode Replay #{episode_id}</div>
|
| 2212 |
+
<div class="header-meta">
|
| 2213 |
+
<div class="meta-item">TASK <span class="meta-value mono">{r['task_id'].upper()}</span></div>
|
| 2214 |
+
<div class="meta-item">SEED <span class="meta-value mono">{r['seed']}</span></div>
|
| 2215 |
+
<div class="meta-item">SCORE <span class="meta-value mono" style="color:{score_col}">{r['final_score']:.3f}</span></div>
|
| 2216 |
+
<div class="meta-item">STEPS <span class="meta-value mono">{r['total_steps']}</span></div>
|
| 2217 |
+
<div class="meta-item">TIME <span class="meta-value mono">{r['timestamp'][:19]}</span></div>
|
| 2218 |
+
</div>
|
| 2219 |
+
</div>
|
| 2220 |
+
<h2>Step Timeline</h2>
|
| 2221 |
+
{steps_html if steps_html else '<div style="color:var(--text-dim);padding:24px">No steps recorded.</div>'}
|
| 2222 |
+
<div class="resolution-banner mono" style="color:{resolved_color};border-color:{resolved_color};background:{'rgba(0,255,136,0.05)' if r['resolved'] else 'rgba(255,51,85,0.05)'}">
|
| 2223 |
+
{resolved_icon} {resolved_text} | Final Score: {r['final_score']:.3f}
|
| 2224 |
+
</div>
|
| 2225 |
+
</body>
|
| 2226 |
+
</html>"""
|
| 2227 |
+
return HTMLResponse(html)
|
| 2228 |
+
|
| 2229 |
+
|
| 2230 |
+
@app.get("/replays/html", response_class=HTMLResponse)
|
| 2231 |
+
def list_replays_html():
|
| 2232 |
+
"""HTML index of all replays."""
|
| 2233 |
+
items = sorted(replay_store.values(), key=lambda x: int(x["episode_id"]), reverse=True)
|
| 2234 |
+
rows = ""
|
| 2235 |
+
for r in items:
|
| 2236 |
+
score_col = "#00ff88" if r["final_score"] >= 0.7 else "#ffaa00" if r["final_score"] >= 0.4 else "#ff3355"
|
| 2237 |
+
resolved_icon = "✓" if r["resolved"] else "✗"
|
| 2238 |
+
rows += f"""<tr>
|
| 2239 |
+
<td class="mono"><a href="/replay/{r['episode_id']}/html" style="color:#4d9fff">#{r['episode_id']}</a></td>
|
| 2240 |
+
<td class="mono">{r['task_id'].upper()}</td>
|
| 2241 |
+
<td class="mono" style="color:{score_col}">{r['final_score']:.3f}</td>
|
| 2242 |
+
<td class="mono" style="color:{'#00ff88' if r['resolved'] else '#ff3355'}">{resolved_icon}</td>
|
| 2243 |
+
<td class="mono">{r['total_steps']}</td>
|
| 2244 |
+
<td class="mono" style="color:#4a6080">{r['timestamp'][:19]}</td>
|
| 2245 |
+
</tr>"""
|
| 2246 |
+
if not rows:
|
| 2247 |
+
rows = '<tr><td colspan="6" style="text-align:center;color:#4a6080;padding:32px">No replays yet. Complete an episode to generate one.</td></tr>'
|
| 2248 |
+
html = f"""<!DOCTYPE html>
|
| 2249 |
+
<html lang="en">
|
| 2250 |
+
<head>
|
| 2251 |
+
<meta charset="UTF-8">
|
| 2252 |
+
<title>ARIA Episode Replays</title>
|
| 2253 |
+
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;600;700&family=Share+Tech+Mono&display=swap" rel="stylesheet">
|
| 2254 |
+
<style>
|
| 2255 |
+
:root{{--bg:#060914;--surface:#0a0f1e;--border:#1a2744;--blue:#4d9fff;--text:#c8d8f0;--text-dim:#4a6080;}}
|
| 2256 |
+
*{{box-sizing:border-box;margin:0;padding:0;}}
|
| 2257 |
+
body{{background:var(--bg);color:var(--text);font-family:'Inter',sans-serif;padding:32px;max-width:900px;margin:0 auto;}}
|
| 2258 |
+
.mono{{font-family:'Share Tech Mono',monospace;}}
|
| 2259 |
+
h1{{font-size:22px;color:var(--blue);margin-bottom:24px;}}
|
| 2260 |
+
table{{width:100%;border-collapse:collapse;}}
|
| 2261 |
+
th{{text-align:left;font-size:11px;color:var(--text-dim);text-transform:uppercase;padding:8px 12px;border-bottom:1px solid var(--border);}}
|
| 2262 |
+
td{{padding:10px 12px;border-bottom:1px solid rgba(26,39,68,0.5);font-size:13px;}}
|
| 2263 |
+
tr:hover td{{background:rgba(77,159,255,0.04);}}
|
| 2264 |
+
.nav-links{{display:flex;gap:16px;margin-bottom:24px;font-size:13px;}}
|
| 2265 |
+
.nav-links a{{color:var(--blue);text-decoration:none;}}
|
| 2266 |
+
</style>
|
| 2267 |
+
</head>
|
| 2268 |
+
<body>
|
| 2269 |
+
<div class="nav-links"><a href="/live">Live NOC</a><a href="/progress">Progress</a><a href="/challenge">Challenge</a></div>
|
| 2270 |
+
<h1>▣ Episode Replays</h1>
|
| 2271 |
+
<table>
|
| 2272 |
+
<thead><tr><th>ID</th><th>Task</th><th>Score</th><th>Resolved</th><th>Steps</th><th>Time</th></tr></thead>
|
| 2273 |
+
<tbody>{rows}</tbody>
|
| 2274 |
+
</table>
|
| 2275 |
+
</body>
|
| 2276 |
+
</html>"""
|
| 2277 |
+
return HTMLResponse(html)
|
| 2278 |
+
|
| 2279 |
+
|
| 2280 |
+
# βββ Feature 2: Human vs Agent Challenge βββββββββββββββββββββββββββββββββββββ
|
| 2281 |
+
|
| 2282 |
+
@app.get("/challenge", response_class=HTMLResponse)
|
| 2283 |
+
async def challenge_page():
|
| 2284 |
+
"""Human-playable incident response challenge."""
|
| 2285 |
+
html = """<!DOCTYPE html>
|
| 2286 |
+
<html lang="en">
|
| 2287 |
+
<head>
|
| 2288 |
+
<meta charset="UTF-8">
|
| 2289 |
+
<title>ARIA Human Challenge</title>
|
| 2290 |
+
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;600;700&family=Share+Tech+Mono&display=swap" rel="stylesheet">
|
| 2291 |
+
<style>
|
| 2292 |
+
:root {
|
| 2293 |
+
--bg:#060914;--surface:#0a0f1e;--surface2:#0d1628;--border:#1a2744;
|
| 2294 |
+
--border-bright:#2a4080;--blue:#4d9fff;--cyan:#00d4ff;--green:#00ff88;
|
| 2295 |
+
--yellow:#ffaa00;--red:#ff3355;--purple:#9d4edd;--text:#c8d8f0;--text-dim:#4a6080;
|
| 2296 |
+
}
|
| 2297 |
+
*{box-sizing:border-box;margin:0;padding:0;}
|
| 2298 |
+
body{background:var(--bg);color:var(--text);font-family:'Inter',sans-serif;min-height:100vh;}
|
| 2299 |
+
.mono{font-family:'Share Tech Mono',monospace;}
|
| 2300 |
+
.nav{background:#000;border-bottom:1px solid var(--border);padding:0 24px;height:48px;display:flex;align-items:center;justify-content:space-between;}
|
| 2301 |
+
.nav-logo{font-size:16px;color:var(--blue);font-weight:700;}
|
| 2302 |
+
.nav-links{display:flex;gap:16px;font-size:12px;}
|
| 2303 |
+
.nav-links a{color:var(--text-dim);text-decoration:none;}
|
| 2304 |
+
.nav-links a:hover{color:var(--blue);}
|
| 2305 |
+
.warning{background:rgba(255,170,0,0.1);border-bottom:1px solid var(--yellow);padding:8px 24px;font-size:12px;color:var(--yellow);text-align:center;}
|
| 2306 |
+
.main{display:grid;grid-template-columns:1fr 380px;height:calc(100vh - 80px);}
|
| 2307 |
+
.left{padding:20px;overflow-y:auto;border-right:1px solid var(--border);}
|
| 2308 |
+
.right{padding:20px;overflow-y:auto;background:var(--surface);}
|
| 2309 |
+
.panel{background:var(--surface2);border:1px solid var(--border);padding:16px;margin-bottom:16px;}
|
| 2310 |
+
.panel-title{font-size:10px;color:var(--text-dim);text-transform:uppercase;letter-spacing:0.1em;margin-bottom:12px;}
|
| 2311 |
+
table{width:100%;border-collapse:collapse;font-size:12px;}
|
| 2312 |
+
th{text-align:left;color:var(--text-dim);font-size:10px;text-transform:uppercase;padding:4px 8px;border-bottom:1px solid var(--border);}
|
| 2313 |
+
td{padding:6px 8px;font-family:'Share Tech Mono',monospace;font-size:12px;border-bottom:1px solid rgba(26,39,68,0.5);}
|
| 2314 |
+
.status-down{color:var(--red);}
|
| 2315 |
+
.status-degraded{color:var(--yellow);}
|
| 2316 |
+
.status-healthy{color:var(--green);}
|
| 2317 |
+
.alert-item{padding:6px 10px;border-left:3px solid;margin-bottom:6px;font-size:12px;}
|
| 2318 |
+
.sev-critical{border-color:var(--red);background:rgba(255,51,85,0.08);}
|
| 2319 |
+
.sev-high{border-color:#ff6600;background:rgba(255,102,0,0.08);}
|
| 2320 |
+
.sev-warning{border-color:var(--yellow);background:rgba(255,170,0,0.08);}
|
| 2321 |
+
.sev-info{border-color:var(--blue);background:rgba(77,159,255,0.08);}
|
| 2322 |
+
.evidence-item{font-size:11px;color:var(--text-dim);padding:4px 0;border-bottom:1px solid rgba(26,39,68,0.3);}
|
| 2323 |
+
.form-row{margin-bottom:12px;}
|
| 2324 |
+
label{display:block;font-size:11px;color:var(--text-dim);text-transform:uppercase;margin-bottom:4px;}
|
| 2325 |
+
select,input[type=text]{width:100%;background:var(--surface);border:1px solid var(--border-bright);color:var(--text);font-family:'Share Tech Mono',monospace;font-size:13px;padding:8px 10px;outline:none;}
|
| 2326 |
+
select:focus,input[type=text]:focus{border-color:var(--blue);}
|
| 2327 |
+
.btn-action{width:100%;background:rgba(77,159,255,0.15);border:1px solid var(--blue);color:var(--blue);font-family:'Share Tech Mono',monospace;font-size:14px;padding:12px;cursor:pointer;transition:0.2s;margin-top:8px;text-transform:uppercase;letter-spacing:0.1em;}
|
| 2328 |
+
.btn-action:hover{background:var(--blue);color:#000;}
|
| 2329 |
+
.btn-action:disabled{opacity:0.4;cursor:not-allowed;}
|
| 2330 |
+
.btn-reset{background:rgba(255,51,85,0.1);border:1px solid var(--red);color:var(--red);}
|
| 2331 |
+
.btn-reset:hover{background:var(--red);color:#000;}
|
| 2332 |
+
.score-header{display:flex;justify-content:space-between;align-items:center;margin-bottom:16px;}
|
| 2333 |
+
.big-score{font-size:36px;font-weight:700;font-family:'Share Tech Mono',monospace;}
|
| 2334 |
+
.step-info{font-size:12px;color:var(--text-dim);}
|
| 2335 |
+
.reward-flash{padding:8px 12px;margin-bottom:8px;font-size:14px;font-family:'Share Tech Mono',monospace;border-left:3px solid;}
|
| 2336 |
+
.result-log{max-height:150px;overflow-y:auto;font-size:11px;color:var(--text-dim);font-family:'Share Tech Mono',monospace;background:var(--surface);padding:8px;margin-top:8px;white-space:pre-wrap;}
|
| 2337 |
+
.comparison{display:grid;grid-template-columns:1fr 1fr;gap:16px;margin-top:16px;}
|
| 2338 |
+
.cmp-card{background:var(--surface2);border:1px solid var(--border);padding:20px;text-align:center;}
|
| 2339 |
+
.cmp-label{font-size:11px;color:var(--text-dim);text-transform:uppercase;margin-bottom:8px;}
|
| 2340 |
+
.cmp-score{font-size:32px;font-weight:700;font-family:'Share Tech Mono',monospace;}
|
| 2341 |
+
.cmp-steps{font-size:12px;color:var(--text-dim);margin-top:4px;}
|
| 2342 |
+
.done-banner{text-align:center;padding:24px;border:2px solid;font-size:20px;font-weight:700;margin-bottom:16px;}
|
| 2343 |
+
::-webkit-scrollbar{width:4px;}::-webkit-scrollbar-thumb{background:var(--border-bright);}
|
| 2344 |
+
</style>
|
| 2345 |
+
</head>
|
| 2346 |
+
<body>
|
| 2347 |
+
<div class="nav">
|
| 2348 |
+
<div class="nav-logo mono">▣ ARIA HUMAN CHALLENGE</div>
|
| 2349 |
+
<div class="nav-links">
|
| 2350 |
+
<a href="/live">Live NOC</a>
|
| 2351 |
+
<a href="/progress">Progress</a>
|
| 2352 |
+
<a href="/replays/html">Replays</a>
|
| 2353 |
+
<a href="/docs">API</a>
|
| 2354 |
+
</div>
|
| 2355 |
+
</div>
|
| 2356 |
+
<div class="warning">⚠ Note: This resets the shared environment — agent runs will be interrupted while you play</div>
|
| 2357 |
+
<div class="main">
|
| 2358 |
+
<div class="left">
|
| 2359 |
+
<div class="panel" id="task-desc-panel" style="display:none">
|
| 2360 |
+
<div class="panel-title">► Task Description</div>
|
| 2361 |
+
<div id="task-desc" style="font-size:13px;color:#8ab4d4"></div>
|
| 2362 |
+
</div>
|
| 2363 |
+
<div class="panel">
|
| 2364 |
+
<div class="panel-title">► Infrastructure Status</div>
|
| 2365 |
+
<table>
|
| 2366 |
+
<thead><tr><th>Service</th><th>Status</th><th>CPU%</th><th>Mem%</th><th>Err/s</th><th>P99ms</th></tr></thead>
|
| 2367 |
+
<tbody id="svc-tbody"><tr><td colspan="6" style="color:var(--text-dim);padding:16px">Loading...</td></tr></tbody>
|
| 2368 |
+
</table>
|
| 2369 |
+
</div>
|
| 2370 |
+
<div class="panel">
|
| 2371 |
+
<div class="panel-title">► Active Alerts <span id="alert-count" style="color:var(--red)"></span></div>
|
| 2372 |
+
<div id="alerts-container"><div style="color:var(--text-dim);font-size:12px">No alerts</div></div>
|
| 2373 |
+
</div>
|
| 2374 |
+
<div class="panel">
|
| 2375 |
+
<div class="panel-title">► Evidence Gathered <span id="evidence-count" style="color:var(--blue)"></span></div>
|
| 2376 |
+
<div id="evidence-container"><div style="color:var(--text-dim);font-size:12px">No evidence yet</div></div>
|
| 2377 |
+
</div>
|
| 2378 |
+
<div id="done-panel" style="display:none">
|
| 2379 |
+
<div class="done-banner mono" id="done-banner"></div>
|
| 2380 |
+
<div class="comparison">
|
| 2381 |
+
<div class="cmp-card">
|
| 2382 |
+
<div class="cmp-label">Your Score</div>
|
| 2383 |
+
<div class="cmp-score" id="human-score">—</div>
|
| 2384 |
+
<div class="cmp-steps" id="human-steps"></div>
|
| 2385 |
+
</div>
|
| 2386 |
+
<div class="cmp-card">
|
| 2387 |
+
<div class="cmp-label">Trained Agent (Llama-3.1-8B)</div>
|
| 2388 |
+
<div class="cmp-score" id="agent-score">—</div>
|
| 2389 |
+
<div class="cmp-steps">from /metrics avg</div>
|
| 2390 |
+
</div>
|
| 2391 |
+
</div>
|
| 2392 |
+
<button class="btn-action" onclick="startGame(true)" style="margin-top:16px;width:100%">▶ Play Again (Random Seed)</button>
|
| 2393 |
+
</div>
|
| 2394 |
+
</div>
|
| 2395 |
+
<div class="right">
|
| 2396 |
+
<div class="score-header">
|
| 2397 |
+
<div>
|
| 2398 |
+
<div class="panel-title">Episode Progress</div>
|
| 2399 |
+
<div class="big-score mono" id="score-display" style="color:var(--text-dim)">0.000</div>
|
| 2400 |
+
</div>
|
| 2401 |
+
<div style="text-align:right">
|
| 2402 |
+
<div class="step-info mono" id="step-display">Step 0 / 15</div>
|
| 2403 |
+
<div class="step-info mono" id="task-display" style="color:var(--blue)">EASY</div>
|
| 2404 |
+
</div>
|
| 2405 |
+
</div>
|
| 2406 |
+
<div class="panel">
|
| 2407 |
+
<div class="panel-title">► Take Action</div>
|
| 2408 |
+
<div class="form-row">
|
| 2409 |
+
<label>Action Type</label>
|
| 2410 |
+
<select id="action-type" onchange="onActionTypeChange()">
|
| 2411 |
+
<option value="read_logs">read_logs — Read full log output</option>
|
| 2412 |
+
<option value="search_logs">search_logs — Search logs for keyword</option>
|
| 2413 |
+
<option value="read_metrics">read_metrics — Read CPU/mem/latency</option>
|
| 2414 |
+
<option value="read_runbook">read_runbook — Open a runbook</option>
|
| 2415 |
+
<option value="diagnose">diagnose — Submit root cause diagnosis</option>
|
| 2416 |
+
<option value="restart_service">restart_service — Restart a crashed service</option>
|
| 2417 |
+
<option value="rollback">rollback — Roll back a deployment</option>
|
| 2418 |
+
<option value="scale_up">scale_up — Scale up replicas</option>
|
| 2419 |
+
<option value="alert_oncall">alert_oncall — Page on-call engineer</option>
|
| 2420 |
+
<option value="acknowledge">acknowledge — Acknowledge an alert</option>
|
| 2421 |
+
<option value="block_ip_range">block_ip_range — Block a CIDR range</option>
|
| 2422 |
+
<option value="create_index">create_index — Create a DB index</option>
|
| 2423 |
+
<option value="failover">failover — Regional failover</option>
|
| 2424 |
+
<option value="noop">noop — No action</option>
|
| 2425 |
+
</select>
|
| 2426 |
+
</div>
|
| 2427 |
+
<div class="form-row" id="svc-row">
|
| 2428 |
+
<label>Service Name</label>
|
| 2429 |
+
<input type="text" id="service-input" placeholder="e.g. payment-service">
|
| 2430 |
+
</div>
|
| 2431 |
+
<div class="form-row" id="rc-row" style="display:none">
|
| 2432 |
+
<label>Root Cause (for diagnose)</label>
|
| 2433 |
+
<input type="text" id="rc-input" placeholder="e.g. Memory leak in payment-service causing OOM">
|
| 2434 |
+
</div>
|
| 2435 |
+
<div class="form-row" id="query-row" style="display:none">
|
| 2436 |
+
<label>Search Query</label>
|
| 2437 |
+
<input type="text" id="query-input" placeholder="e.g. OOM">
|
| 2438 |
+
</div>
|
| 2439 |
+
<div class="form-row" id="rb-row" style="display:none">
|
| 2440 |
+
<label>Runbook File</label>
|
| 2441 |
+
<input type="text" id="rb-input" placeholder="e.g. memory_leak.md">
|
| 2442 |
+
</div>
|
| 2443 |
+
<div class="form-row" id="ip-row" style="display:none">
|
| 2444 |
+
<label>IP Range</label>
|
| 2445 |
+
<input type="text" id="ip-input" placeholder="e.g. 185.220.0.0/16">
|
| 2446 |
+
</div>
|
| 2447 |
+
<div class="form-row" id="reason-row" style="display:none">
|
| 2448 |
+
<label>Reason (for alert_oncall)</label>
|
| 2449 |
+
<input type="text" id="reason-input" placeholder="e.g. Data corruption detected">
|
| 2450 |
+
</div>
|
| 2451 |
+
<button class="btn-action" id="action-btn" onclick="takeAction()">▶ TAKE ACTION</button>
|
| 2452 |
+
<div id="reward-flash" style="display:none" class="reward-flash"></div>
|
| 2453 |
+
<div id="result-log" class="result-log" style="display:none"></div>
|
| 2454 |
+
</div>
|
| 2455 |
+
<div class="panel">
|
| 2456 |
+
<div class="panel-title">► Reward History</div>
|
| 2457 |
+
<div id="reward-history" class="mono" style="font-size:11px;max-height:120px;overflow-y:auto"></div>
|
| 2458 |
+
</div>
|
| 2459 |
+
<button class="btn-action btn-reset" onclick="startGame(true)" style="margin-top:8px">↻ New Incident (Random Seed)</button>
|
| 2460 |
+
</div>
|
| 2461 |
+
</div>
|
| 2462 |
+
<script>
|
| 2463 |
+
let isDone = false;
|
| 2464 |
+
let currentSeed = 1234;
|
| 2465 |
+
let stepCount = 0;
|
| 2466 |
+
let totalScore = 0;
|
| 2467 |
+
let maxSteps = 15;
|
| 2468 |
+
|
| 2469 |
+
async function startGame(randomSeed) {
|
| 2470 |
+
currentSeed = randomSeed ? Math.floor(Math.random() * 99999) : 1234;
|
| 2471 |
+
stepCount = 0; totalScore = 0; isDone = false;
|
| 2472 |
+
document.getElementById('done-panel').style.display = 'none';
|
| 2473 |
+
document.getElementById('reward-history').innerHTML = '';
|
| 2474 |
+
document.getElementById('result-log').style.display = 'none';
|
| 2475 |
+
document.getElementById('reward-flash').style.display = 'none';
|
| 2476 |
+
document.getElementById('action-btn').disabled = false;
|
| 2477 |
+
const res = await fetch('/reset', {
|
| 2478 |
+
method: 'POST', headers: {'Content-Type': 'application/json'},
|
| 2479 |
+
body: JSON.stringify({task_id: 'easy', seed: currentSeed})
|
| 2480 |
+
});
|
| 2481 |
+
const obs = await res.json();
|
| 2482 |
+
maxSteps = obs.max_steps || 15;
|
| 2483 |
+
renderObs(obs);
|
| 2484 |
+
document.getElementById('step-display').textContent = 'Step 0 / ' + maxSteps;
|
| 2485 |
+
document.getElementById('task-display').textContent = (obs.task_id || 'easy').toUpperCase() + ' \\u00b7 SEED ' + currentSeed;
|
| 2486 |
+
if (obs.task_description) {
|
| 2487 |
+
document.getElementById('task-desc').textContent = obs.task_description;
|
| 2488 |
+
document.getElementById('task-desc-panel').style.display = 'block';
|
| 2489 |
+
}
|
| 2490 |
+
updateScoreDisplay(0);
|
| 2491 |
+
}
|
| 2492 |
+
|
| 2493 |
+
function onActionTypeChange() {
|
| 2494 |
+
const at = document.getElementById('action-type').value;
|
| 2495 |
+
document.getElementById('rc-row').style.display = at === 'diagnose' ? 'block' : 'none';
|
| 2496 |
+
document.getElementById('query-row').style.display = at === 'search_logs' ? 'block' : 'none';
|
| 2497 |
+
document.getElementById('rb-row').style.display = at === 'read_runbook' ? 'block' : 'none';
|
| 2498 |
+
document.getElementById('ip-row').style.display = at === 'block_ip_range' ? 'block' : 'none';
|
| 2499 |
+
document.getElementById('reason-row').style.display = at === 'alert_oncall' ? 'block' : 'none';
|
| 2500 |
+
document.getElementById('svc-row').style.display = ['read_runbook','block_ip_range','noop'].includes(at) ? 'none' : 'block';
|
| 2501 |
+
}
|
| 2502 |
+
|
| 2503 |
+
async function takeAction() {
|
| 2504 |
+
if (isDone) return;
|
| 2505 |
+
const at = document.getElementById('action-type').value;
|
| 2506 |
+
const svc = document.getElementById('service-input').value.trim() || null;
|
| 2507 |
+
const body = {action_type: at};
|
| 2508 |
+
if (svc) body.service = svc;
|
| 2509 |
+
if (at === 'diagnose') body.root_cause = document.getElementById('rc-input').value.trim();
|
| 2510 |
+
if (at === 'search_logs') body.query = document.getElementById('query-input').value.trim();
|
| 2511 |
+
if (at === 'read_runbook') body.runbook = document.getElementById('rb-input').value.trim();
|
| 2512 |
+
if (at === 'block_ip_range') body.ip_range = document.getElementById('ip-input').value.trim();
|
| 2513 |
+
if (at === 'alert_oncall') body.reason = document.getElementById('reason-input').value.trim();
|
| 2514 |
+
document.getElementById('action-btn').disabled = true;
|
| 2515 |
+
const res = await fetch('/step', {
|
| 2516 |
+
method: 'POST', headers: {'Content-Type': 'application/json'},
|
| 2517 |
+
body: JSON.stringify(body)
|
| 2518 |
+
});
|
| 2519 |
+
const data = await res.json();
|
| 2520 |
+
document.getElementById('action-btn').disabled = isDone ? true : false;
|
| 2521 |
+
const reward = data.reward || 0;
|
| 2522 |
+
totalScore += reward; stepCount += 1;
|
| 2523 |
+
renderObs(data.observation);
|
| 2524 |
+
updateScoreDisplay(totalScore);
|
| 2525 |
+
document.getElementById('step-display').textContent = 'Step ' + stepCount + ' / ' + maxSteps;
|
| 2526 |
+
showReward(reward, data.observation && data.observation.last_action_result);
|
| 2527 |
+
appendRewardHistory(stepCount, at, reward);
|
| 2528 |
+
if (data.done) { isDone = true; document.getElementById('action-btn').disabled = true; showDonePanel(totalScore, stepCount, data.observation && data.observation.incident_resolved); }
|
| 2529 |
+
}
|
| 2530 |
+
|
| 2531 |
+
function renderObs(obs) {
|
| 2532 |
+
if (!obs) return;
|
| 2533 |
+
if (obs.services) {
|
| 2534 |
+
document.getElementById('svc-tbody').innerHTML = obs.services.map(s => {
|
| 2535 |
+
const sc = s.status === 'down' ? 'status-down' : s.status === 'degraded' ? 'status-degraded' : 'status-healthy';
|
| 2536 |
+
return '<tr><td>' + s.name + '</td><td class="' + sc + '">' + s.status.toUpperCase() + '</td><td>' + (s.cpu_percent||0).toFixed(1) + '</td><td>' + (s.memory_percent||0).toFixed(1) + '</td><td>' + (s.error_rate||0).toFixed(3) + '</td><td>' + (s.latency_p99_ms||0).toFixed(0) + '</td></tr>';
|
| 2537 |
+
}).join('');
|
| 2538 |
+
}
|
| 2539 |
+
const ac = document.getElementById('alerts-container');
|
| 2540 |
+
if (obs.active_alerts && obs.active_alerts.length > 0) {
|
| 2541 |
+
document.getElementById('alert-count').textContent = '(' + obs.active_alerts.length + ')';
|
| 2542 |
+
ac.innerHTML = obs.active_alerts.map(a => {
|
| 2543 |
+
const sev = (a.severity||'INFO').toLowerCase();
|
| 2544 |
+
const cls = sev === 'critical' ? 'sev-critical' : sev === 'high' ? 'sev-high' : sev === 'warning' ? 'sev-warning' : 'sev-info';
|
| 2545 |
+
return '<div class="alert-item ' + cls + ' mono"><strong>' + a.severity + '</strong> [' + a.service + '] ' + a.message + '</div>';
|
| 2546 |
+
}).join('');
|
| 2547 |
+
} else { document.getElementById('alert-count').textContent = ''; ac.innerHTML = '<div style="color:var(--text-dim);font-size:12px">No active alerts</div>'; }
|
| 2548 |
+
const ec = document.getElementById('evidence-container');
|
| 2549 |
+
if (obs.evidence_log && obs.evidence_log.length > 0) {
|
| 2550 |
+
document.getElementById('evidence-count').textContent = '(' + obs.evidence_log.length + ')';
|
| 2551 |
+
ec.innerHTML = obs.evidence_log.slice(-5).map(e => '<div class="evidence-item">► [step ' + e.step + '] ' + e.source + ': ' + e.summary + '</div>').join('');
|
| 2552 |
+
} else { document.getElementById('evidence-count').textContent = ''; ec.innerHTML = '<div style="color:var(--text-dim);font-size:12px">No evidence yet — use read_logs, read_metrics, or search_logs</div>'; }
|
| 2553 |
+
}
|
| 2554 |
+
|
| 2555 |
+
function updateScoreDisplay(score) {
|
| 2556 |
+
const el = document.getElementById('score-display');
|
| 2557 |
+
el.textContent = score.toFixed(3);
|
| 2558 |
+
el.style.color = score >= 0.7 ? 'var(--green)' : score >= 0.4 ? 'var(--yellow)' : score > 0 ? 'var(--red)' : 'var(--text-dim)';
|
| 2559 |
+
}
|
| 2560 |
+
|
| 2561 |
+
function showReward(reward, resultText) {
|
| 2562 |
+
const el = document.getElementById('reward-flash');
|
| 2563 |
+
const sign = reward > 0 ? '+' : '';
|
| 2564 |
+
el.style.display = 'block'; el.style.borderColor = reward > 0 ? 'var(--green)' : 'var(--red)';
|
| 2565 |
+
el.style.color = reward > 0 ? 'var(--green)' : 'var(--red)';
|
| 2566 |
+
el.style.background = reward > 0 ? 'rgba(0,255,136,0.08)' : 'rgba(255,51,85,0.08)';
|
| 2567 |
+
el.textContent = 'Reward: ' + sign + reward.toFixed(3);
|
| 2568 |
+
if (resultText) { const rl = document.getElementById('result-log'); rl.style.display = 'block'; rl.textContent = resultText.substring(0, 500); }
|
| 2569 |
+
}
|
| 2570 |
+
|
| 2571 |
+
function appendRewardHistory(step, action, reward) {
|
| 2572 |
+
const el = document.getElementById('reward-history');
|
| 2573 |
+
const sign = reward > 0 ? '+' : '';
|
| 2574 |
+
const col = reward > 0 ? 'var(--green)' : 'var(--red)';
|
| 2575 |
+
el.innerHTML += '<div style="color:' + col + '">S' + step + ' ' + action + ' → ' + sign + reward.toFixed(3) + '</div>';
|
| 2576 |
+
el.scrollTop = el.scrollHeight;
|
| 2577 |
+
}
|
| 2578 |
+
|
| 2579 |
+
async function showDonePanel(score, steps, resolved) {
|
| 2580 |
+
const banner = document.getElementById('done-banner');
|
| 2581 |
+
banner.style.color = resolved ? 'var(--green)' : 'var(--red)';
|
| 2582 |
+
banner.style.borderColor = resolved ? 'var(--green)' : 'var(--red)';
|
| 2583 |
+
banner.style.background = resolved ? 'rgba(0,255,136,0.05)' : 'rgba(255,51,85,0.05)';
|
| 2584 |
+
banner.textContent = (resolved ? '\\u2713 INCIDENT RESOLVED' : '\\u2717 INCIDENT ESCALATED') + ' \\u2014 Score: ' + score.toFixed(3);
|
| 2585 |
+
document.getElementById('human-score').textContent = score.toFixed(3);
|
| 2586 |
+
document.getElementById('human-score').style.color = score >= 0.7 ? 'var(--green)' : score >= 0.4 ? 'var(--yellow)' : 'var(--red)';
|
| 2587 |
+
document.getElementById('human-steps').textContent = steps + ' steps taken';
|
| 2588 |
+
try {
|
| 2589 |
+
const m = await fetch('/metrics'); const mdata = await m.json();
|
| 2590 |
+
const easyData = mdata.by_task && mdata.by_task['easy'];
|
| 2591 |
+
if (easyData) { document.getElementById('agent-score').textContent = easyData.avg_score.toFixed(3); document.getElementById('agent-score').style.color = easyData.avg_score >= 0.7 ? 'var(--green)' : easyData.avg_score >= 0.4 ? 'var(--yellow)' : 'var(--red)'; }
|
| 2592 |
+
else { document.getElementById('agent-score').textContent = 'N/A'; }
|
| 2593 |
+
} catch(e) { document.getElementById('agent-score').textContent = 'N/A'; }
|
| 2594 |
+
document.getElementById('done-panel').style.display = 'block';
|
| 2595 |
+
}
|
| 2596 |
+
|
| 2597 |
+
startGame(false);
|
| 2598 |
+
</script>
|
| 2599 |
+
</body>
|
| 2600 |
+
</html>"""
|
| 2601 |
+
return HTMLResponse(html)
|
| 2602 |
+
|
| 2603 |
+
|
| 2604 |
+
# βββ Feature 3: Progress Visualization βββββββββββββββββββββββββββββββββββββββ
|
| 2605 |
+
|
| 2606 |
+
@app.get("/progress", response_class=HTMLResponse)
|
| 2607 |
+
def progress_page():
|
| 2608 |
+
"""Training progress and live performance visualization."""
|
| 2609 |
+
metrics = get_metrics()
|
| 2610 |
+
by_task = metrics.get("by_task", {})
|
| 2611 |
+
|
| 2612 |
+
BASELINES = {
|
| 2613 |
+
"easy": 0.05, "medium": 0.03, "hard": 0.01,
|
| 2614 |
+
"bonus": 0.01, "security": 0.01, "database": 0.01, "failover": 0.01,
|
| 2615 |
+
}
|
| 2616 |
+
TRAINING_RESULTS = [
|
| 2617 |
+
("easy", 0.320, 0.685),
|
| 2618 |
+
("medium", 0.050, 0.378),
|
| 2619 |
+
("hard", 0.190, 0.869),
|
| 2620 |
+
("bonus", 0.152, 0.682),
|
| 2621 |
+
]
|
| 2622 |
+
ALL_TASKS = ["easy", "medium", "hard", "bonus", "security", "database", "failover"]
|
| 2623 |
+
|
| 2624 |
+
def bar_row(task: str, score: float, baseline: float) -> str:
|
| 2625 |
+
pct = round(score * 100, 1)
|
| 2626 |
+
bpct = round(baseline * 100, 1)
|
| 2627 |
+
col = "#00ff88" if score >= 0.7 else "#ffaa00" if score >= 0.4 else "#ff3355"
|
| 2628 |
+
return f"""
|
| 2629 |
+
<div class="bar-group">
|
| 2630 |
+
<div class="bar-label mono">{task.upper()}</div>
|
| 2631 |
+
<div class="bar-tracks">
|
| 2632 |
+
<div class="bar-track">
|
| 2633 |
+
<span class="bar-tag">RANDOM</span>
|
| 2634 |
+
<div class="bar-bg"><div class="bar-fill" style="width:{bpct}%;background:#4a6080"></div></div>
|
| 2635 |
+
<span class="bar-val mono" style="color:#4a6080">{baseline:.3f}</span>
|
| 2636 |
+
</div>
|
| 2637 |
+
<div class="bar-track">
|
| 2638 |
+
<span class="bar-tag">ARIA</span>
|
| 2639 |
+
<div class="bar-bg"><div class="bar-fill" style="width:{pct}%;background:{col}"></div></div>
|
| 2640 |
+
<span class="bar-val mono" style="color:{col}">{score:.3f}</span>
|
| 2641 |
+
</div>
|
| 2642 |
+
</div>
|
| 2643 |
+
</div>"""
|
| 2644 |
+
|
| 2645 |
+
bars = "".join(
|
| 2646 |
+
bar_row(task, by_task.get(task, {}).get("avg_score", 0.0), BASELINES.get(task, 0.01))
|
| 2647 |
+
for task in ALL_TASKS
|
| 2648 |
+
)
|
| 2649 |
+
|
| 2650 |
+
training_rows = "".join(
|
| 2651 |
+
f"""<tr>
|
| 2652 |
+
<td class="mono">{task.upper()}</td>
|
| 2653 |
+
<td class="mono" style="color:#4a6080">{base:.3f}</td>
|
| 2654 |
+
<td class="mono" style="color:#4d9fff">{finetuned:.3f}</td>
|
| 2655 |
+
<td class="mono" style="color:#00ff88">+{finetuned - base:.3f} ✓</td>
|
| 2656 |
+
</tr>"""
|
| 2657 |
+
for task, base, finetuned in TRAINING_RESULTS
|
| 2658 |
+
)
|
| 2659 |
+
|
| 2660 |
+
recent = list(episode_history)[-20:] if episode_history else []
|
| 2661 |
+
recent_scores_js = str([round(r["final_score"], 3) for r in recent])
|
| 2662 |
+
total_eps = metrics.get("total_episodes", 0)
|
| 2663 |
+
overall = metrics.get("overall_avg_score", 0.0)
|
| 2664 |
+
overall_col = "#00ff88" if overall >= 0.7 else "#ffaa00" if overall >= 0.4 else "#4d9fff"
|
| 2665 |
+
|
| 2666 |
+
html = f"""<!DOCTYPE html>
|
| 2667 |
+
<html lang="en">
|
| 2668 |
+
<head>
|
| 2669 |
+
<meta charset="UTF-8">
|
| 2670 |
+
<title>ARIA Progress</title>
|
| 2671 |
+
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;600;700&family=Share+Tech+Mono&display=swap" rel="stylesheet">
|
| 2672 |
+
<style>
|
| 2673 |
+
:root{{--bg:#060914;--surface:#0a0f1e;--surface2:#0d1628;--border:#1a2744;--blue:#4d9fff;--cyan:#00d4ff;--green:#00ff88;--yellow:#ffaa00;--red:#ff3355;--text:#c8d8f0;--text-dim:#4a6080;}}
|
| 2674 |
+
*{{box-sizing:border-box;margin:0;padding:0;}}
|
| 2675 |
+
body{{background:var(--bg);color:var(--text);font-family:'Inter',sans-serif;}}
|
| 2676 |
+
.mono{{font-family:'Share Tech Mono',monospace;}}
|
| 2677 |
+
.nav{{background:#000;border-bottom:1px solid var(--border);padding:0 32px;height:48px;display:flex;align-items:center;justify-content:space-between;}}
|
| 2678 |
+
.nav-logo{{font-size:16px;color:var(--blue);font-weight:700;}}
|
| 2679 |
+
.nav-links{{display:flex;gap:16px;font-size:12px;}}
|
| 2680 |
+
.nav-links a{{color:var(--text-dim);text-decoration:none;}}
|
| 2681 |
+
.nav-links a:hover{{color:var(--blue);}}
|
| 2682 |
+
.page{{max-width:960px;margin:0 auto;padding:32px;}}
|
| 2683 |
+
h1{{font-size:22px;color:var(--blue);margin-bottom:8px;}}
|
| 2684 |
+
.subtitle{{font-size:13px;color:var(--text-dim);margin-bottom:40px;}}
|
| 2685 |
+
h2{{font-size:13px;color:var(--text-dim);text-transform:uppercase;letter-spacing:0.1em;margin-bottom:20px;padding-bottom:8px;border-bottom:1px solid var(--border);}}
|
| 2686 |
+
section{{margin-bottom:48px;}}
|
| 2687 |
+
.stats-row{{display:flex;gap:24px;margin-bottom:32px;flex-wrap:wrap;}}
|
| 2688 |
+
.stat-card{{background:var(--surface);border:1px solid var(--border);padding:20px 24px;flex:1;min-width:140px;}}
|
| 2689 |
+
.stat-label{{font-size:10px;color:var(--text-dim);text-transform:uppercase;margin-bottom:6px;}}
|
| 2690 |
+
.stat-value{{font-size:28px;font-weight:700;font-family:'Share Tech Mono',monospace;}}
|
| 2691 |
+
.bar-group{{margin-bottom:20px;}}
|
| 2692 |
+
.bar-label{{font-size:11px;color:var(--text-dim);text-transform:uppercase;margin-bottom:8px;letter-spacing:0.08em;}}
|
| 2693 |
+
.bar-tracks{{display:flex;flex-direction:column;gap:6px;}}
|
| 2694 |
+
.bar-track{{display:flex;align-items:center;gap:10px;}}
|
| 2695 |
+
.bar-tag{{font-size:9px;color:var(--text-dim);width:50px;text-align:right;font-family:'Share Tech Mono',monospace;}}
|
| 2696 |
+
.bar-bg{{flex:1;height:12px;background:var(--surface);border:1px solid var(--border);overflow:hidden;}}
|
| 2697 |
+
.bar-fill{{height:100%;transition:width 0.6s ease;}}
|
| 2698 |
+
.bar-val{{font-size:12px;width:50px;text-align:right;}}
|
| 2699 |
+
table{{width:100%;border-collapse:collapse;}}
|
| 2700 |
+
th{{text-align:left;font-size:11px;color:var(--text-dim);text-transform:uppercase;padding:8px 12px;border-bottom:1px solid var(--border);}}
|
| 2701 |
+
td{{padding:10px 12px;border-bottom:1px solid rgba(26,39,68,0.5);}}
|
| 2702 |
+
.canvas-wrap{{background:var(--surface);border:1px solid var(--border);padding:16px;}}
|
| 2703 |
+
canvas{{display:block;width:100%;max-width:880px;}}
|
| 2704 |
+
.canvas-label{{font-size:11px;color:var(--text-dim);margin-top:8px;text-align:center;font-family:'Share Tech Mono',monospace;}}
|
| 2705 |
+
</style>
|
| 2706 |
+
</head>
|
| 2707 |
+
<body>
|
| 2708 |
+
<div class="nav">
|
| 2709 |
+
<div class="nav-logo mono">▣ ARIA PROGRESS</div>
|
| 2710 |
+
<div class="nav-links">
|
| 2711 |
+
<a href="/live">Live NOC</a>
|
| 2712 |
+
<a href="/challenge">Challenge</a>
|
| 2713 |
+
<a href="/replays/html">Replays</a>
|
| 2714 |
+
<a href="/docs">API Docs</a>
|
| 2715 |
+
</div>
|
| 2716 |
+
</div>
|
| 2717 |
+
<div class="page">
|
| 2718 |
+
<h1>Training Progress & Live Performance</h1>
|
| 2719 |
+
<div class="subtitle">Llama-3.1-8B fine-tuned with GRPO on 7 task types · 14 actions · Dense reward shaping</div>
|
| 2720 |
+
<div class="stats-row">
|
| 2721 |
+
<div class="stat-card"><div class="stat-label">Live Episodes</div><div class="stat-value" id="live-eps" style="color:var(--blue)">{total_eps}</div></div>
|
| 2722 |
+
<div class="stat-card"><div class="stat-label">Avg Live Score</div><div class="stat-value" id="live-avg" style="color:{overall_col}">{overall:.3f}</div></div>
|
| 2723 |
+
<div class="stat-card"><div class="stat-label">Training Episodes</div><div class="stat-value" style="color:var(--blue)">160</div></div>
|
| 2724 |
+
<div class="stat-card"><div class="stat-label">Model</div><div class="stat-value" style="font-size:14px;color:var(--cyan)">8B GRPO</div></div>
|
| 2725 |
+
</div>
|
| 2726 |
+
<section>
|
| 2727 |
+
<h2>Section 1 — Live Task Performance vs Random Baseline</h2>
|
| 2728 |
+
{bars}
|
| 2729 |
+
</section>
|
| 2730 |
+
<section>
|
| 2731 |
+
<h2>Section 2 — Training Improvement (Llama-3.1-8B, GRPO)</h2>
|
| 2732 |
+
<table>
|
| 2733 |
+
<thead><tr><th>Task</th><th>Baseline</th><th>Fine-tuned</th><th>Improvement</th></tr></thead>
|
| 2734 |
+
<tbody>{training_rows}</tbody>
|
| 2735 |
+
</table>
|
| 2736 |
+
</section>
|
| 2737 |
+
<section>
|
| 2738 |
+
<h2>Section 3 — Live Episode Score Timeline</h2>
|
| 2739 |
+
<div class="canvas-wrap">
|
| 2740 |
+
<canvas id="timeline-canvas" width="880" height="120"></canvas>
|
| 2741 |
+
<div class="canvas-label mono" id="canvas-label">Last 20 episodes · updates every 30s</div>
|
| 2742 |
+
</div>
|
| 2743 |
+
</section>
|
| 2744 |
+
</div>
|
| 2745 |
+
<script>
|
| 2746 |
+
const SCORES = {recent_scores_js};
|
| 2747 |
+
function drawTimeline(scores) {{
|
| 2748 |
+
const canvas = document.getElementById('timeline-canvas');
|
| 2749 |
+
canvas.width = canvas.parentElement.clientWidth - 32;
|
| 2750 |
+
const ctx = canvas.getContext('2d');
|
| 2751 |
+
const W = canvas.width, H = 120;
|
| 2752 |
+
ctx.clearRect(0, 0, W, H);
|
| 2753 |
+
if (scores.length === 0) {{
|
| 2754 |
+
ctx.fillStyle = '#4a6080'; ctx.font = '13px Share Tech Mono, monospace';
|
| 2755 |
+
ctx.textAlign = 'center'; ctx.fillText('No episodes yet', W/2, H/2); return;
|
| 2756 |
+
}}
|
| 2757 |
+
const pad = 30, plotW = W - pad * 2, plotH = H - pad * 2;
|
| 2758 |
+
ctx.strokeStyle = '#1a2744'; ctx.lineWidth = 1;
|
| 2759 |
+
[0, 0.25, 0.5, 0.75, 1.0].forEach(v => {{
|
| 2760 |
+
const y = pad + plotH - v * plotH;
|
| 2761 |
+
ctx.beginPath(); ctx.moveTo(pad, y); ctx.lineTo(W - pad, y); ctx.stroke();
|
| 2762 |
+
ctx.fillStyle = '#4a6080'; ctx.font = '10px Share Tech Mono, monospace';
|
| 2763 |
+
ctx.textAlign = 'right'; ctx.fillText(v.toFixed(2), pad - 4, y + 3);
|
| 2764 |
+
}});
|
| 2765 |
+
const n = scores.length;
|
| 2766 |
+
const pts = scores.map((s, i) => [pad + (i / Math.max(n-1, 1)) * plotW, pad + plotH - s * plotH]);
|
| 2767 |
+
ctx.strokeStyle = '#4d9fff'; ctx.lineWidth = 2; ctx.lineJoin = 'round';
|
| 2768 |
+
ctx.beginPath(); pts.forEach(([x,y], i) => i === 0 ? ctx.moveTo(x,y) : ctx.lineTo(x,y)); ctx.stroke();
|
| 2769 |
+
pts.forEach(([x,y], i) => {{
|
| 2770 |
+
const s = scores[i];
|
| 2771 |
+
ctx.fillStyle = s >= 0.7 ? '#00ff88' : s >= 0.4 ? '#ffaa00' : '#ff3355';
|
| 2772 |
+
ctx.beginPath(); ctx.arc(x, y, 4, 0, Math.PI*2); ctx.fill();
|
| 2773 |
+
}});
|
| 2774 |
+
}}
|
| 2775 |
+
drawTimeline(SCORES);
|
| 2776 |
+
async function refresh() {{
|
| 2777 |
+
try {{
|
| 2778 |
+
const res = await fetch('/metrics'); const data = await res.json();
|
| 2779 |
+
document.getElementById('live-eps').textContent = data.total_episodes || 0;
|
| 2780 |
+
const avg = data.overall_avg_score || 0;
|
| 2781 |
+
document.getElementById('live-avg').textContent = avg.toFixed(3);
|
| 2782 |
+
}} catch(e) {{}}
|
| 2783 |
+
}}
|
| 2784 |
+
setInterval(refresh, 30000);
|
| 2785 |
+
</script>
|
| 2786 |
+
</body>
|
| 2787 |
+
</html>"""
|
| 2788 |
+
return HTMLResponse(html)
|