""" Polis API — FastAPI server that drives the simulation and serves the 3D UI. Endpoints --------- GET /api/health -> liveness + whether a real OpenAI key is wired GET /api/state -> current world snapshot POST /api/reset -> new world from a seed POST /api/step -> advance N ticks (live LLM or mock), returns snapshots POST /api/event -> inject a world event ("a storm floods the harbor") GET /api/demo -> pre-recorded run so the Space works with zero budget GET / -> the 3D scroll site (static/index.html) The design goal: a recruiter can open the Space and immediately watch a society unfold via /api/demo, then flip to live mode if a key + budget are present. """ from __future__ import annotations import json import os from pathlib import Path from fastapi import FastAPI, Body from fastapi.middleware.cors import CORSMiddleware from fastapi.responses import FileResponse, JSONResponse from fastapi.staticfiles import StaticFiles from pydantic import BaseModel from .llm import llm, BudgetExceeded from .world import World, LOCATIONS ROOT = Path(__file__).resolve().parent.parent STATIC = ROOT / "static" DEMO_FILE = ROOT / "data" / "demo_run.json" app = FastAPI(title="Polis", version="1.0.0", description="A living society of generative AI agents.") app.add_middleware(CORSMiddleware, allow_origins=["*"], allow_methods=["*"], allow_headers=["*"]) WORLD = World.bootstrap() class StepReq(BaseModel): ticks: int = 1 class EventReq(BaseModel): text: str class ResetReq(BaseModel): seed: int = 42 @app.get("/api/health") def health(): return {"status": "ok", "live_llm": llm.live, "budget": llm.ledger.as_dict(), "locations": LOCATIONS} @app.get("/api/state") def state(): return { "tick": WORLD.tick, "agents": [a.snapshot() for a in WORLD.agents], "locations": LOCATIONS, "budget": llm.ledger.as_dict(), "live": llm.live, } @app.post("/api/reset") def reset(req: ResetReq = Body(default=ResetReq())): global WORLD WORLD = World.bootstrap(seed=req.seed) return state() @app.post("/api/step") def step(req: StepReq = Body(default=StepReq())): snapshots = [] n = max(1, min(20, req.ticks)) for _ in range(n): try: snapshots.append(WORLD.step()) except BudgetExceeded as exc: return JSONResponse( status_code=402, content={"error": str(exc), "budget": llm.ledger.as_dict(), "snapshots": snapshots}, ) return {"snapshots": snapshots, "budget": llm.ledger.as_dict()} @app.post("/api/event") def event(req: EventReq): WORLD.inject_event(req.text) return {"ok": True, "queued": req.text} @app.get("/api/demo") def demo(): if DEMO_FILE.exists(): return json.loads(DEMO_FILE.read_text()) return JSONResponse(status_code=404, content={"error": "demo_run.json not generated yet"}) # ---- static site ------------------------------------------------------------- if STATIC.exists(): app.mount("/static", StaticFiles(directory=str(STATIC)), name="static") @app.get("/") def index(): idx = STATIC / "index.html" if idx.exists(): return FileResponse(str(idx)) return {"message": "Polis API running. Build static/index.html for the UI."} if __name__ == "__main__": import uvicorn uvicorn.run("backend.main:app", host="0.0.0.0", port=int(os.getenv("PORT", "7860")), reload=False)