"""FastAPI control surface for the agent (served on localhost). Start / stop the worker pool, tune the worker count live (trades desktop responsiveness for throughput), and watch GPU load + progress + the server-side queue. Config is env-seeded; the worker count is adjustable here on the fly. """ from fastapi import FastAPI, Request from fastapi.responses import HTMLResponse, JSONResponse from . import logbuf from .config import Config from .gpu import read_gpu from .worker import Worker logbuf.install() cfg = Config.from_env() worker = Worker(cfg) app = FastAPI(title="FabledCurator GPU agent") @app.middleware("http") async def _no_store(request, call_next): # The control page is a static string and the status/gpu/logs polls are # live data — never let the browser cache either, or a freshly-pulled agent # image still shows the OLD UI until a hard refresh (operator-flagged # 2026-06-30). resp = await call_next(request) resp.headers["Cache-Control"] = "no-store" return resp @app.on_event("startup") def _maybe_autostart() -> None: # With AUTO_START set, a container restart (host reboot, or `restart: # unless-stopped` after a crash) resumes the worker on its own — the slots # then ride out a still-down curator via lease backoff. Lets the agent # survive a redeploy with nobody at the desktop to click Start. if cfg.auto_start and cfg.token: worker.start() @app.get("/", response_class=HTMLResponse) def index() -> str: return _PAGE @app.post("/start") def start(): worker.start() return JSONResponse(worker.status()) @app.post("/stop") def stop(): worker.stop() return JSONResponse(worker.status()) @app.post("/concurrency") async def concurrency(request: Request): body = await request.json() worker.set_concurrency(int(body.get("value", 1))) return JSONResponse(worker.status()) @app.post("/auto") async def auto(request: Request): body = await request.json() worker.set_auto(bool(body.get("value", True))) return JSONResponse(worker.status()) @app.get("/gpu") def gpu(): # GPU meters poll this on their own fast cadence. It only reads local # nvidia-smi — no curator round-trip — so the util/VRAM bars stay live even # when /status is slow waiting on the (sometimes busy) curator queue call. return JSONResponse(read_gpu() or {}) @app.get("/logs") def logs(): return JSONResponse({"lines": list(logbuf.LINES)}) @app.get("/status") def status(): s = worker.status() s["fc_url"] = cfg.fc_url s["configured"] = bool(cfg.token) # GPU is served by /gpu (cached); /status stays light so Start/Stop never # queue behind it. try: s["queue"] = worker.client.queue_status() except Exception: s["queue"] = None return JSONResponse(s) _PAGE = """ FabledCurator · GPU agent
FabledCurator GPU agent

Server · token

Control
auto-tuning to fill the GPU · max 8
Status
state
0
active
0
processed
0
errors
0
waited out
GPU util
VRAM
queue —
Logs
waiting for activity…
"""