Spaces:
Sleeping
Sleeping
| """FastAPI application for LandscapeForge. | |
| Mounts: | |
| - OpenEnv endpoints (`/reset`, `/step`, `/schema`, `/ws`, etc.) via create_app | |
| - Frontend API helpers at `/api/*` (see `api_routes.py`) | |
| - React SPA at `/` (built to `/app/env/frontend/dist` via Vite) | |
| """ | |
| from pathlib import Path | |
| try: | |
| from openenv.core.env_server.http_server import create_app | |
| except Exception as e: # pragma: no cover | |
| raise ImportError( | |
| "openenv is required for the web interface. Install dependencies via 'uv sync'." | |
| ) from e | |
| try: | |
| from ..models import LandscapeforgeAction, LandscapeforgeObservation | |
| from .landscapeforge_environment import LandscapeforgeEnvironment | |
| from .api_routes import router as lf_api_router | |
| except (ModuleNotFoundError, ImportError): | |
| from models import LandscapeforgeAction, LandscapeforgeObservation # type: ignore | |
| from server.landscapeforge_environment import LandscapeforgeEnvironment # type: ignore | |
| from server.api_routes import router as lf_api_router # type: ignore | |
| app = create_app( | |
| LandscapeforgeEnvironment, | |
| LandscapeforgeAction, | |
| LandscapeforgeObservation, | |
| env_name="landscapeforge", | |
| # Single shared env for plain HTTP /reset + /step so the API playground | |
| # (which doesn't carry session IDs) operates on consistent state. | |
| # WebSocket clients still get fresh instances via SUPPORTS_CONCURRENT_SESSIONS. | |
| max_concurrent_envs=1, | |
| ) | |
| # Frontend-facing API (landscape, baseline_race, arena, llm_run) | |
| app.include_router(lf_api_router) | |
| # ---- React SPA serving ---- | |
| # The Dockerfile builds the frontend into `frontend/dist/`. Locate it relative | |
| # to this file, and serve it under `/` with a SPA fallback to index.html. | |
| try: | |
| from fastapi.staticfiles import StaticFiles | |
| from fastapi.responses import FileResponse | |
| _here = Path(__file__).resolve().parent.parent | |
| _dist = _here / "frontend" / "dist" | |
| if _dist.is_dir(): | |
| # Assets (hashed css/js) at /assets, vite dist root items at / | |
| app.mount("/assets", StaticFiles(directory=_dist / "assets"), | |
| name="lf-assets") | |
| def _index(): | |
| return FileResponse(_dist / "index.html") | |
| # SPA fallback: any route the FastAPI router doesn't own, serve | |
| # index.html so client-side routing (if any) works. | |
| def _spa_fallback(path: str): | |
| # Don't swallow API/OpenEnv routes — FastAPI matches those first | |
| # because they're registered before this catch-all. | |
| candidate = _dist / path | |
| if candidate.is_file(): | |
| return FileResponse(candidate) | |
| return FileResponse(_dist / "index.html") | |
| else: | |
| import logging | |
| logging.getLogger(__name__).warning( | |
| "Frontend dist not found at %s — SPA routes disabled.", _dist | |
| ) | |
| except Exception as _e: | |
| import logging | |
| logging.getLogger(__name__).warning("SPA mount failed: %s", _e) | |
| def main(): | |
| """Entry point for direct execution.""" | |
| import argparse | |
| import os | |
| import uvicorn | |
| parser = argparse.ArgumentParser() | |
| parser.add_argument("--port", type=int, | |
| default=int(os.environ.get("PORT", 8000))) | |
| parser.add_argument("--host", type=str, default="0.0.0.0") | |
| args = parser.parse_args() | |
| uvicorn.run(app, host=args.host, port=args.port) | |
| if __name__ == "__main__": | |
| main() | |