File size: 3,442 Bytes
b89c27d
b0b140b
b89c27d
 
 
 
b0b140b
 
b89c27d
b0b140b
 
 
 
 
b89c27d
b0b140b
 
 
 
 
b89c27d
962ad43
 
 
b89c27d
b0b140b
 
 
 
 
 
 
4a535ea
 
 
 
b0b140b
 
b89c27d
 
 
 
 
 
 
b0b140b
b89c27d
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
b0b140b
b89c27d
b0b140b
 
 
b89c27d
b0b140b
 
 
 
 
 
 
 
 
 
 
 
 
 
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
"""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")

        @app.get("/")
        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.
        @app.get("/{path:path}")
        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()