Spaces:
Sleeping
Sleeping
| """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> 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 | |
| # --------------------------------------------------------------------------- | |
| async def root_dashboard() -> HTMLResponse: | |
| """Single-page dashboard served at the Space root.""" | |
| return HTMLResponse(content=DASHBOARD_HTML) | |
| 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", | |
| }, | |
| } | |
| 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", | |
| ], | |
| } | |
| 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) | |
| ], | |
| } | |
| 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) | |