Spaces:
Running on Zero
Running on Zero
Download app.py from Aniruddha7/QueryLensV2b: direct link, hf CLI and curl.
- Browser
- Download file 9.57 kB
-
https://huggingface.co/spaces/Aniruddha7/QueryLensV2b/resolve/main/app.py
- Command line
-
hf download hf://spaces/Aniruddha7/QueryLensV2b/app.py
-
curl -L -o app.py https://huggingface.co/spaces/Aniruddha7/QueryLensV2b/resolve/main/app.py
9.57 kB
| """ | |
| Hugging Face Spaces entrypoint β Gradio SDK on the free ZeroGPU tier | |
| ==================================================================== | |
| Free personal accounts can only host Gradio Spaces on ZeroGPU hardware | |
| (CPU-basic requires a paid plan), so this launcher satisfies ZeroGPU's | |
| startup contract while keeping the exact three-service architecture the | |
| Docker image used: | |
| this process (port 7860, public) | |
| βββ Agent.worker β spawned uvicorn :8700 (Text2SQL / DocVQA backend) | |
| βββ Agent.mcp_server β spawned uvicorn :8701 (MCP tool server) | |
| βββ web.app FastAPI β grafted into Gradio's own server on :7860 | |
| ZeroGPU contract (all four sources of failure, proven by real deploys): | |
| 1. ``spaces`` must be imported BEFORE gradio ("ZeroGPU spaces error if | |
| the spaces library is not imported first β stricter since v2"). | |
| 2. At least one ``@spaces.GPU`` function must exist at module level. | |
| 3. ZeroGPU's startup scan walks GRADIO'S REGISTERED EVENT HANDLERS β a | |
| decorated function that is never bound to any event does not count | |
| ("No @spaces.GPU function detected during startup"). Our no-op probe | |
| is therefore registered as the ``.click`` handler of an invisible | |
| button, so it is discoverable but can never actually be invoked | |
| (nothing requests a GPU β zero quota consumed). | |
| 4. A real ``demo.launch()`` is mandatory (``gr.mount_gradio_app`` + | |
| ``uvicorn.run`` suppresses the ZeroGPU startup report path). | |
| Serving strategy (proven pattern for FastAPI-on-ZeroGPU spaces): | |
| - ``demo.launch(server_port=7860, prevent_thread_lock=True, | |
| ssr_mode=False)`` makes Gradio own the public port. | |
| - Our full FastAPI web app (sepia UI + /api proxies) is then grafted | |
| into ``demo.app.router.routes`` as a Mount inserted in FRONT of | |
| Gradio's routes, so ``/`` serves the sepia UI (Gradio also registers | |
| ``get /`` and ``get /static/{path}``, which would otherwise shadow it). | |
| - ``ssr_mode=False`` is mandatory: Spaces' Node SSR proxy intercepts | |
| every request it does not recognise as Gradio-owned and never | |
| forwards it to Python. | |
| - Mounted sub-apps do not receive lifespan events, so the shared | |
| httpx client of ``web.app`` is created eagerly after launch. | |
| Outside ZeroGPU (local dev / Docker), the previous behaviour is kept: | |
| ``gr.mount_gradio_app`` + ``uvicorn.run``. | |
| """ | |
| import os | |
| import socket | |
| import sys | |
| import time | |
| import traceback | |
| # ----------------------------------------------------------------------------- | |
| # Diagnostic env snapshot β printed FIRST so every deploy log is self-sufficient | |
| # ----------------------------------------------------------------------------- | |
| _ZEROGPU_ENV_KEYS = ( | |
| "SPACES_ZERO_GPU", | |
| "SPACES_ZERO_DEVICE_API_URL", | |
| "SPACES_GRADIO_AUTO_WRAP", | |
| "ZEROGPU_V2", | |
| "ZEROGPU_OFFLOAD_DIR", | |
| "GRADIO_SERVER_PORT", | |
| "PORT", | |
| ) | |
| print("[LAUNCHER] QueryLens app.py starting; ZeroGPU env snapshot:", flush=True) | |
| for _key in _ZEROGPU_ENV_KEYS: | |
| print(f"[LAUNCHER] env {_key}={os.environ.get(_key)!r}", flush=True) | |
| # ZeroGPU rule 1: import spaces BEFORE anything else (gradio in particular) | |
| try: | |
| import spaces # noqa: E402 (provided by the HF runtime; absent locally) | |
| except Exception: | |
| spaces = None | |
| print("[LAUNCHER] 'spaces' import FAILED β running in non-ZeroGPU mode:", flush=True) | |
| traceback.print_exc() | |
| if spaces is not None: | |
| # NB: spaces.config is a MODULE; the Settings instance is spaces.config.Config. | |
| _zero_settings = getattr(spaces.config, "Config", None) | |
| print( | |
| f"[LAUNCHER] spaces zero_gpu = {getattr(_zero_settings, 'zero_gpu', None)}, " | |
| f"device_api = {getattr(_zero_settings, 'zero_device_api_url', None)!r}", | |
| flush=True, | |
| ) | |
| import subprocess | |
| from pathlib import Path | |
| ROOT = Path(__file__).resolve().parent | |
| PORT = int(os.environ.get("PORT", "7860")) | |
| # ----------------------------------------------------------------------------- | |
| # Background services (same as start_hf.sh in the Docker image). | |
| # Port-guarded: the Spaces runtime can re-execute this script inside the same | |
| # container, and duplicate uvicorn children would just exit with bind errors. | |
| # ----------------------------------------------------------------------------- | |
| def _listening(port: int) -> bool: | |
| with socket.socket() as s: # type: ignore[name-defined] | |
| s.settimeout(0.25) | |
| return s.connect_ex(("127.0.0.1", port)) == 0 | |
| def _spawn(module: str, port: int, name: str): | |
| if _listening(port): | |
| print(f"[LAUNCHER] {name} already listening on :{port} β skipping spawn", flush=True) | |
| return None | |
| print(f"[LAUNCHER] starting {name} on :{port} ...", flush=True) | |
| return subprocess.Popen( | |
| [sys.executable, "-u", "-m", "uvicorn", module, | |
| "--host", "0.0.0.0", "--port", str(port)], | |
| cwd=str(ROOT), | |
| ) | |
| _children = {} | |
| def _supervise(specs): | |
| """Respawn dead background services forever. | |
| The Spaces container can OOM-kill or crash the worker (it is the | |
| heaviest process: torch + llama-index + chat stores). Without | |
| supervision that meant "all queries fail until a manual restart"; | |
| with it, the launcher brings a fresh worker back within seconds. | |
| """ | |
| while True: | |
| for key, module, port, name in specs: | |
| if _listening(port): | |
| continue # healthy (or handled by an older instance) | |
| proc = _children.get(key) | |
| if proc is not None and proc.poll() is None: | |
| continue # spawned and still starting up (heavy import) | |
| print(f"[LAUNCHER] {name} is DOWN - respawning on :{port}", flush=True) | |
| try: | |
| _children[key] = _spawn(module, port, name) | |
| except Exception as spawn_err: | |
| print(f"[LAUNCHER] respawn of {name} failed: {spawn_err}", flush=True) | |
| time.sleep(10) | |
| _supervise_specs = ( | |
| ("worker", "Agent.worker:app", 8700, "backend worker"), | |
| ("mcp", "Agent.mcp_server:app", 8701, "MCP tool server"), | |
| ) | |
| for _key, _module, _port, _name in _supervise_specs: | |
| _children[_key] = _spawn(_module, _port, _name) | |
| import threading # noqa: E402 | |
| threading.Thread(target=_supervise, args=(_supervise_specs,), daemon=True).start() | |
| import gradio as gr # noqa: E402 (after 'spaces' β ZeroGPU rule 1) | |
| from web.app import app as fastapi_app # noqa: E402 | |
| _ZERO_GPU = bool(spaces is not None and getattr(getattr(spaces.config, "Config", None), "zero_gpu", False)) | |
| print(f"[LAUNCHER] ZeroGPU branch = {_ZERO_GPU}", flush=True) | |
| if spaces is not None: | |
| def _zerogpu_platform_check(): | |
| """Registered below as an invisible-button click handler. | |
| Never meaningfully called in the UI: it exists because ZeroGPU's | |
| startup scan walks Gradio's registered event handlers and kills the | |
| container when none of them is decorated. The decorator only | |
| allocates a GPU when the function actually runs, and the button is | |
| invisible β so zero GPU quota is consumed for our CPU-only app.""" | |
| return None | |
| with gr.Blocks(title="QueryLens") as _lp: | |
| gr.Markdown( | |
| "**QueryLens** β the full sepia UI is served at this Space's root `/`.\n\n" | |
| "This page is only an anchor for the Gradio runtime." | |
| ) | |
| gr.HTML('<a href="/" style="text-decoration:none;font-weight:600;">β Open the QueryLens UI</a>') | |
| if spaces is not None: | |
| # ZeroGPU rule 3: the decorated no-op must be bound to a real event | |
| # so the startup scan discovers it. Invisible => unclickable. | |
| _probe_btn = gr.Button("ZeroGPU platform probe", visible=False) | |
| _probe_btn.click(_zerogpu_platform_check, None, None) | |
| if __name__ == "__main__": | |
| if _ZERO_GPU: | |
| import httpx | |
| import web.app as _webapp | |
| from starlette.routing import Mount | |
| try: | |
| # Gradio owns the public port; prevent_thread_lock returns control | |
| # so we can graft the web app below, then block_thread holds the | |
| # main thread (what launch() would have done). | |
| _lp.launch( | |
| server_name="0.0.0.0", | |
| server_port=PORT, | |
| prevent_thread_lock=True, | |
| ssr_mode=False, | |
| ) | |
| except OSError as exc: | |
| # The Spaces runtime re-executed this script while a first copy is | |
| # already serving β do not touch the port, just idle forever. | |
| print(f"[LAUNCHER] port {PORT} already held by the first instance ({exc}); idling", flush=True) | |
| while True: | |
| time.sleep(3600) | |
| # Mounted sub-apps do not receive startup events β initialise the | |
| # shared httpx client eagerly so /api proxies never see client=None. | |
| if _webapp.client is None: | |
| _webapp.client = httpx.AsyncClient(timeout=_webapp.WORKER_TIMEOUT) | |
| # Serve the full web app in front of Gradio's own routes: the visible | |
| # UI (root /) becomes the sepia web app, /api/* proxies work as-is. | |
| _lp.app.router.routes.insert(0, Mount("/", app=fastapi_app, name="querylens_web")) | |
| print(f"[LAUNCHER] QueryLens web UI live on http://0.0.0.0:{PORT}", flush=True) | |
| try: | |
| _lp.block_thread() | |
| except KeyboardInterrupt: | |
| pass | |
| else: | |
| app = gr.mount_gradio_app(fastapi_app, _lp, path="/gradio-lp") | |
| print(f"[LAUNCHER] QueryLens web UI live on http://0.0.0.0:{PORT}", flush=True) | |
| import uvicorn | |
| uvicorn.run(app, host="0.0.0.0", port=PORT) | |