Spaces:
Sleeping
Sleeping
| """ | |
| FastAPI application for the hardened NovaTech OpenEnv environment. | |
| """ | |
| from __future__ import annotations | |
| import os | |
| from typing import Any, Dict, Optional | |
| from fastapi import Body, FastAPI, HTTPException, Query, Request | |
| from fastapi.middleware.cors import CORSMiddleware | |
| from fastapi.responses import HTMLResponse, RedirectResponse | |
| from pydantic import BaseModel | |
| import uvicorn | |
| from env.environment import DEBUG_STATE_ENABLED, store | |
| from env.models import Action, Observation, Reward | |
| app = FastAPI( | |
| title="NovaTech Incident Command", | |
| description="Seeded, session-safe OpenEnv environment for incident response under partial observability.", | |
| version="3.0.0", | |
| ) | |
| app.add_middleware(CORSMiddleware, allow_origins=["*"], allow_methods=["*"], allow_headers=["*"]) | |
| class ResetRequest(BaseModel): | |
| task_id: str = "easy" | |
| seed: Optional[int] = None | |
| class StepResponse(BaseModel): | |
| observation: Dict[str, Any] | |
| reward: Dict[str, Any] | |
| done: bool | |
| info: Dict[str, Any] | |
| def _root_payload() -> Dict[str, Any]: | |
| return { | |
| "name": "NovaTech Incident Command", | |
| "version": "3.0.0", | |
| "debug_state_enabled": DEBUG_STATE_ENABLED, | |
| "endpoints": { | |
| "POST /reset": "Create an episode and return the initial observation.", | |
| "POST /step": "Apply an action using a session_id.", | |
| "GET /state": "Return public, non-leaking session state.", | |
| "GET /health": "Liveness probe.", | |
| }, | |
| "action_schema": Action.model_json_schema(), | |
| "observation_schema": Observation.model_json_schema(), | |
| "reward_schema": Reward.model_json_schema(), | |
| } | |
| def root(request: Request): | |
| if "text/html" in (request.headers.get("accept") or "").lower(): | |
| return RedirectResponse(url="/playground", status_code=307) | |
| return _root_payload() | |
| def playground() -> str: | |
| return """ | |
| <!doctype html> | |
| <html> | |
| <head> | |
| <meta charset="utf-8" /> | |
| <meta name="viewport" content="width=device-width, initial-scale=1" /> | |
| <title>NovaTech Incident Command</title> | |
| <style> | |
| :root { | |
| --ink: #f4f1e8; | |
| --muted: #b5c1d1; | |
| --line: rgba(198, 218, 245, 0.14); | |
| --panel: rgba(10, 18, 30, 0.78); | |
| --panel-strong: rgba(8, 14, 25, 0.9); | |
| --card-glow: 0 24px 80px rgba(5, 10, 18, 0.38); | |
| --accent: #d54f36; | |
| --accent-soft: #ff9e7b; | |
| --teal: #3ca7a1; | |
| --gold: #f1c56e; | |
| --ok: #65d197; | |
| --bad: #ff7d7d; | |
| --mono: "IBM Plex Mono", "SFMono-Regular", monospace; | |
| --sans: "Space Grotesk", "Avenir Next", sans-serif; | |
| } | |
| * { box-sizing: border-box; } | |
| html { scroll-behavior: smooth; } | |
| body { | |
| margin: 0; | |
| color: var(--ink); | |
| font-family: var(--sans); | |
| background: | |
| radial-gradient(circle at 15% 20%, rgba(213, 79, 54, 0.24), transparent 24%), | |
| radial-gradient(circle at 82% 10%, rgba(60, 167, 161, 0.24), transparent 22%), | |
| radial-gradient(circle at 50% 100%, rgba(241, 197, 110, 0.12), transparent 34%), | |
| linear-gradient(145deg, #09111d 0%, #0d1626 42%, #101a2d 100%); | |
| min-height: 100vh; | |
| } | |
| .chrome { | |
| position: fixed; | |
| inset: 0; | |
| pointer-events: none; | |
| background-image: | |
| linear-gradient(rgba(255,255,255,0.04) 1px, transparent 1px), | |
| linear-gradient(90deg, rgba(255,255,255,0.04) 1px, transparent 1px); | |
| background-size: 44px 44px; | |
| mask-image: linear-gradient(to bottom, rgba(0,0,0,0.42), rgba(0,0,0,0.1)); | |
| opacity: 0.3; | |
| } | |
| .wrap { max-width: 1400px; margin: 0 auto; padding: 24px 18px 40px; position: relative; z-index: 1; } | |
| .hero { | |
| position: relative; | |
| overflow: hidden; | |
| background: | |
| linear-gradient(135deg, rgba(18, 31, 51, 0.92), rgba(10, 18, 30, 0.84)), | |
| linear-gradient(90deg, rgba(213, 79, 54, 0.16), rgba(60, 167, 161, 0.16)); | |
| border: 1px solid var(--line); | |
| border-radius: 28px; | |
| padding: 28px; | |
| box-shadow: var(--card-glow); | |
| margin-bottom: 18px; | |
| } | |
| .hero::after { | |
| content: ""; | |
| position: absolute; | |
| right: -80px; | |
| top: -80px; | |
| width: 260px; | |
| height: 260px; | |
| border-radius: 50%; | |
| background: radial-gradient(circle, rgba(241, 197, 110, 0.28), transparent 70%); | |
| filter: blur(8px); | |
| } | |
| .hero-top { | |
| display: flex; | |
| align-items: flex-start; | |
| justify-content: space-between; | |
| gap: 18px; | |
| flex-wrap: wrap; | |
| } | |
| .eyebrow { | |
| display: inline-flex; | |
| align-items: center; | |
| gap: 10px; | |
| border: 1px solid rgba(241, 197, 110, 0.25); | |
| border-radius: 999px; | |
| padding: 7px 12px; | |
| color: var(--gold); | |
| font-size: 0.77rem; | |
| letter-spacing: 0.12em; | |
| text-transform: uppercase; | |
| background: rgba(241, 197, 110, 0.08); | |
| margin-bottom: 14px; | |
| } | |
| .hero h1 { | |
| margin: 0; | |
| font-size: clamp(2rem, 4vw, 3.35rem); | |
| line-height: 0.96; | |
| letter-spacing: -0.04em; | |
| max-width: 720px; | |
| } | |
| .hero p { | |
| margin: 16px 0 0; | |
| max-width: 760px; | |
| color: var(--muted); | |
| font-size: 1.04rem; | |
| line-height: 1.55; | |
| } | |
| .hero-statbar { | |
| display: grid; | |
| grid-template-columns: repeat(3, minmax(110px, 1fr)); | |
| gap: 10px; | |
| min-width: 320px; | |
| } | |
| .hero-stat { | |
| border: 1px solid var(--line); | |
| border-radius: 18px; | |
| padding: 14px; | |
| background: rgba(255,255,255,0.04); | |
| backdrop-filter: blur(12px); | |
| } | |
| .hero-stat .label { | |
| color: var(--muted); | |
| text-transform: uppercase; | |
| font-size: 0.72rem; | |
| letter-spacing: 0.09em; | |
| } | |
| .hero-stat .value { | |
| margin-top: 8px; | |
| font-size: 1.18rem; | |
| font-weight: 700; | |
| } | |
| .dashboard { | |
| display: grid; | |
| grid-template-columns: 380px 1.05fr 0.8fr; | |
| gap: 16px; | |
| align-items: start; | |
| } | |
| .panel { | |
| background: linear-gradient(180deg, rgba(14, 23, 37, 0.92), rgba(8, 14, 25, 0.92)); | |
| border: 1px solid var(--line); | |
| border-radius: 24px; | |
| box-shadow: var(--card-glow); | |
| overflow: hidden; | |
| } | |
| .panel-head { | |
| display: flex; | |
| align-items: center; | |
| justify-content: space-between; | |
| gap: 12px; | |
| padding: 18px 18px 0; | |
| } | |
| .panel-title { | |
| margin: 0; | |
| font-size: 1rem; | |
| letter-spacing: 0.02em; | |
| } | |
| .panel-subtitle { | |
| margin: 6px 18px 0; | |
| color: var(--muted); | |
| font-size: 0.9rem; | |
| line-height: 1.45; | |
| } | |
| .panel-body { padding: 18px; } | |
| .stack { display: grid; gap: 14px; } | |
| .field label, .group-label { | |
| display: block; | |
| margin-bottom: 8px; | |
| color: var(--muted); | |
| text-transform: uppercase; | |
| font-size: 0.72rem; | |
| letter-spacing: 0.08em; | |
| } | |
| input, select, textarea, button { | |
| width: 100%; | |
| border-radius: 16px; | |
| border: 1px solid rgba(196, 217, 245, 0.12); | |
| background: rgba(255,255,255,0.05); | |
| color: var(--ink); | |
| padding: 12px 14px; | |
| font: inherit; | |
| transition: border-color 0.18s ease, background 0.18s ease, transform 0.18s ease; | |
| } | |
| input::placeholder, textarea::placeholder { color: rgba(181, 193, 209, 0.65); } | |
| input:focus, select:focus, textarea:focus { | |
| outline: none; | |
| border-color: rgba(241, 197, 110, 0.55); | |
| background: rgba(255,255,255,0.08); | |
| } | |
| textarea { | |
| min-height: 250px; | |
| resize: vertical; | |
| font-family: var(--mono); | |
| font-size: 0.92rem; | |
| line-height: 1.5; | |
| } | |
| button { | |
| cursor: pointer; | |
| border: 0; | |
| font-weight: 700; | |
| letter-spacing: 0.01em; | |
| background: linear-gradient(135deg, var(--accent), #ef7a59); | |
| box-shadow: 0 12px 24px rgba(213, 79, 54, 0.24); | |
| } | |
| button:hover { | |
| transform: translateY(-1px); | |
| filter: brightness(1.03); | |
| } | |
| .button-secondary { | |
| background: linear-gradient(135deg, #1a6374, #2d8896); | |
| box-shadow: 0 12px 24px rgba(45, 136, 150, 0.2); | |
| } | |
| .button-ghost { | |
| background: rgba(255,255,255,0.06); | |
| border: 1px solid rgba(196, 217, 245, 0.12); | |
| box-shadow: none; | |
| } | |
| .button-grid { | |
| display: grid; | |
| grid-template-columns: 1fr 1fr; | |
| gap: 10px; | |
| } | |
| .status { | |
| min-height: 48px; | |
| border-radius: 18px; | |
| border: 1px solid rgba(196, 217, 245, 0.12); | |
| background: rgba(255,255,255,0.04); | |
| padding: 12px 14px; | |
| color: var(--muted); | |
| line-height: 1.5; | |
| } | |
| .status.ok { color: var(--ok); border-color: rgba(101, 209, 151, 0.2); } | |
| .status.bad { color: var(--bad); border-color: rgba(255, 125, 125, 0.22); } | |
| .chips { | |
| display: flex; | |
| gap: 8px; | |
| flex-wrap: wrap; | |
| } | |
| .chip { | |
| display: inline-flex; | |
| align-items: center; | |
| gap: 8px; | |
| padding: 8px 11px; | |
| border-radius: 999px; | |
| border: 1px solid rgba(196, 217, 245, 0.12); | |
| background: rgba(255,255,255,0.04); | |
| color: var(--muted); | |
| font-size: 0.82rem; | |
| } | |
| .kpis { | |
| display: grid; | |
| grid-template-columns: repeat(2, minmax(0, 1fr)); | |
| gap: 10px; | |
| } | |
| .kpi { | |
| border-radius: 18px; | |
| border: 1px solid rgba(196, 217, 245, 0.12); | |
| background: rgba(255,255,255,0.035); | |
| padding: 14px; | |
| } | |
| .kpi .label { | |
| color: var(--muted); | |
| font-size: 0.72rem; | |
| text-transform: uppercase; | |
| letter-spacing: 0.08em; | |
| } | |
| .kpi .value { | |
| margin-top: 8px; | |
| font-size: 1.05rem; | |
| font-weight: 700; | |
| word-break: break-word; | |
| } | |
| .template-grid { | |
| display: grid; | |
| gap: 8px; | |
| } | |
| .template { | |
| text-align: left; | |
| padding: 12px 13px; | |
| border-radius: 16px; | |
| background: rgba(255,255,255,0.045); | |
| border: 1px solid rgba(196, 217, 245, 0.1); | |
| color: var(--ink); | |
| font-size: 0.92rem; | |
| box-shadow: none; | |
| } | |
| .template strong { | |
| display: block; | |
| margin-bottom: 4px; | |
| font-size: 0.9rem; | |
| } | |
| .template span { | |
| color: var(--muted); | |
| font-size: 0.82rem; | |
| line-height: 1.45; | |
| } | |
| .viewer-tabs { | |
| display: flex; | |
| gap: 8px; | |
| margin-bottom: 12px; | |
| } | |
| .tab { | |
| width: auto; | |
| padding: 10px 14px; | |
| border-radius: 999px; | |
| background: rgba(255,255,255,0.05); | |
| box-shadow: none; | |
| font-size: 0.86rem; | |
| } | |
| .tab.active { | |
| background: linear-gradient(135deg, rgba(241, 197, 110, 0.18), rgba(213, 79, 54, 0.2)); | |
| border: 1px solid rgba(241, 197, 110, 0.28); | |
| } | |
| .viewer { | |
| min-height: 620px; | |
| border-radius: 20px; | |
| background: linear-gradient(180deg, #0d1626, #0b1220); | |
| border: 1px solid rgba(196, 217, 245, 0.1); | |
| overflow: hidden; | |
| } | |
| pre { | |
| margin: 0; | |
| min-height: 620px; | |
| padding: 18px; | |
| overflow: auto; | |
| white-space: pre-wrap; | |
| word-break: break-word; | |
| color: #e7efff; | |
| font-family: var(--mono); | |
| font-size: 0.9rem; | |
| line-height: 1.58; | |
| } | |
| .hidden { display: none; } | |
| .brief { | |
| display: grid; | |
| gap: 12px; | |
| } | |
| .brief-card { | |
| border-radius: 18px; | |
| border: 1px solid rgba(196, 217, 245, 0.1); | |
| background: rgba(255,255,255,0.04); | |
| padding: 14px; | |
| } | |
| .brief-card h3 { | |
| margin: 0 0 8px; | |
| font-size: 0.86rem; | |
| text-transform: uppercase; | |
| letter-spacing: 0.08em; | |
| color: var(--gold); | |
| } | |
| .brief-card p, .brief-card ul { | |
| margin: 0; | |
| color: var(--muted); | |
| line-height: 1.55; | |
| font-size: 0.92rem; | |
| } | |
| .brief-card ul { | |
| padding-left: 18px; | |
| } | |
| .brief-card li + li { margin-top: 6px; } | |
| .footer-note { | |
| margin-top: 10px; | |
| color: rgba(181, 193, 209, 0.66); | |
| font-size: 0.78rem; | |
| line-height: 1.5; | |
| } | |
| @media (max-width: 1240px) { | |
| .dashboard { grid-template-columns: 360px 1fr; } | |
| .sidebar-right { grid-column: 1 / -1; } | |
| } | |
| @media (max-width: 900px) { | |
| .wrap { padding: 18px 14px 28px; } | |
| .dashboard { grid-template-columns: 1fr; } | |
| .hero-top { flex-direction: column; } | |
| .hero-statbar { width: 100%; min-width: 0; } | |
| .button-grid { grid-template-columns: 1fr; } | |
| .viewer, pre { min-height: 420px; } | |
| } | |
| </style> | |
| </head> | |
| <body> | |
| <div class="chrome"></div> | |
| <div class="wrap"> | |
| <section class="hero"> | |
| <div class="hero-top"> | |
| <div> | |
| <div class="eyebrow">Live OpenEnv Ops Console</div> | |
| <h1>NovaTech Incident Command</h1> | |
| <p>Run a full incident workflow from one place: shape your search space, surface the most credible evidence, lock in a structured causal hypothesis, and pressure-test the final report before submission.</p> | |
| </div> | |
| <div class="hero-statbar"> | |
| <div class="hero-stat"> | |
| <div class="label">Mode</div> | |
| <div class="value">Seeded, Partial</div> | |
| </div> | |
| <div class="hero-stat"> | |
| <div class="label">Sessions</div> | |
| <div class="value" id="hero-session">None</div> | |
| </div> | |
| <div class="hero-stat"> | |
| <div class="label">Last Reward</div> | |
| <div class="value" id="hero-reward">-</div> | |
| </div> | |
| </div> | |
| </div> | |
| </section> | |
| <section class="dashboard"> | |
| <div class="panel"> | |
| <div class="panel-head"> | |
| <h2 class="panel-title">Mission Control</h2> | |
| </div> | |
| <p class="panel-subtitle">Start a seeded episode, track session health, and jump into common action patterns without writing boilerplate from scratch.</p> | |
| <div class="panel-body stack"> | |
| <div class="field"> | |
| <label>Task</label> | |
| <select id="task"> | |
| <option value="easy">easy · auth heap exhaustion</option> | |
| <option value="medium">medium · competing checkout hypotheses</option> | |
| <option value="hard">hard · cascading multi-service incident</option> | |
| </select> | |
| </div> | |
| <div class="field"> | |
| <label>Seed</label> | |
| <input id="seed" placeholder="Optional integer seed for reproducibility" /> | |
| </div> | |
| <div class="button-grid"> | |
| <button onclick="resetEpisode()">Reset Episode</button> | |
| <button class="button-secondary" onclick="loadState()">Load Public State</button> | |
| </div> | |
| <div id="status" class="status">No active session yet. Reset an episode to begin.</div> | |
| <div class="kpis"> | |
| <div class="kpi"> | |
| <div class="label">Session ID</div> | |
| <div class="value" id="session-pill">-</div> | |
| </div> | |
| <div class="kpi"> | |
| <div class="label">Task</div> | |
| <div class="value" id="task-pill">-</div> | |
| </div> | |
| <div class="kpi"> | |
| <div class="label">Step</div> | |
| <div class="value" id="step-pill">-</div> | |
| </div> | |
| <div class="kpi"> | |
| <div class="label">Done</div> | |
| <div class="value" id="done-pill">-</div> | |
| </div> | |
| </div> | |
| <div> | |
| <div class="group-label">Quick Templates</div> | |
| <div class="template-grid"> | |
| <button class="template" onclick="useTemplate('critical_window')"> | |
| <strong>Critical Window Query</strong> | |
| <span>Pull the highest-risk logs in the incident window first.</span> | |
| </button> | |
| <button class="template" onclick="useTemplate('dependency_sweep')"> | |
| <strong>Dependency Sweep</strong> | |
| <span>Inspect the most suspicious service and its adjacent services.</span> | |
| </button> | |
| <button class="template" onclick="useTemplate('hypothesis_auth')"> | |
| <strong>Auth Hypothesis</strong> | |
| <span>Start from resource exhaustion in auth-service.</span> | |
| </button> | |
| <button class="template" onclick="useTemplate('submit_shell')"> | |
| <strong>Final Report Shell</strong> | |
| <span>Fill a structured report with observed evidence only.</span> | |
| </button> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <div class="panel"> | |
| <div class="panel-head"> | |
| <h2 class="panel-title">Action Composer</h2> | |
| <button class="button-ghost" style="width:auto;" onclick="formatAction()">Format JSON</button> | |
| </div> | |
| <p class="panel-subtitle">Work directly against the typed API. The current session id is auto-injected when missing, so you can focus on the action payload itself.</p> | |
| <div class="panel-body"> | |
| <div class="field"> | |
| <label>Action JSON</label> | |
| <textarea id="action">{ | |
| "action_type": "query_logs", | |
| "query": { | |
| "levels": ["CRITICAL", "ERROR"], | |
| "limit": 5 | |
| } | |
| }</textarea> | |
| </div> | |
| <div class="button-grid" style="margin-top: 12px;"> | |
| <button onclick="submitStep()">Submit Step</button> | |
| <button class="button-secondary" onclick="copySessionAction()">Inject Session + Copy</button> | |
| </div> | |
| <div class="footer-note">Tip: keep evidence grounded. The grader now rejects unseen log ids and penalizes contradictions across service, impact, and containment.</div> | |
| </div> | |
| </div> | |
| <div class="panel sidebar-right"> | |
| <div class="panel-head"> | |
| <h2 class="panel-title">Situation Room</h2> | |
| </div> | |
| <p class="panel-subtitle">Read the live incident summary, then switch between raw JSON and a cleaner operator view to understand what changed after each step.</p> | |
| <div class="panel-body"> | |
| <div class="brief"> | |
| <div class="brief-card"> | |
| <h3>Incident Snapshot</h3> | |
| <p id="brief-title">No active incident briefing yet.</p> | |
| </div> | |
| <div class="brief-card"> | |
| <h3>Operational Constraints</h3> | |
| <ul id="constraints-list"> | |
| <li>Reset an episode to load task-specific constraints.</li> | |
| </ul> | |
| </div> | |
| <div class="brief-card"> | |
| <h3>Suspected Services</h3> | |
| <div class="chips" id="suspected-services"> | |
| <span class="chip">None</span> | |
| </div> | |
| </div> | |
| </div> | |
| <div class="viewer-tabs" style="margin-top: 16px;"> | |
| <button class="tab active" id="tab-raw" onclick="switchTab('raw')">Raw JSON</button> | |
| <button class="tab" id="tab-ops" onclick="switchTab('ops')">Ops Summary</button> | |
| </div> | |
| <div class="viewer"> | |
| <pre id="output-raw">No data yet.</pre> | |
| <pre id="output-ops" class="hidden">No data yet.</pre> | |
| </div> | |
| </div> | |
| </div> | |
| </section> | |
| </div> | |
| <script> | |
| let currentSessionId = null; | |
| const templates = { | |
| critical_window: { | |
| action_type: "query_logs", | |
| query: { levels: ["CRITICAL", "ERROR"], limit: 6 } | |
| }, | |
| dependency_sweep: { | |
| action_type: "inspect_dependencies", | |
| target_service: "payment-api" | |
| }, | |
| hypothesis_auth: { | |
| action_type: "update_hypothesis", | |
| hypothesis: { | |
| primary_service: "auth-service", | |
| failure_mode: "resource_exhaustion", | |
| dependency: "none", | |
| customer_impact: "login_failures", | |
| confidence: 0.82 | |
| } | |
| }, | |
| submit_shell: { | |
| action_type: "submit_report", | |
| report: { | |
| evidence_log_ids: [], | |
| impacted_services: ["auth-service"], | |
| root_cause: { | |
| primary_service: "auth-service", | |
| failure_mode: "resource_exhaustion", | |
| dependency: "none", | |
| customer_impact: "login_failures", | |
| confidence: 0.82 | |
| }, | |
| containment_plan: ["increase_auth_heap", "enable_login_rate_limiting"], | |
| summary: "Replace this with a concise, evidence-backed incident summary." | |
| } | |
| } | |
| }; | |
| function buildOpsView(data) { | |
| const source = data.observation || data; | |
| const reward = data.reward || data.last_reward || {}; | |
| const logs = source.visible_logs || []; | |
| const lines = []; | |
| lines.push("Session Overview"); | |
| lines.push(`- Session: ${source.session_id || currentSessionId || "-"}`); | |
| lines.push(`- Task: ${source.task_id || "-"}`); | |
| lines.push(`- Step: ${source.step_number ?? data.step_number ?? "-"} / ${source.max_steps ?? data.max_steps ?? "-"}`); | |
| lines.push(`- Revealed logs: ${source.revealed_log_count ?? data.revealed_log_count ?? logs.length ?? 0}`); | |
| lines.push(`- Done: ${String(source.done ?? data.done ?? "-")}`); | |
| if (source.feedback) { | |
| lines.push(""); | |
| lines.push("Feedback"); | |
| lines.push(source.feedback); | |
| } | |
| if (source.briefing) { | |
| lines.push(""); | |
| lines.push("Briefing"); | |
| lines.push(`- Title: ${source.briefing.title}`); | |
| lines.push(`- Objective: ${source.briefing.objective}`); | |
| lines.push(`- Customer: ${source.briefing.customer_statement}`); | |
| } | |
| if (source.last_hypothesis) { | |
| lines.push(""); | |
| lines.push("Latest Hypothesis"); | |
| lines.push(`- Service: ${source.last_hypothesis.primary_service}`); | |
| lines.push(`- Failure mode: ${source.last_hypothesis.failure_mode}`); | |
| lines.push(`- Dependency: ${source.last_hypothesis.dependency}`); | |
| lines.push(`- Impact: ${source.last_hypothesis.customer_impact}`); | |
| lines.push(`- Confidence: ${source.last_hypothesis.confidence}`); | |
| } | |
| if (source.submitted_containment && source.submitted_containment.length) { | |
| lines.push(""); | |
| lines.push("Containment"); | |
| source.submitted_containment.forEach((item) => lines.push(`- ${item}`)); | |
| } | |
| if (reward.value !== undefined) { | |
| lines.push(""); | |
| lines.push("Reward"); | |
| lines.push(`- Total: ${Number(reward.value).toFixed(4)}`); | |
| if (reward.signal_reward !== undefined) lines.push(`- Signal: ${Number(reward.signal_reward).toFixed(4)}`); | |
| if (reward.hypothesis_reward !== undefined) lines.push(`- Hypothesis: ${Number(reward.hypothesis_reward).toFixed(4)}`); | |
| if (reward.efficiency_reward !== undefined) lines.push(`- Efficiency: ${Number(reward.efficiency_reward).toFixed(4)}`); | |
| if (reward.penalty !== undefined) lines.push(`- Penalty: ${Number(reward.penalty).toFixed(4)}`); | |
| } | |
| if (logs.length) { | |
| lines.push(""); | |
| lines.push(`Visible Logs (${logs.length})`); | |
| logs.slice(0, 10).forEach((log) => { | |
| lines.push(`- [${log.log_level}] ${log.log_id} · ${log.service_name} · ${log.server_id}`); | |
| lines.push(` ${log.message}`); | |
| }); | |
| } | |
| return lines.join("\\n"); | |
| } | |
| function refreshBriefing(observation) { | |
| document.getElementById("session-pill").textContent = observation.session_id || currentSessionId || "-"; | |
| document.getElementById("task-pill").textContent = observation.task_id || "-"; | |
| document.getElementById("step-pill").textContent = `${observation.step_number ?? "-"} / ${observation.max_steps ?? "-"}`; | |
| document.getElementById("done-pill").textContent = String(observation.done ?? "-"); | |
| document.getElementById("hero-session").textContent = observation.session_id ? observation.session_id.slice(0, 8) : "None"; | |
| if (observation.briefing) { | |
| document.getElementById("brief-title").textContent = `${observation.briefing.title}: ${observation.briefing.customer_statement}`; | |
| const list = document.getElementById("constraints-list"); | |
| list.innerHTML = ""; | |
| observation.briefing.operational_constraints.forEach((item) => { | |
| const li = document.createElement("li"); | |
| li.textContent = item; | |
| list.appendChild(li); | |
| }); | |
| const chips = document.getElementById("suspected-services"); | |
| chips.innerHTML = ""; | |
| observation.briefing.suspected_services.forEach((service) => { | |
| const chip = document.createElement("span"); | |
| chip.className = "chip"; | |
| chip.textContent = service; | |
| chips.appendChild(chip); | |
| }); | |
| } | |
| } | |
| function show(data) { | |
| document.getElementById("output-raw").textContent = JSON.stringify(data, null, 2); | |
| document.getElementById("output-ops").textContent = buildOpsView(data); | |
| const observation = data.observation || data; | |
| if (observation.session_id) currentSessionId = observation.session_id; | |
| refreshBriefing(observation); | |
| const reward = data.reward || data.last_reward; | |
| document.getElementById("hero-reward").textContent = reward && reward.value !== undefined ? Number(reward.value).toFixed(3) : "-"; | |
| } | |
| function status(text, ok=true) { | |
| const node = document.getElementById("status"); | |
| node.textContent = text; | |
| node.className = ok ? "status ok" : "status bad"; | |
| } | |
| function switchTab(which) { | |
| const raw = document.getElementById("output-raw"); | |
| const ops = document.getElementById("output-ops"); | |
| const rawTab = document.getElementById("tab-raw"); | |
| const opsTab = document.getElementById("tab-ops"); | |
| if (which === "ops") { | |
| raw.classList.add("hidden"); | |
| ops.classList.remove("hidden"); | |
| rawTab.classList.remove("active"); | |
| opsTab.classList.add("active"); | |
| } else { | |
| ops.classList.add("hidden"); | |
| raw.classList.remove("hidden"); | |
| opsTab.classList.remove("active"); | |
| rawTab.classList.add("active"); | |
| } | |
| } | |
| function useTemplate(name) { | |
| const template = JSON.parse(JSON.stringify(templates[name])); | |
| if (currentSessionId) template.session_id = currentSessionId; | |
| document.getElementById("action").value = JSON.stringify(template, null, 2); | |
| } | |
| function formatAction() { | |
| try { | |
| const payload = JSON.parse(document.getElementById("action").value); | |
| document.getElementById("action").value = JSON.stringify(payload, null, 2); | |
| status("Action JSON formatted."); | |
| } catch (error) { | |
| status(error.message, false); | |
| } | |
| } | |
| async function copySessionAction() { | |
| try { | |
| const payload = JSON.parse(document.getElementById("action").value); | |
| if (currentSessionId) payload.session_id = currentSessionId; | |
| const text = JSON.stringify(payload, null, 2); | |
| document.getElementById("action").value = text; | |
| if (navigator.clipboard) { | |
| await navigator.clipboard.writeText(text); | |
| } | |
| status("Session id injected and action copied."); | |
| } catch (error) { | |
| status(error.message, false); | |
| } | |
| } | |
| async function resetEpisode() { | |
| const task_id = document.getElementById("task").value; | |
| const rawSeed = document.getElementById("seed").value.trim(); | |
| const payload = { task_id }; | |
| if (rawSeed) payload.seed = Number(rawSeed); | |
| const res = await fetch("/reset", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(payload) }); | |
| const data = await res.json(); | |
| if (!res.ok) return status(JSON.stringify(data), false); | |
| show(data); | |
| status("Episode reset."); | |
| } | |
| async function submitStep() { | |
| try { | |
| const payload = JSON.parse(document.getElementById("action").value); | |
| if (currentSessionId && !payload.session_id) payload.session_id = currentSessionId; | |
| const res = await fetch("/step", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(payload) }); | |
| const data = await res.json(); | |
| if (!res.ok) return status(JSON.stringify(data), false); | |
| show(data); | |
| status("Step completed."); | |
| } catch (error) { | |
| status(error.message, false); | |
| } | |
| } | |
| async function loadState() { | |
| const url = currentSessionId ? `/state?session_id=${encodeURIComponent(currentSessionId)}` : "/state"; | |
| const res = await fetch(url); | |
| const data = await res.json(); | |
| if (!res.ok) return status(JSON.stringify(data), false); | |
| show(data); | |
| status("Public state loaded."); | |
| } | |
| switchTab('raw'); | |
| </script> | |
| </body> | |
| </html> | |
| """ | |
| def health() -> Dict[str, str]: | |
| return {"status": "ok"} | |
| def reset(request: Optional[ResetRequest] = Body(default=None)) -> Dict[str, Any]: | |
| try: | |
| payload = request or ResetRequest() | |
| observation = store.reset(task_id=payload.task_id, seed=payload.seed) | |
| except ValueError as exc: | |
| raise HTTPException(status_code=422, detail=str(exc)) from exc | |
| return observation.model_dump() | |
| def step(action: Action) -> StepResponse: | |
| try: | |
| observation, reward, done, info = store.step(action) | |
| except (RuntimeError, ValueError) as exc: | |
| raise HTTPException(status_code=400, detail=str(exc)) from exc | |
| return StepResponse( | |
| observation=observation.model_dump(), | |
| reward=reward.model_dump(), | |
| done=done, | |
| info=info, | |
| ) | |
| def state(session_id: Optional[str] = Query(default=None)) -> Dict[str, Any]: | |
| try: | |
| return store.public_state(session_id=session_id) | |
| except RuntimeError as exc: | |
| raise HTTPException(status_code=400, detail=str(exc)) from exc | |
| def debug_state(session_id: Optional[str] = Query(default=None)) -> Dict[str, Any]: | |
| try: | |
| return store.debug_state(session_id=session_id) | |
| except PermissionError as exc: | |
| raise HTTPException(status_code=403, detail=str(exc)) from exc | |
| except RuntimeError as exc: | |
| raise HTTPException(status_code=400, detail=str(exc)) from exc | |
| def main() -> None: | |
| port = int(os.getenv("PORT", "7860")) | |
| uvicorn.run("app:app", host="0.0.0.0", port=port) | |