AK commited on
Commit
159ae71
·
1 Parent(s): 202a308

feat: FastAPI server (state/step/event/demo) + static serving

Browse files
Files changed (1) hide show
  1. backend/main.py +126 -0
backend/main.py ADDED
@@ -0,0 +1,126 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Polis API — FastAPI server that drives the simulation and serves the 3D UI.
3
+
4
+ Endpoints
5
+ ---------
6
+ GET /api/health -> liveness + whether a real OpenAI key is wired
7
+ GET /api/state -> current world snapshot
8
+ POST /api/reset -> new world from a seed
9
+ POST /api/step -> advance N ticks (live LLM or mock), returns snapshots
10
+ POST /api/event -> inject a world event ("a storm floods the harbor")
11
+ GET /api/demo -> pre-recorded run so the Space works with zero budget
12
+ GET / -> the 3D scroll site (static/index.html)
13
+
14
+ The design goal: a recruiter can open the Space and immediately watch a society
15
+ unfold via /api/demo, then flip to live mode if a key + budget are present.
16
+ """
17
+ from __future__ import annotations
18
+
19
+ import json
20
+ import os
21
+ from pathlib import Path
22
+
23
+ from fastapi import FastAPI, Body
24
+ from fastapi.middleware.cors import CORSMiddleware
25
+ from fastapi.responses import FileResponse, JSONResponse
26
+ from fastapi.staticfiles import StaticFiles
27
+ from pydantic import BaseModel
28
+
29
+ from .llm import llm, BudgetExceeded
30
+ from .world import World, LOCATIONS
31
+
32
+ ROOT = Path(__file__).resolve().parent.parent
33
+ STATIC = ROOT / "static"
34
+ DEMO_FILE = ROOT / "data" / "demo_run.json"
35
+
36
+ app = FastAPI(title="Polis", version="1.0.0",
37
+ description="A living society of generative AI agents.")
38
+ app.add_middleware(CORSMiddleware, allow_origins=["*"], allow_methods=["*"],
39
+ allow_headers=["*"])
40
+
41
+ WORLD = World.bootstrap()
42
+
43
+
44
+ class StepReq(BaseModel):
45
+ ticks: int = 1
46
+
47
+
48
+ class EventReq(BaseModel):
49
+ text: str
50
+
51
+
52
+ class ResetReq(BaseModel):
53
+ seed: int = 42
54
+
55
+
56
+ @app.get("/api/health")
57
+ def health():
58
+ return {"status": "ok", "live_llm": llm.live, "budget": llm.ledger.as_dict(),
59
+ "locations": LOCATIONS}
60
+
61
+
62
+ @app.get("/api/state")
63
+ def state():
64
+ return {
65
+ "tick": WORLD.tick,
66
+ "agents": [a.snapshot() for a in WORLD.agents],
67
+ "locations": LOCATIONS,
68
+ "budget": llm.ledger.as_dict(),
69
+ "live": llm.live,
70
+ }
71
+
72
+
73
+ @app.post("/api/reset")
74
+ def reset(req: ResetReq = Body(default=ResetReq())):
75
+ global WORLD
76
+ WORLD = World.bootstrap(seed=req.seed)
77
+ return state()
78
+
79
+
80
+ @app.post("/api/step")
81
+ def step(req: StepReq = Body(default=StepReq())):
82
+ snapshots = []
83
+ n = max(1, min(20, req.ticks))
84
+ for _ in range(n):
85
+ try:
86
+ snapshots.append(WORLD.step())
87
+ except BudgetExceeded as exc:
88
+ return JSONResponse(
89
+ status_code=402,
90
+ content={"error": str(exc), "budget": llm.ledger.as_dict(),
91
+ "snapshots": snapshots},
92
+ )
93
+ return {"snapshots": snapshots, "budget": llm.ledger.as_dict()}
94
+
95
+
96
+ @app.post("/api/event")
97
+ def event(req: EventReq):
98
+ WORLD.inject_event(req.text)
99
+ return {"ok": True, "queued": req.text}
100
+
101
+
102
+ @app.get("/api/demo")
103
+ def demo():
104
+ if DEMO_FILE.exists():
105
+ return json.loads(DEMO_FILE.read_text())
106
+ return JSONResponse(status_code=404,
107
+ content={"error": "demo_run.json not generated yet"})
108
+
109
+
110
+ # ---- static site -------------------------------------------------------------
111
+ if STATIC.exists():
112
+ app.mount("/static", StaticFiles(directory=str(STATIC)), name="static")
113
+
114
+
115
+ @app.get("/")
116
+ def index():
117
+ idx = STATIC / "index.html"
118
+ if idx.exists():
119
+ return FileResponse(str(idx))
120
+ return {"message": "Polis API running. Build static/index.html for the UI."}
121
+
122
+
123
+ if __name__ == "__main__":
124
+ import uvicorn
125
+ uvicorn.run("backend.main:app", host="0.0.0.0",
126
+ port=int(os.getenv("PORT", "7860")), reload=False)