Spaces:
Running on Zero
Running on Zero
Download app/main.py from RealFalconsAI/DecisionLab: direct link, hf CLI and curl.
- Browser
- Download file 4.96 kB
-
https://huggingface.co/spaces/RealFalconsAI/DecisionLab/resolve/main/app/main.py
- Command line
-
hf download hf://spaces/RealFalconsAI/DecisionLab/app/main.py
-
curl -L -o main.py https://huggingface.co/spaces/RealFalconsAI/DecisionLab/resolve/main/app/main.py
4.96 kB
| """DecisionLab: every LightDec (FalconDec) and Arthur model found in the models folder, plus Laya, side by side. Serves the web UI and a small JSON API on port 9910.""" | |
| from __future__ import annotations | |
| import os | |
| from contextlib import asynccontextmanager | |
| import platform | |
| import threading | |
| import time | |
| from pathlib import Path | |
| from typing import Any | |
| import torch | |
| from fastapi import FastAPI, HTTPException | |
| from fastapi.responses import HTMLResponse | |
| from fastapi.staticfiles import StaticFiles | |
| from pydantic import BaseModel | |
| from .assets import stamp_assets | |
| from .demos import DEMOS, GROUPS | |
| from .models import BACKENDS, gpu_label, load_all | |
| from .security import Gate, SecurityMiddleware | |
| from .validation import validate_models, validate_questions | |
| STATIC = Path(__file__).parent / "static" | |
| MAX_OPTIONS = int(os.getenv("MAX_OPTIONS", "40")) | |
| MAX_BODY_BYTES = int(os.getenv("MAX_BODY_BYTES", str(256 * 1024))) | |
| DECIDE_SLOTS = Gate(int(os.getenv("MAX_PENDING_DECIDES", "4"))) # DL-SA-004 | |
| # DL-SA-007: no /docs, /redoc or /openapi.json | |
| VERSION = "2.3.1" | |
| PAGE = stamp_assets((STATIC / "index.html").read_text(encoding="utf-8"), VERSION) | |
| async def lifespan(_app): | |
| """Start loading every model in the background when the server starts (replaces the deprecated on_event).""" | |
| threading.Thread(target=lambda: load_all(), name="model-loader", daemon=True).start() | |
| yield | |
| app = FastAPI(title="DecisionLab", version=VERSION, docs_url=None, redoc_url=None, openapi_url=None, lifespan=lifespan) | |
| app.mount("/static", StaticFiles(directory=STATIC), name="static") | |
| # Body limit and security headers on the lab's own routes (no token auth: operator ruling 2026-09-28) | |
| app.add_middleware(SecurityMiddleware, max_body=MAX_BODY_BYTES) | |
| def index() -> HTMLResponse: | |
| # Asset URLs carry the release, so browsers never reuse app.js / app.css from an older release. | |
| return HTMLResponse(PAGE) | |
| def health() -> dict: | |
| return {"ok": True} | |
| def status() -> dict: | |
| gpu = gpu_label() | |
| return { | |
| "models": {k: b.describe() for k, b in BACKENDS.items()}, | |
| "order": list(BACKENDS), | |
| "env": {"torch": torch.__version__, "python": platform.python_version(), "gpu": gpu, | |
| "threads": torch.get_num_threads(), | |
| "models_dir": os.getenv("MODELS_DIR") or "/models"}, | |
| } | |
| def demos(group: str | None = None) -> list[dict]: | |
| return [d for d in DEMOS if d["group"] == group] if group else DEMOS | |
| def groups() -> list[dict]: | |
| return [dict(g, demos=sum(d["group"] == g["id"] for d in DEMOS), | |
| questions=sum(len(d["questions"]) for d in DEMOS if d["group"] == g["id"])) for g in GROUPS] | |
| def reload(key: str) -> dict: | |
| b = BACKENDS.get(key) | |
| if not b: | |
| raise HTTPException(404, f"unknown model '{key}'") | |
| b.start_load() # DL-SA-005: a second request while loading starts nothing | |
| return {"status": "loading"} | |
| class DecideRequest(BaseModel): | |
| state: Any | |
| questions: dict[str, dict] | |
| models: list[str] | None = None # every model when omitted | |
| class Busy(Exception): | |
| """Too many decisions are already running.""" | |
| def run_decision(state, questions: dict, models: list[str] | None = None) -> dict: | |
| """One decision, run on the requested models (every model if None). Shared by /api/decide and the Gradio app. | |
| Raises ValueError for an invalid request and Busy when the server is at its limit.""" | |
| validate_questions(questions, MAX_OPTIONS) | |
| keys = validate_models(models, list(BACKENDS)) # DL-SA-008 | |
| if not DECIDE_SLOTS.try_enter(): | |
| raise Busy("DecisionLab is busy with other requests. Try again in a moment.") | |
| try: | |
| return RUN_MODELS(state, questions, keys) | |
| finally: | |
| DECIDE_SLOTS.leave() | |
| def run_models(state, questions: dict, keys: list[str]) -> dict: | |
| """Run the models one after another (they never compete for the device); each model times itself.""" | |
| out, t0 = {}, time.perf_counter() | |
| for key in keys: | |
| try: | |
| out[key] = BACKENDS[key].decide(state, questions) | |
| except Exception as exc: | |
| out[key] = {"error": f"{type(exc).__name__}: {exc}"[:500]} | |
| return {"results": out, "server_ms": round((time.perf_counter() - t0) * 1000, 1)} | |
| # The model-running step. The Hugging Face Space replaces it with a @spaces.GPU version (ZeroGPU); the checks and the | |
| # concurrency limit above stay in the main process. | |
| RUN_MODELS = run_models | |
| def decide(req: DecideRequest) -> dict: | |
| try: | |
| return run_decision(req.state, req.questions, req.models) | |
| except ValueError as exc: | |
| raise HTTPException(422, str(exc)) from None | |
| except Busy as exc: | |
| raise HTTPException(429, str(exc)) from None | |