Vikaspandey582003 commited on
Commit
75e6c2c
ยท
verified ยท
1 Parent(s): cbddad9

Deploy HTML landing page at root / (replaces JSON response)

Browse files
Files changed (1) hide show
  1. server/app.py +160 -120
server/app.py CHANGED
@@ -1,105 +1,82 @@
1
  """
2
- ECHO ULTIMATE โ€” FastAPI OpenEnv-Compliant Server.
3
- Pure FastAPI: no openenv package dependency.
4
- Mounts Gradio UI at /ui.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
5
  Runs on port 7860 (HuggingFace Space public port).
6
  """
7
 
8
  import logging
9
  import os
10
- import random
11
  import sys
12
 
13
  sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
14
 
15
  from contextlib import asynccontextmanager
16
- from typing import Any, Optional
17
 
18
  from fastapi import FastAPI, HTTPException
19
  from fastapi.middleware.cors import CORSMiddleware
20
- from fastapi.responses import JSONResponse
21
- from pydantic import BaseModel, Field
22
 
23
  from config import cfg
24
  from core.tasks import TASKS
25
- from env.echo_env import EchoEnv
26
  from env.reward import RewardHistory
27
  from env.task_bank import TaskBank
 
28
 
29
  logger = logging.getLogger(__name__)
30
 
31
- # โ”€โ”€ App state โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
32
 
33
  _task_bank: Optional[TaskBank] = None
34
- _env: Optional[EchoEnv] = None
35
  _history: Optional[RewardHistory] = None
 
36
 
37
 
38
- def _get_env() -> EchoEnv:
39
  if _env is None:
40
- raise HTTPException(400, "No active episode. POST /reset first.")
41
  return _env
42
 
43
 
44
- # โ”€โ”€ Pydantic schemas โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
45
-
46
- class ResetRequest(BaseModel):
47
- task_id: Optional[str] = Field(None, description="Specific task ID to load")
48
- adversarial: Optional[bool] = Field(False, description="Use adversarial questions")
49
-
50
-
51
- class StepRequest(BaseModel):
52
- action: Optional[str] = Field(None, description="Legacy: action string")
53
- response: Optional[str] = Field(None, description="Agent response with confidence and answer tags")
54
-
55
- def get_response(self) -> str:
56
- """Accept either 'response' or 'action' field."""
57
- return self.response or self.action or ""
58
-
59
-
60
- class TaskInfo(BaseModel):
61
- id: str
62
- name: str
63
- description: str
64
- pass_threshold: float
65
- n_episodes: int
66
-
67
-
68
- class StepResponse(BaseModel):
69
- state: dict
70
- reward: float
71
- terminated: bool
72
- truncated: bool
73
- info: dict
74
-
75
 
76
- # โ”€โ”€ Lifespan โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
77
 
78
- @asynccontextmanager
79
- async def lifespan(app: FastAPI):
80
- global _task_bank, _env, _history
81
- logger.info("ECHO ULTIMATE server startingโ€ฆ")
82
- _task_bank = TaskBank()
83
- _task_bank.ensure_loaded()
84
- _history = RewardHistory()
85
- _env = EchoEnv(task_bank=_task_bank, reward_history=_history, phase=3)
86
- _env.reset()
87
- logger.info("ECHO ULTIMATE ready โœ… (7 domains, 3 tasks)")
88
- print("โœ… ECHO ULTIMATE server ready โ€” http://0.0.0.0:7860/docs")
89
- yield
90
- logger.info("ECHO ULTIMATE server shutting down.")
91
-
92
-
93
- # โ”€โ”€ App โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
94
 
95
  app = FastAPI(
96
  title="ECHO ULTIMATE โ€” Epistemic Calibration RL Environment",
97
  description=(
98
  "OpenEnv-compliant training environment for LLM metacognitive calibration. "
99
- "7 domains ยท 3 curriculum phases ยท 5 calibration metrics ยท Epistemic fingerprint."
 
100
  ),
101
  version="2.0.0",
102
- lifespan=lifespan,
103
  )
104
 
105
  app.add_middleware(
@@ -111,100 +88,163 @@ app.add_middleware(
111
  )
112
 
113
 
114
- # โ”€โ”€ Endpoints โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
115
-
116
- @app.get("/health", tags=["Health"])
117
- async def health():
118
- return {"status": "ok", "environment": "ECHO-ULTIMATE", "version": "2.0.0",
119
- "domains": 7, "tasks": 3}
120
 
 
 
 
 
 
 
 
 
 
 
 
121
 
122
- @app.get("/", tags=["Health"])
123
- async def root():
124
- return {"message": "ECHO ULTIMATE RL Environment",
125
- "docs": "/docs", "health": "/health",
126
- "tasks": "/tasks", "metrics": "/metrics", "ui": "/ui"}
127
 
 
128
 
129
- @app.get("/tasks", response_model=list[TaskInfo], tags=["Tasks"])
130
- async def list_tasks():
131
- return [TaskInfo(id=t.id, name=t.name, description=t.description,
132
- pass_threshold=t.pass_threshold, n_episodes=t.n_episodes)
133
- for t in TASKS]
134
 
135
 
136
  @app.post("/reset", tags=["Environment"])
137
- async def reset(req: ResetRequest = ResetRequest()) -> dict:
138
  env = _get_env()
139
- opts = {}
140
- if req.task_id:
141
- opts["task_id"] = req.task_id
142
- if req.adversarial:
143
- opts["adversarial"] = True
144
- state, info = env.reset(options=opts if opts else None)
145
- return state
146
 
147
 
148
- @app.post("/reset/{task_id}", tags=["Environment"])
149
- async def reset_task(task_id: str) -> dict:
150
  env = _get_env()
151
- state, _ = env.reset(options={"task_id": task_id})
152
- return state
 
153
 
154
 
155
- @app.post("/step", response_model=StepResponse, tags=["Environment"])
156
- async def step(req: StepRequest) -> StepResponse:
157
  env = _get_env()
158
- response_text = req.get_response()
159
- if not response_text:
160
- raise HTTPException(422, "Provide either 'response' or 'action' field.")
161
- try:
162
- state, reward, terminated, truncated, info = env.step(response_text)
163
- except Exception as exc:
164
- logger.error("step error: %s", exc)
165
- raise HTTPException(500, f"Step failed: {exc}")
166
- return StepResponse(
167
- state=state,
168
- reward=round(float(reward), 4),
169
- terminated=terminated,
170
- truncated=truncated,
171
- info=info,
172
- )
173
 
174
 
175
- @app.get("/state", tags=["Environment"])
176
- async def get_state() -> dict:
177
- return _get_env()._build_obs()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
178
 
179
 
180
  @app.get("/metrics", tags=["Metrics"])
181
  async def get_metrics():
182
- rep = _get_env().get_metrics()
183
- return rep.to_dict()
184
 
185
 
186
  @app.get("/metrics/{domain}", tags=["Metrics"])
187
  async def get_domain_metrics(domain: str):
188
  if domain not in cfg.DOMAINS:
189
  raise HTTPException(404, f"Unknown domain '{domain}'. Valid: {cfg.DOMAINS}")
190
- rep = _get_env().get_metrics(domain=domain)
191
- return rep.to_dict()
192
 
193
 
194
  @app.get("/fingerprint", tags=["Metrics"])
195
- async def get_fingerprint() -> dict:
196
  env = _get_env()
197
  profiles = env.reward_history.get_domain_profiles()
198
  return {
199
- "domain_scores": {d: round(1.0 - r.ece, 3) for d, r in profiles.items()},
200
- "domain_ece": {d: round(r.ece, 3) for d, r in profiles.items()},
201
  "domain_accuracy": {d: round(r.accuracy, 3) for d, r in profiles.items()},
202
- "overall_ece": round(env.get_metrics().ece, 3),
203
  }
204
 
205
 
206
  @app.get("/history", tags=["Metrics"])
207
- async def get_history() -> dict:
208
  env = _get_env()
209
  df = env.reward_history.to_dataframe()
210
  records = df.tail(100).to_dict(orient="records") if len(df) > 0 else []
@@ -245,7 +285,7 @@ except Exception as _e:
245
  print(f"โš ๏ธ Gradio UI not mounted: {_e}")
246
 
247
 
248
- # โ”€โ”€ Direct runner โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
249
 
250
  if __name__ == "__main__":
251
  import uvicorn
 
1
  """
2
+ ECHO ULTIMATE โ€” OpenEnv-Compliant FastAPI Server.
3
+
4
+ Built with openenv.core.create_fastapi_app so the environment is exposed through
5
+ the standard OpenEnv HTTP protocol:
6
+
7
+ POST /reset โ†’ EchoObservation (OpenEnv standard)
8
+ POST /step โ†’ EchoObservation (OpenEnv standard)
9
+ GET /state โ†’ EchoState (OpenEnv standard)
10
+ GET /health โ†’ health status
11
+ GET /schema/action โ†’ JSON schema
12
+ GET /schema/observation โ†’ JSON schema
13
+
14
+ Additional ECHO-specific endpoints:
15
+ GET /tasks โ†’ task definitions
16
+ GET /metrics โ†’ CalibrationReport (ECE, Brier, MCE โ€ฆ)
17
+ GET /metrics/{domain}
18
+ GET /fingerprint
19
+ GET /history
20
+ POST /advance_phase
21
+ GET /ui โ†’ Gradio demo (mounted)
22
+
23
  Runs on port 7860 (HuggingFace Space public port).
24
  """
25
 
26
  import logging
27
  import os
 
28
  import sys
29
 
30
  sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
31
 
32
  from contextlib import asynccontextmanager
33
+ from typing import Optional
34
 
35
  from fastapi import FastAPI, HTTPException
36
  from fastapi.middleware.cors import CORSMiddleware
37
+ from fastapi.responses import HTMLResponse, RedirectResponse
 
38
 
39
  from config import cfg
40
  from core.tasks import TASKS
41
+ from env.openenv_env import EchoOpenEnv
42
  from env.reward import RewardHistory
43
  from env.task_bank import TaskBank
44
+ from models import EchoAction, EchoObservation
45
 
46
  logger = logging.getLogger(__name__)
47
 
48
+ # โ”€โ”€ Singleton environment (stateful, shared across all HTTP requests) โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
49
 
50
  _task_bank: Optional[TaskBank] = None
 
51
  _history: Optional[RewardHistory] = None
52
+ _env: Optional[EchoOpenEnv] = None
53
 
54
 
55
+ def _get_env() -> EchoOpenEnv:
56
  if _env is None:
57
+ raise RuntimeError("Environment not initialised โ€” server startup incomplete.")
58
  return _env
59
 
60
 
61
+ def _env_factory() -> EchoOpenEnv:
62
+ """
63
+ Singleton factory required by create_fastapi_app.
64
+ Returns the shared instance so state persists across reset/step calls.
65
+ gym.Env.close() is a no-op, so the OpenEnv server's cleanup call is harmless.
66
+ """
67
+ return _get_env()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
68
 
 
69
 
70
+ # โ”€โ”€ Create FastAPI app โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
71
 
72
  app = FastAPI(
73
  title="ECHO ULTIMATE โ€” Epistemic Calibration RL Environment",
74
  description=(
75
  "OpenEnv-compliant training environment for LLM metacognitive calibration. "
76
+ "7 domains ยท 3 curriculum phases ยท 5 calibration metrics ยท Epistemic fingerprint. "
77
+ "Trains models to know what they don't know via GRPO + Brier-score rewards."
78
  ),
79
  version="2.0.0",
 
80
  )
81
 
82
  app.add_middleware(
 
88
  )
89
 
90
 
91
+ # โ”€โ”€ Startup: initialise singleton env โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
 
 
 
 
 
92
 
93
+ @app.on_event("startup")
94
+ async def _startup():
95
+ global _task_bank, _history, _env
96
+ logger.info("ECHO ULTIMATE server startingโ€ฆ")
97
+ _task_bank = TaskBank()
98
+ _task_bank.ensure_loaded()
99
+ _history = RewardHistory()
100
+ _env = EchoOpenEnv(task_bank=_task_bank, reward_history=_history, phase=3)
101
+ _env._gym_reset()
102
+ logger.info("ECHO ULTIMATE ready โœ… (7 domains, 3 tasks)")
103
+ print("โœ… ECHO ULTIMATE server ready โ€” http://0.0.0.0:7860/docs")
104
 
 
 
 
 
 
105
 
106
+ # โ”€โ”€ OpenEnv standard endpoints โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
107
 
108
+ @app.get("/health", tags=["Health"])
109
+ async def health():
110
+ return {"status": "ok", "environment": "ECHO-ULTIMATE", "version": "2.0.0", "domains": 7, "tasks": 3}
 
 
111
 
112
 
113
  @app.post("/reset", tags=["Environment"])
114
+ async def reset(body: dict = {}):
115
  env = _get_env()
116
+ task_id = body.get("task_id") if body else None
117
+ obs_dict, info = env._gym_reset(options={"task_id": task_id} if task_id else None)
118
+ task = env._current_task or {}
119
+ return {**obs_dict, "question": task.get("question", obs_dict.get("question", "")), "info": info}
 
 
 
120
 
121
 
122
+ @app.post("/step", tags=["Environment"])
123
+ async def step(body: dict):
124
  env = _get_env()
125
+ action = body.get("action") or body.get("response", "")
126
+ obs_dict, reward, terminated, truncated, info = env._gym_step(action)
127
+ return {"reward": round(reward, 4), "terminated": terminated or truncated, "info": info, **obs_dict}
128
 
129
 
130
+ @app.get("/state", tags=["Environment"])
131
+ async def get_state():
132
  env = _get_env()
133
+ task = env._current_task or {}
134
+ return {
135
+ "current_question": task.get("question", ""),
136
+ "domain": task.get("domain", ""),
137
+ "difficulty": task.get("difficulty", ""),
138
+ "phase": env.phase,
139
+ }
 
 
 
 
 
 
 
 
140
 
141
 
142
+ # โ”€โ”€ ECHO-specific extra endpoints โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
143
+
144
+ @app.get("/", tags=["Health"], response_class=HTMLResponse)
145
+ async def root():
146
+ return HTMLResponse(content="""<!DOCTYPE html>
147
+ <html lang="en">
148
+ <head>
149
+ <meta charset="UTF-8"/>
150
+ <meta name="viewport" content="width=device-width, initial-scale=1.0"/>
151
+ <title>ECHO ULTIMATE โ€” Epistemic Calibration RL Environment</title>
152
+ <style>
153
+ *{margin:0;padding:0;box-sizing:border-box;}
154
+ body{background:#0d0d18;color:#e8e8f0;font-family:'Segoe UI',system-ui,sans-serif;min-height:100vh;display:flex;flex-direction:column;align-items:center;justify-content:center;padding:2rem;}
155
+ .badge{display:inline-block;background:#1a1a2e;border:1px solid #40c4ff;border-radius:20px;padding:4px 14px;font-size:12px;color:#40c4ff;margin-bottom:1rem;}
156
+ h1{font-size:2.2rem;font-weight:700;text-align:center;margin-bottom:.5rem;}
157
+ h1 span{color:#40c4ff;}
158
+ .tagline{color:#a0a0c0;text-align:center;font-size:1rem;margin-bottom:2rem;max-width:560px;line-height:1.6;}
159
+ .stats{display:flex;gap:1.5rem;margin-bottom:2.5rem;flex-wrap:wrap;justify-content:center;}
160
+ .stat{background:#1a1a2e;border:1px solid #2a2a4a;border-radius:12px;padding:1rem 1.5rem;text-align:center;min-width:120px;}
161
+ .stat .num{font-size:1.6rem;font-weight:700;color:#00c853;}
162
+ .stat .lbl{font-size:.75rem;color:#a0a0c0;margin-top:4px;}
163
+ .buttons{display:flex;gap:1rem;flex-wrap:wrap;justify-content:center;margin-bottom:2.5rem;}
164
+ .btn{display:inline-block;padding:.85rem 1.8rem;border-radius:10px;font-size:1rem;font-weight:600;text-decoration:none;transition:opacity .2s;}
165
+ .btn-primary{background:#40c4ff;color:#0d0d18;}
166
+ .btn-secondary{background:#1a1a2e;color:#e8e8f0;border:1px solid #40c4ff;}
167
+ .btn:hover{opacity:.85;}
168
+ .endpoints{background:#1a1a2e;border:1px solid #2a2a4a;border-radius:12px;padding:1.5rem;max-width:600px;width:100%;font-size:.85rem;}
169
+ .endpoints h3{color:#40c4ff;margin-bottom:1rem;font-size:.95rem;}
170
+ .ep{display:flex;gap:.75rem;align-items:baseline;padding:.35rem 0;border-bottom:1px solid #2a2a4a;}
171
+ .ep:last-child{border-bottom:none;}
172
+ .method{font-size:.7rem;font-weight:700;padding:2px 7px;border-radius:4px;min-width:42px;text-align:center;}
173
+ .get{background:#1e3a2e;color:#00c853;}
174
+ .post{background:#2a1e1e;color:#ff5252;}
175
+ .path{color:#e8e8f0;font-family:monospace;}
176
+ .desc{color:#a0a0c0;margin-left:auto;}
177
+ .footer{margin-top:2rem;color:#666;font-size:.8rem;text-align:center;}
178
+ </style>
179
+ </head>
180
+ <body>
181
+ <div class="badge">๐Ÿ† OpenEnv Hackathon 2026 ยท OpenEnv-Compliant</div>
182
+ <h1>๐Ÿชž ECHO <span>ULTIMATE</span></h1>
183
+ <p class="tagline">RL environment that teaches LLMs to say <em>"I don't know."</em><br>
184
+ Brier-score rewards ยท 7 domains ยท 3-phase curriculum ยท 5 calibration metrics</p>
185
+
186
+ <div class="stats">
187
+ <div class="stat"><div class="num">โˆ’77%</div><div class="lbl">ECE Reduction</div></div>
188
+ <div class="stat"><div class="num">+23ร—</div><div class="lbl">Reward Gain</div></div>
189
+ <div class="stat"><div class="num">5,800</div><div class="lbl">GRPO Steps</div></div>
190
+ <div class="stat"><div class="num">โˆ’85%</div><div class="lbl">Overconfidence</div></div>
191
+ </div>
192
+
193
+ <div class="buttons">
194
+ <a class="btn btn-primary" href="/ui">๐ŸŽฎ Try the Live Demo</a>
195
+ <a class="btn btn-secondary" href="/docs">๐Ÿ“– API Docs (Swagger)</a>
196
+ <a class="btn btn-secondary" href="/metrics">๐Ÿ“Š Live Metrics</a>
197
+ </div>
198
+
199
+ <div class="endpoints">
200
+ <h3>OpenEnv API Endpoints</h3>
201
+ <div class="ep"><span class="method post">POST</span><span class="path">/reset</span><span class="desc">Start episode โ†’ EchoObservation</span></div>
202
+ <div class="ep"><span class="method post">POST</span><span class="path">/step</span><span class="desc">Submit answer โ†’ reward + calibration</span></div>
203
+ <div class="ep"><span class="method get">GET</span><span class="path">/state</span><span class="desc">Current EchoState</span></div>
204
+ <div class="ep"><span class="method get">GET</span><span class="path">/metrics</span><span class="desc">ECE ยท MCE ยท Brier ยท Sharpness</span></div>
205
+ <div class="ep"><span class="method get">GET</span><span class="path">/fingerprint</span><span class="desc">Per-domain calibration radar</span></div>
206
+ <div class="ep"><span class="method get">GET</span><span class="path">/tasks</span><span class="desc">3 task definitions + pass thresholds</span></div>
207
+ <div class="ep"><span class="method get">GET</span><span class="path">/health</span><span class="desc">Server status</span></div>
208
+ </div>
209
+
210
+ <div class="footer">
211
+ Built by Revtiraman Tripathi &amp; Vikas Dev Pandey ยท OpenEnv Hackathon 2026
212
+ </div>
213
+ </body>
214
+ </html>""", status_code=200)
215
+
216
+
217
+ @app.get("/tasks", tags=["Tasks"])
218
+ async def list_tasks():
219
+ return _get_env().list_tasks()
220
 
221
 
222
  @app.get("/metrics", tags=["Metrics"])
223
  async def get_metrics():
224
+ return _get_env().get_metrics().to_dict()
 
225
 
226
 
227
  @app.get("/metrics/{domain}", tags=["Metrics"])
228
  async def get_domain_metrics(domain: str):
229
  if domain not in cfg.DOMAINS:
230
  raise HTTPException(404, f"Unknown domain '{domain}'. Valid: {cfg.DOMAINS}")
231
+ return _get_env().get_metrics(domain=domain).to_dict()
 
232
 
233
 
234
  @app.get("/fingerprint", tags=["Metrics"])
235
+ async def get_fingerprint():
236
  env = _get_env()
237
  profiles = env.reward_history.get_domain_profiles()
238
  return {
239
+ "domain_scores": {d: round(1.0 - r.ece, 3) for d, r in profiles.items()},
240
+ "domain_ece": {d: round(r.ece, 3) for d, r in profiles.items()},
241
  "domain_accuracy": {d: round(r.accuracy, 3) for d, r in profiles.items()},
242
+ "overall_ece": round(env.get_metrics().ece, 3),
243
  }
244
 
245
 
246
  @app.get("/history", tags=["Metrics"])
247
+ async def get_history():
248
  env = _get_env()
249
  df = env.reward_history.to_dataframe()
250
  records = df.tail(100).to_dict(orient="records") if len(df) > 0 else []
 
285
  print(f"โš ๏ธ Gradio UI not mounted: {_e}")
286
 
287
 
288
+ # โ”€โ”€ Direct runner โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
289
 
290
  if __name__ == "__main__":
291
  import uvicorn