Algio-1452's picture
feat: unified full-stack architecture (React + FastAPI + Docker)
30596cf
Raw
History Blame Contribute Delete
4.99 kB
"""Final Unified Server: OpenEnv API + React Visualizer + Live Streaming."""
import os
import json
import time
import logging
import asyncio
from pathlib import Path
from typing import Any, Optional
from dotenv import load_dotenv
from fastapi import FastAPI, HTTPException, Request
from fastapi.staticfiles import StaticFiles
from fastapi.middleware.cors import CORSMiddleware
from fastapi.responses import StreamingResponse
from openenv.core.env_server import create_app
# Import core environment and visualizer logic
from server.environment import DEFAULT_ENV_NAME, NationOpenEnv
from server.models import NationAction, NationObservation
from server.live_runner import LiveRun, LiveRunManager, build_config_dict
from server.visualizer_server import StartRunBody, StartRunResponse, _RULE_BASED_MODES, _llm_mode_entry, _json_default
load_dotenv()
LOG = logging.getLogger(__name__)
# --- 1. INITIALIZE OPENENV CORE ---
app = create_app(
NationOpenEnv,
NationAction,
NationObservation,
env_name=DEFAULT_ENV_NAME,
max_concurrent_envs=int(os.getenv("MAX_CONCURRENT_ENVS", "4")),
)
# Add CORS for local development
app.add_middleware(
CORSMiddleware,
allow_origins=["*"],
allow_methods=["*"],
allow_headers=["*"],
)
# --- 2. INITIALIZE VISUALIZER MANAGER ---
manager = LiveRunManager()
# --- 3. VISUALIZER API ROUTES (/api/*) ---
@app.get("/api/health")
def health() -> dict[str, Any]:
return {"ok": True, "ts": time.time()}
@app.get("/api/config")
def config() -> dict[str, Any]:
return build_config_dict()
@app.get("/api/modes")
def modes() -> dict[str, Any]:
return {"modes": [_llm_mode_entry(), *_RULE_BASED_MODES]}
@app.get("/api/runs")
def list_runs() -> dict[str, Any]:
return {"runs": manager.list_runs()}
@app.post("/api/runs", response_model=StartRunResponse)
def start_run(body: StartRunBody) -> StartRunResponse:
if body.mode == "llm":
token = os.environ.get("HF_TOKEN")
if not token:
raise HTTPException(status_code=400, detail="LLM mode requires HF_TOKEN.")
model_id = body.model_id or os.environ.get("HF_MODEL_ID")
else:
token = None
model_id = body.model_id
try:
run = manager.create(
mode=body.mode,
model_id=model_id,
seed=body.seed,
max_rounds=body.max_rounds,
temperature=body.temperature,
token=token,
)
return StartRunResponse(
run_id=run.run_id,
mode=run.mode,
policy=run.policy,
seed=run.seed,
max_rounds=run.max_rounds,
config=run.config,
)
except ValueError as exc:
raise HTTPException(status_code=400, detail=str(exc))
@app.get("/api/runs/{run_id}/snapshot")
def snapshot(run_id: str) -> dict[str, Any]:
run = manager.get(run_id)
if run is None:
raise HTTPException(status_code=404, detail=f"Unknown run {run_id}")
return run.snapshot()
@app.get("/api/runs/{run_id}/stream")
async def stream(run_id: str, request: Request) -> StreamingResponse:
run = manager.get(run_id)
if run is None:
raise HTTPException(status_code=404, detail=f"Unknown run {run_id}")
async def _sse_iterator():
queue = run.subscribe()
try:
last_heartbeat = time.time()
while True:
if await request.is_disconnected():
break
# Drain queue (non-blocking in executor)
event = await asyncio.get_event_loop().run_in_executor(
None, lambda: (None if queue.empty() else queue.get())
)
if event is not None:
event_type = event.get("type", "message")
payload = event.get("data", {})
body = json.dumps(payload, default=_json_default)
yield f"event: {event_type}\ndata: {body}\n\n"
if event_type == "done":
break
continue
if time.time() - last_heartbeat > 15:
yield ": keep-alive\n\n"
last_heartbeat = time.time()
await asyncio.sleep(0.1)
finally:
run.unsubscribe(queue)
return StreamingResponse(
_sse_iterator(),
media_type="text/event-stream",
headers={"Cache-Control": "no-cache", "Connection": "keep-alive"},
)
# --- 4. STATIC FRONTEND MOUNTING ---
dist_path = Path(__file__).parent.parent / "visualizer" / "dist"
if dist_path.exists():
app.mount("/", StaticFiles(directory=str(dist_path), html=True), name="visualizer")
LOG.info(f"⚛️ React Visualizer mounted from {dist_path}")
else:
LOG.warning(f"⚠️ Visualizer build not found at {dist_path}")
if __name__ == "__main__":
import uvicorn
uvicorn.run(app, host="0.0.0.0", port=int(os.getenv("PORT", "8000")))