Spaces:
Sleeping
Sleeping
File size: 8,823 Bytes
92d87c0 b96f305 92d87c0 8be219d b96f305 92d87c0 8be219d 92d87c0 b96f305 92d87c0 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 | """FastAPI server for the Space Manufacturing RL OpenEnv environment."""
from __future__ import annotations
import asyncio
import os
import sys
import uvicorn
from pathlib import Path
from typing import Any, Dict, Optional
# Make 'SpaceFactory' importable regardless of working directory.
# server/app.py is at <root>/SpaceFactory/server/app.py β parentΓ3 = <root>
_pkg_parent = str(Path(__file__).resolve().parent.parent.parent)
if _pkg_parent not in sys.path:
sys.path.insert(0, _pkg_parent)
try:
from fastapi import FastAPI, HTTPException, Query
from fastapi.middleware.cors import CORSMiddleware
from fastapi.responses import FileResponse, HTMLResponse
from fastapi.staticfiles import StaticFiles
from pydantic import BaseModel
except ImportError as exc:
raise SystemExit(f"FastAPI is required: pip install fastapi uvicorn\n{exc}") from exc
from .ui_demo import ManufacturingUIDemo
from SpaceFactory.env import ManufacturingTaskEnv
from SpaceFactory.models import ManufacturingAction, ManufacturingObservation
# ββ app ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
app = FastAPI(
title="Space Manufacturing RL",
description="Custom space manufacturing frontend with mounted OpenEnv API routes.",
version="0.1.0",
)
app.add_middleware(
CORSMiddleware,
allow_origins=["http://localhost:4173", "http://127.0.0.1:4173", "http://localhost:5173", "http://127.0.0.1:5173"],
allow_methods=["*"],
allow_headers=["*"],
)
@app.on_event("startup")
async def _suppress_win32_connection_reset() -> None:
"""Suppress spurious WinError 10054 from asyncio ProactorEventLoop on Windows."""
if sys.platform != "win32":
return
loop = asyncio.get_event_loop()
def _handler(loop: asyncio.AbstractEventLoop, context: dict) -> None:
exc = context.get("exception")
if isinstance(exc, ConnectionResetError):
return # benign Windows ProactorEventLoop artifact β silently ignore
loop.default_exception_handler(context)
loop.set_exception_handler(_handler)
# Single shared environment instance (stateful per process)
_env = ManufacturingTaskEnv(task_name="easy")
# UI demo instance
_ui_demo = ManufacturingUIDemo()
# Mount built frontend static assets if dist/ exists
_here = Path(__file__).resolve().parent
_dist = _here.parent / "frontend" / "dist"
_assets = _dist / "assets"
if _assets.exists():
app.mount("/assets", StaticFiles(directory=str(_assets)), name="assets")
app.mount("/web/assets", StaticFiles(directory=str(_assets)), name="web-assets")
# ββ helpers ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
class StepRequest(BaseModel):
"""Wrapper for a single environment step action."""
action: Dict[str, Any]
timeout_s: Optional[float] = 30.0
# ββ OpenEnv core endpoints βββββββββββββββββββββββββββββββββββββββββββββββββββββ
@app.post("/reset")
def reset(task: Optional[str] = Query(default=None)):
"""Reset the episode, optionally switching to a different task."""
if task is not None:
try:
_env.set_task(task)
except ValueError as exc:
raise HTTPException(status_code=400, detail=str(exc)) from exc
return _env.reset().model_dump()
@app.post("/step")
def step(body: StepRequest):
"""Advance the environment by one step."""
try:
action = ManufacturingAction.model_validate(body.action)
except Exception as exc:
raise HTTPException(status_code=422, detail=f"Invalid action: {exc}") from exc
obs, reward, done, info = _env.step(action)
return {"observation": obs.model_dump(), "reward": reward.model_dump(),
"done": done, "info": info}
@app.get("/state")
def state():
"""Return the full current environment state snapshot."""
return _env.state().model_dump()
@app.get("/tasks")
def list_tasks():
"""List all available task presets."""
return ManufacturingTaskEnv.list_tasks()
@app.get("/schema")
def schema():
"""Return action, observation, and state JSON schemas."""
return {
"action_space": ManufacturingAction.model_json_schema(),
"observation_space": ManufacturingObservation.model_json_schema(),
"state": _env.state().model_json_schema(),
}
@app.get("/health")
def health():
"""Health probe used by OpenEnv validators."""
return {"status": "healthy"}
@app.get("/metadata")
def metadata():
"""Environment metadata required by the OpenEnv runtime standard."""
return {
"name": "space_manufacturing",
"description": (
"Orbital manufacturing platform coordination environment "
"for production and delivery scheduling."
),
"version": "0.1.0",
"tasks": list(ManufacturingTaskEnv.list_tasks().keys()),
}
# ββ UI demo endpoints ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
@app.get("/api/ui/demo")
def ui_demo_snapshot():
"""Return the current UI demo snapshot (used by the React frontend)."""
return _ui_demo.snapshot()
@app.post("/api/ui/demo/reset")
def ui_demo_reset(task_name: str = "medium"):
"""Reset the UI demo to a specific task."""
if task_name not in ManufacturingTaskEnv.list_tasks():
raise HTTPException(status_code=400, detail=f"Unknown task: {task_name}")
return _ui_demo.reset(task_name)
@app.post("/api/ui/demo/step")
def ui_demo_step():
"""Advance the UI demo by one heuristic step."""
return _ui_demo.step()
# ββ Frontend SPA routes ββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
@app.get("/web", response_class=FileResponse, include_in_schema=False)
def serve_web_index():
"""Serve the React SPA index page."""
index = _dist / "index.html"
if index.exists():
return FileResponse(str(index))
return HTMLResponse(_fallback_html())
@app.get("/web/{full_path:path}", include_in_schema=False)
def serve_web_app(full_path: str):
"""Serve built SPA assets, falling back to index.html for client-side routing."""
candidate = _dist / full_path
if candidate.exists() and candidate.is_file():
return FileResponse(str(candidate))
index = _dist / "index.html"
if index.exists():
return FileResponse(str(index))
return HTMLResponse(_fallback_html())
def _fallback_html() -> str:
"""Minimal status page shown when the frontend dist/ hasn't been built yet."""
s = _env.state()
platforms_html = "".join(
f"<li>Platform {p.id}: energy={p.energy:.1f}, "
f"mat={p.material_stock:.1f}, comp={p.component_stock:.1f}, "
f"prod={p.product_stock}, last={p.last_action}</li>"
for p in s.platforms
)
return (
f"<!DOCTYPE html><html><head><title>Space Manufacturing RL</title>"
f"<style>body{{font-family:monospace;background:#06111f;color:#c8deff;padding:24px}}"
f"h1{{color:#7cf7c9}}li{{margin:4px 0}}"
f".warn{{color:#ffb86b;font-size:.85rem;margin-top:16px}}</style></head><body>"
f"<h1>Space Manufacturing RL β {s.task_name}</h1>"
f"<p>Step: {s.step_count} / {s.max_steps} | "
f"Total reward: {s.total_reward:.2f} | Done: {s.done}</p>"
f"<h2>Platforms</h2><ul>{platforms_html}</ul>"
f"<p class='warn'>Frontend not built. Run: "
f"cd frontend && npm install && npm run build</p>"
f"</body></html>"
)
# ββ Entry point ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
def main() -> None:
"""Start the uvicorn server with Windows-compatible event loop policy."""
# Windows ProactorEventLoop raises spurious ConnectionResetError (WinError 10054)
# when clients close keep-alive connections. Selector loop avoids the issue.
if sys.platform == "win32":
asyncio.set_event_loop_policy(asyncio.WindowsSelectorEventLoopPolicy())
uvicorn.run("SpaceFactory.server.app:app", host="0.0.0.0", port=8000, reload=False)
if __name__ == "__main__":
main()
|