Kr0issant
reduce tasks (6 to 3)
ceed85e
Raw
History Blame Contribute Delete
4.62 kB
"""OpenEnv-compatible HTTP server for the ATC RL Environment.
Exposes /reset, /step, and /state endpoints wrapping ATCEnv.
Designed to run inside a Docker container or HF Space on port 7860.
"""
from typing import Any, Optional
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
from pydantic import BaseModel
from rl_env.environment import ATCEnv
from rl_env.models import ATCAction, ATCObservation, ATCState
AVAILABLE_TASKS = [
"single_approach",
"multi_departure",
"traffic_pattern",
]
app = FastAPI(
title="ATC RL Environment",
description="OpenEnv-compatible Air Traffic Control RL Environment",
version="1.0.0",
)
app.add_middleware(
CORSMiddleware,
allow_origins=["*"],
allow_methods=["*"],
allow_headers=["*"],
)
env = ATCEnv()
# ---------------------------------------------------------------------------
# Request / response schemas
# ---------------------------------------------------------------------------
class ResetRequest(BaseModel):
task: str = "single_approach"
seed: Optional[int] = None
class ResetResponse(BaseModel):
observation: ATCObservation
done: bool = False
info: dict[str, Any] = {}
class StepRequest(BaseModel):
commands: list[str] = []
thought: Optional[str] = None
class StepResponse(BaseModel):
observation: ATCObservation
reward: float
done: bool
truncated: bool
info: dict[str, Any] = {}
class TaskInfo(BaseModel):
name: str
description: str
difficulty: str
aircraft_count: int
# ---------------------------------------------------------------------------
# Endpoints
# ---------------------------------------------------------------------------
@app.get("/")
async def health():
return {"status": "ok", "environment": "atc-rl-env", "version": "1.0.0"}
@app.post("/reset", response_model=ResetResponse)
async def reset(request: ResetRequest = ResetRequest()):
"""Reset the environment and start a new episode."""
task = request.task if request.task in AVAILABLE_TASKS else "single_approach"
observation, info = env.reset(seed=request.seed, task=task)
return ResetResponse(observation=observation, done=False, info=info)
@app.post("/step", response_model=StepResponse)
async def step(request: StepRequest):
"""Execute one step with the given ATC commands."""
action = ATCAction(commands=request.commands, thought=request.thought)
observation, reward, done, truncated, info = env.step(action)
serializable_info = _make_serializable(info)
return StepResponse(
observation=observation,
reward=round(reward, 4),
done=done,
truncated=truncated,
info=serializable_info,
)
@app.get("/state", response_model=ATCState)
async def state():
"""Return current episode metadata."""
return env.state
@app.get("/tasks", response_model=list[TaskInfo])
async def list_tasks():
"""List all available tasks with metadata."""
task_metadata = {
"single_approach": TaskInfo(
name="single_approach",
description="Guide a single aircraft to land safely",
difficulty="easy",
aircraft_count=1,
),
"multi_departure": TaskInfo(
name="multi_departure",
description="Sequence three departures through a shared runway",
difficulty="medium",
aircraft_count=3,
),
"traffic_pattern": TaskInfo(
name="traffic_pattern",
description="Manage four aircraft arriving from cardinal directions",
difficulty="medium",
aircraft_count=4,
),
}
return [task_metadata[t] for t in AVAILABLE_TASKS]
def main() -> None:
"""Entry point for console script and direct execution."""
import uvicorn
uvicorn.run("server.app:app", host="0.0.0.0", port=7860)
def _make_serializable(info: dict[str, Any]) -> dict[str, Any]:
"""Ensure all values in the info dict are JSON-serializable."""
clean: dict[str, Any] = {}
for key, value in info.items():
if key == "events":
clean[key] = [
{
k: str(v) if not isinstance(v, (int, float, bool, str, type(None))) else v
for k, v in evt.items()
}
for evt in (value or [])
]
elif key == "reward_breakdown":
clean[key] = {k: round(v, 4) for k, v in (value or {}).items()}
else:
clean[key] = value
return clean
if __name__ == "__main__":
main()