Spaces:
Running
Running
| """ | |
| 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 | |
| def health(): | |
| return {"status": "ok", "live_llm": llm.live, "budget": llm.ledger.as_dict(), | |
| "locations": LOCATIONS} | |
| def state(): | |
| return { | |
| "tick": WORLD.tick, | |
| "agents": [a.snapshot() for a in WORLD.agents], | |
| "locations": LOCATIONS, | |
| "budget": llm.ledger.as_dict(), | |
| "live": llm.live, | |
| } | |
| def reset(req: ResetReq = Body(default=ResetReq())): | |
| global WORLD | |
| WORLD = World.bootstrap(seed=req.seed) | |
| return state() | |
| 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()} | |
| def event(req: EventReq): | |
| WORLD.inject_event(req.text) | |
| return {"ok": True, "queued": req.text} | |
| 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") | |
| 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) | |