polis / backend /main.py
AK
feat: FastAPI server (state/step/event/demo) + static serving
159ae71
Raw
History Blame Contribute Delete
3.6 kB
"""
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)