Spaces:
Sleeping
Sleeping
GGOisW
fix(grader): use 0.01 and 0.99 bounds to definitively prevent edge rounding to 1.0 by OpenEnv validators
8d47366 | """ | |
| FastAPI application exposing CryptoRiskEnv as an OpenEnv-compliant API. | |
| Endpoints (OpenEnv standard): | |
| POST /reset — Reset the environment for a given task | |
| POST /step — Submit an action and advance one step | |
| GET /state — Retrieve the full environment state | |
| Additional endpoints: | |
| GET /tasks — List available evaluation tasks | |
| POST /grade — Grade the completed episode | |
| GET /health — Health check (returns 200) | |
| GET / — Root endpoint (redirects to docs) | |
| """ | |
| from __future__ import annotations | |
| import os | |
| from fastapi import FastAPI, HTTPException, Body | |
| from fastapi.middleware.cors import CORSMiddleware | |
| from fastapi.responses import RedirectResponse | |
| from server.env import CryptoRiskEnv | |
| from server.models import ( | |
| Action, | |
| GradeResponse, | |
| ResetRequest, | |
| StateResponse, | |
| StepResponse, | |
| TaskListResponse, | |
| Observation, | |
| ) | |
| from server.tasks import create_env_for_task, get_task_list, grade_task | |
| # --------------------------------------------------------------------------- | |
| # App | |
| # --------------------------------------------------------------------------- | |
| app = FastAPI( | |
| title="CryptoRiskEnv — OpenEnv", | |
| description=( | |
| "An OpenEnv-compliant environment for evaluating LLM agents on " | |
| "cryptocurrency risk management discipline. Tests market-data parsing, " | |
| "risk-constrained trading, and profitable decision-making under volatility." | |
| ), | |
| version="1.0.0", | |
| ) | |
| app.add_middleware( | |
| CORSMiddleware, | |
| allow_origins=["*"], | |
| allow_methods=["*"], | |
| allow_headers=["*"], | |
| ) | |
| # --------------------------------------------------------------------------- | |
| # Shared state (single-session for hackathon simplicity) | |
| # --------------------------------------------------------------------------- | |
| _env: CryptoRiskEnv | None = None | |
| def _get_env() -> CryptoRiskEnv: | |
| global _env | |
| if _env is None: | |
| raise HTTPException( | |
| status_code=400, | |
| detail="Environment not initialised. Call /reset first.", | |
| ) | |
| return _env | |
| # --------------------------------------------------------------------------- | |
| # Endpoints | |
| # --------------------------------------------------------------------------- | |
| def root(): | |
| """Root endpoint — redirect to API docs.""" | |
| return RedirectResponse(url="/docs") | |
| def health(): | |
| """Health check endpoint.""" | |
| return {"status": "ok", "environment": "CryptoRiskEnv", "version": "1.0.0"} | |
| def list_tasks(): | |
| """List all available evaluation tasks.""" | |
| return TaskListResponse(tasks=get_task_list()) | |
| def reset(request: ResetRequest | None = Body(default=None)): | |
| """Reset the environment for the specified task and return the initial observation. | |
| If no request body is provided, defaults to the 'easy' task. | |
| This ensures compatibility with automated validators that ping /reset. | |
| """ | |
| global _env | |
| # Handle empty body (validator ping) | |
| task_id = request.task_id if request else "easy" | |
| try: | |
| _env = create_env_for_task(task_id) | |
| except ValueError as exc: | |
| raise HTTPException(status_code=400, detail=str(exc)) | |
| obs = _env.reset() | |
| return obs | |
| def step(action: Action = Body(...)): | |
| """Submit an action and advance the environment by one step.""" | |
| env = _get_env() | |
| if env.done: | |
| raise HTTPException( | |
| status_code=400, | |
| detail="Episode is done. Call /reset to start a new episode.", | |
| ) | |
| try: | |
| obs, reward, done, info = env.step(action) | |
| except Exception as exc: | |
| raise HTTPException(status_code=400, detail=str(exc)) | |
| return StepResponse(observation=obs, reward=reward, done=done, info=info) | |
| def state(): | |
| """Retrieve the full current environment state.""" | |
| env = _get_env() | |
| s = env.state() | |
| return StateResponse( | |
| observation=Observation(**s["observation"]), | |
| portfolio=s["portfolio"], | |
| step_count=s["step_count"], | |
| done=s["done"], | |
| task_id=s["task_id"], | |
| episode_metrics=s["episode_metrics"], | |
| info=s["info"], | |
| ) | |
| def grade(): | |
| """Grade the current (completed) episode.""" | |
| env = _get_env() | |
| if not env.done: | |
| raise HTTPException( | |
| status_code=400, | |
| detail="Episode is not done yet. Complete all steps before grading.", | |
| ) | |
| result = grade_task(env) | |
| # Final safety net: ensure score is strictly in (0, 1) before Pydantic validation | |
| import math | |
| score = float(result.get("score", 0.5)) | |
| if not math.isfinite(score) or score <= 0.01 or score >= 0.99: | |
| score = max(0.01, min(0.99, score if math.isfinite(score) else 0.5)) | |
| result["score"] = score | |
| return GradeResponse(**result) | |
| def main(): | |
| """Entry point for the server as required by OpenEnv validator.""" | |
| import uvicorn | |
| port = int(os.environ.get("PORT", "7860")) | |
| uvicorn.run(app, host="0.0.0.0", port=port) | |
| if __name__ == "__main__": | |
| main() | |