claims-env / space_app.py
akhiilll's picture
fix: auto-reset on first step + dashboard auto-reset on load (no more 500 on click query_policy)
9e60416 verified
"""HF Spaces server: ClaimSense adjudication gym + lightweight dashboard.
Run with::
uvicorn space_app:app --host 0.0.0.0 --port 7860
Adds three things on top of the OpenEnv FastAPI scaffolding:
1. ``GET /`` — an HTML dashboard so the Space's landing page looks
like a product, not raw JSON.
2. ``GET /api`` — the JSON metadata block that used to live at ``/``.
3. ``GET /info`` — verbose env description used by notebooks/training.
"""
from __future__ import annotations
import os
import sys
from pathlib import Path
# Make local sibling modules importable when running inside the Space's
# Docker image (where the working directory is ``/app``).
sys.path.insert(0, str(Path(__file__).resolve().parent))
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
from fastapi.responses import HTMLResponse
from openenv.core.env_server import create_fastapi_app
from models import AdjudicatorAction, AdjudicatorObservation
from server.claims_environment import AdjudicationGym
# ---------------------------------------------------------------------------
# Compose the FastAPI app
# ---------------------------------------------------------------------------
app: FastAPI = create_fastapi_app(
AdjudicationGym, AdjudicatorAction, AdjudicatorObservation
)
# Allow notebooks/clients on any origin to call us during demos.
app.add_middleware(
CORSMiddleware,
allow_origins=["*"],
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
# ---------------------------------------------------------------------------
# Dashboard HTML
# ---------------------------------------------------------------------------
DASHBOARD_HTML = """<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width,initial-scale=1" />
<title>ClaimSense AI · Adjudication Gym</title>
<style>
:root {
--bg:#0b1020; --card:#141a36; --muted:#8a93b8; --fg:#e8ecff;
--accent:#7c5cff; --good:#22c55e; --bad:#ef4444; --warn:#f59e0b;
}
* { box-sizing: border-box; }
body {
margin: 0; background: linear-gradient(180deg, #0b1020 0%, #0c1230 100%);
color: var(--fg); font: 15px/1.55 -apple-system, Segoe UI, Roboto, sans-serif;
}
.wrap { max-width: 1100px; margin: 0 auto; padding: 32px 20px 80px; }
header {
display: flex; align-items: center; gap: 16px; margin-bottom: 24px;
flex-wrap: wrap;
}
h1 { margin: 0; font-size: 28px; letter-spacing: .2px; }
.pill {
display: inline-flex; align-items: center; gap: 8px; background: var(--card);
padding: 6px 12px; border-radius: 999px; font-size: 13px; color: var(--muted);
}
.dot {
width: 8px; height: 8px; border-radius: 50%; background: var(--good);
box-shadow: 0 0 0 4px rgba(34,197,94,.18);
}
.dot.bad { background: var(--bad); box-shadow: 0 0 0 4px rgba(239,68,68,.18); }
.dot.wait { background: var(--warn); box-shadow: 0 0 0 4px rgba(245,158,11,.18); }
.grid {
display: grid; gap: 16px;
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
}
.card {
background: var(--card); border: 1px solid #20284e;
border-radius: 14px; padding: 18px;
}
.card h3 {
margin: 0 0 10px; font-size: 14px; color: var(--muted);
text-transform: uppercase; letter-spacing: .8px;
}
.kv {
display: flex; justify-content: space-between; padding: 6px 0;
border-bottom: 1px dashed #1f2748; font-size: 14px;
}
.kv:last-child { border: none; }
.kv span { color: var(--muted); }
code, .mono {
font-family: ui-monospace, SFMono-Regular, Consolas, monospace;
}
.actions { display: flex; flex-wrap: wrap; gap: 8px; }
.tag {
background: #1c2350; border: 1px solid #2a3470; color: #bfc7ff;
font-size: 12px; padding: 4px 10px; border-radius: 999px;
}
.row { display: flex; gap: 12px; flex-wrap: wrap; align-items: center; }
button {
background: var(--accent); color: white; border: none;
padding: 10px 16px; border-radius: 10px; font-weight: 600;
cursor: pointer; font-size: 14px;
}
button:hover { filter: brightness(1.1); }
button.alt { background: #1f2748; }
pre {
background: #070b1d; padding: 14px; border-radius: 10px;
overflow: auto; max-height: 280px; border: 1px solid #1c2350;
font-size: 12.5px;
}
a { color: #a4b1ff; }
.footer {
margin-top: 32px; color: var(--muted); font-size: 12px; text-align: center;
}
.hero {
background: linear-gradient(135deg, #1a1f4d, #2a1c5e);
padding: 24px; border-radius: 14px;
}
.badge {
background: #2a3470; padding: 3px 8px; border-radius: 6px;
font-size: 12px; color: #bfc7ff;
}
</style>
</head>
<body>
<div class="wrap">
<header>
<h1>🛡️ ClaimSense AI</h1>
<span class="pill" id="health">
<span class="dot wait"></span><span id="healthText">checking…</span>
</span>
<span class="pill"><span class="badge">Space</span>&nbsp; akhiilll/claims-env</span>
</header>
<div class="hero">
<div style="font-size:13px;color:var(--muted);text-transform:uppercase;letter-spacing:.8px;margin-bottom:6px;">
OpenEnv Hackathon · Statement 3.1 · Scaler AI Labs
</div>
<div style="font-size:18px;line-height:1.5;">
An adjudication gym for training LLM agents to triage insurance
claims — partial observability, eight curated cases, fraud signals,
and bank-transaction verification.
</div>
<div class="row" style="margin-top:14px;">
<button onclick="runReset()">▶ Reset episode</button>
<button class="alt" onclick="runStep('query_policy')">step: query_policy</button>
<button class="alt" onclick="runStep('check_fraud')">step: check_fraud</button>
<button class="alt" onclick="loadInfo()">refresh info</button>
<a class="pill" href="/docs">📘 OpenAPI /docs</a>
<a class="pill" href="/api">{ } JSON /api</a>
</div>
</div>
<div class="grid" style="margin-top:18px;">
<div class="card">
<h3>Endpoints</h3>
<div class="kv"><span>HTTP base</span><code id="base"></code></div>
<div class="kv"><span>WebSocket</span><code id="ws"></code></div>
<div class="kv"><span>Reset</span><code>POST /reset</code></div>
<div class="kv"><span>Step</span><code>POST /step</code></div>
<div class="kv"><span>State</span><code>GET /state</code></div>
<div class="kv"><span>Health</span><code>GET /health</code></div>
</div>
<div class="card">
<h3>Reward shaping</h3>
<div class="kv"><span>Correct decision</span><code style="color:var(--good)">+10</code></div>
<div class="kv"><span>Fraud caught (deny)</span><code style="color:var(--good)">+5</code></div>
<div class="kv"><span>Plaid discrepancy surfaced</span><code style="color:var(--good)">+2</code></div>
<div class="kv"><span>Fast resolution (≤4 steps)</span><code style="color:var(--good)">+1</code></div>
<div class="kv"><span>Wrong decision</span><code style="color:var(--bad)">-5</code></div>
<div class="kv"><span>Fraud missed (approve)</span><code style="color:var(--bad)">-10</code></div>
<div class="kv"><span>Query cost</span><code style="color:var(--warn)">-0.1 … -0.5</code></div>
</div>
<div class="card">
<h3>Curated case set (8)</h3>
<div class="actions">
<span class="tag">Routine fender-bender</span>
<span class="tag">Burst pipe (capped)</span>
<span class="tag">Staged accident</span>
<span class="tag">External flood (excluded)</span>
<span class="tag">Six-figure house fire</span>
<span class="tag">Inflated stolen vehicle</span>
<span class="tag">Slip-and-fall liability</span>
<span class="tag">Lapsed policy</span>
</div>
</div>
<div class="card">
<h3>Action vocabulary (10)</h3>
<div class="actions" id="actions">loading…</div>
</div>
</div>
<div class="card" style="margin-top:18px;">
<h3>Live API probe</h3>
<pre id="output">click a button above to call the API</pre>
</div>
<div class="footer">
Built on OpenEnv · FastAPI · Hugging Face Spaces
</div>
</div>
<script>
const out = document.getElementById('output');
const dot = document.querySelector('#health .dot');
const dotText = document.getElementById('healthText');
document.getElementById('base').textContent = window.location.origin;
document.getElementById('ws').textContent =
window.location.origin.replace('https', 'wss').replace('http', 'ws') + '/ws';
async function loadHealth() {
try {
const r = await fetch('/health');
const j = await r.json();
dot.className = 'dot';
dotText.textContent = j.status === 'healthy' ? 'healthy · running' : 'degraded';
} catch (e) {
dot.className = 'dot bad';
dotText.textContent = 'offline';
}
}
async function loadInfo() {
try {
const r = await fetch('/api');
const j = await r.json();
const acts = j.valid_actions || [];
document.getElementById('actions').innerHTML =
acts.map(a => '<span class="tag">' + a + '</span>').join('');
out.textContent = JSON.stringify(j, null, 2);
} catch (e) {
out.textContent = 'failed: ' + e;
}
}
async function runReset() {
out.textContent = 'POST /reset …';
try {
const r = await fetch('/reset', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: '{}',
});
out.textContent = JSON.stringify(await r.json(), null, 2);
} catch (e) {
out.textContent = 'error: ' + e;
}
}
async function runStep(action_type) {
out.textContent = 'POST /step ' + action_type + ' …';
try {
const r = await fetch('/step', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ action: { action_type, parameters: {} } }),
});
out.textContent = JSON.stringify(await r.json(), null, 2);
} catch (e) {
out.textContent = 'error: ' + e;
}
}
loadHealth();
loadInfo();
runReset();
setInterval(loadHealth, 15000);
</script>
</body>
</html>
"""
# ---------------------------------------------------------------------------
# Routes
# ---------------------------------------------------------------------------
@app.get("/", response_class=HTMLResponse)
async def root_dashboard() -> HTMLResponse:
"""Single-page dashboard served at the Space root."""
return HTMLResponse(content=DASHBOARD_HTML)
@app.get("/api")
async def api_metadata() -> dict[str, object]:
"""Machine-readable metadata (was at ``/`` historically)."""
return {
"name": "ClaimSense Adjudication Gym",
"version": "1.1.0",
"hackathon": "OpenEnv Hackathon - Cerebral Valley",
"problem_statement": "3.1 - Professional Tasks (World Modeling)",
"partner_theme": "Scaler AI Labs - Enterprise Workflows",
"status": "running",
"valid_actions": list(AdjudicationGym.VALID_ACTIONS),
"endpoints": {
"health": "/health",
"info": "/info",
"reset": "POST /reset",
"step": "POST /step",
"state": "GET /state",
"websocket": "/ws",
},
}
@app.get("/info")
async def long_info() -> dict[str, object]:
"""Verbose description used by notebooks for documentation."""
return {
"name": "ClaimSense Adjudication Gym",
"version": "1.1.0",
"description": (
"RL environment for training LLM agents to triage insurance "
"claims through a sequence of evidence-gathering steps and a "
"final verdict."
),
"problem_statement": "3.1 - Professional Tasks (World Modeling)",
"partner_theme": "Scaler AI Labs - Enterprise Workflows",
"features": [
"Partial observability — agent must query for facts",
"Multi-step decision making with terminal verdicts",
"Fraud detection signals and Plaid-style transaction audit",
"Business rule enforcement (deductibles, exclusions, lapsed)",
"Enterprise-flavoured workflow with escalation paths",
],
"valid_actions": list(AdjudicationGym.VALID_ACTIONS),
"action_costs": AdjudicationGym.ACTION_TIME_COSTS,
"reward_structure": {
"correct_decision": "+10",
"wrong_decision": "-5",
"fraud_caught": "+5",
"fraud_missed": "-10",
"plaid_discrepancy_found": "+2",
"query_cost": "-0.1 to -0.5",
"fast_resolution_bonus": "+1 (≤ 4 steps)",
"slow_resolution_penalty": "-0.2 per step beyond 8",
},
"scenarios": 8,
"scenario_types": [
"Routine approval",
"Partial settlement (capped)",
"Staged accident fraud",
"Excluded coverage denial",
"Six-figure escalation",
"Inflated theft fraud",
"Liability slip-and-fall",
"Lapsed-policy denial",
],
}
@app.get("/scenarios")
async def list_scenarios() -> dict[str, object]:
"""Enumerate the curated case library."""
from server.mock_systems import CASE_LIBRARY # local to avoid import cycle
return {
"total_scenarios": len(CASE_LIBRARY),
"scenarios": [
{
"index": i,
"claim_id": case.claim_id,
"claim_type": case.claim_type,
"complexity": case.complexity,
"amount": case.claim_amount,
"is_fraud": case.is_fraud,
}
for i, case in enumerate(CASE_LIBRARY)
],
}
@app.get("/health")
async def health_probe() -> dict[str, str]:
"""Liveness probe used by Spaces, monitors, and the dashboard."""
return {"status": "healthy", "environment": "claimsense"}
# ---------------------------------------------------------------------------
# Local dev entrypoint (``python space_app.py``)
# ---------------------------------------------------------------------------
if __name__ == "__main__":
import uvicorn
port = int(os.environ.get("PORT", 7860))
uvicorn.run(app, host="0.0.0.0", port=port)