Aniruddha7 commited on
Commit
ebaf77e
Β·
1 Parent(s): ced5ccc

Deploy QueryLens (Gradio-SDK launcher: worker :8700 / MCP :8701 / web :7860)

Browse files
Files changed (1) hide show
  1. app.py +106 -32
app.py CHANGED
@@ -1,41 +1,67 @@
1
  """
2
- Hugging Face Spaces entrypoint β€” Gradio SDK wrapper
3
- ====================================================
4
 
5
- HF Spaces moved the Docker SDK to a paid tier, so this Gradio-SDK Space
6
- replicates the exact same three-service architecture the Docker image used:
 
 
7
 
8
  this process (port 7860, public)
9
  β”œβ”€β”€ Agent.worker β†’ spawned uvicorn :8700 (Text2SQL / DocVQA backend)
10
  β”œβ”€β”€ Agent.mcp_server β†’ spawned uvicorn :8701 (MCP tool server)
11
- └── web.app FastAPI β†’ serves the sepia UI + /api proxies on :7860
12
-
13
- A minimal Gradio Blocks page is mounted at ``/gradio-lp`` purely so the
14
- Gradio-SDK runtime sees a registered Blocks application β€” the real UI is
15
- the sepia web app at ``/``.
16
-
17
- Secrets (Space settings β†’ Variables and Secrets, never baked into the code):
18
- HF_API_TOKEN β€” HF Inference API, Text-to-SQL on
19
- Qwen/Qwen2.5-Coder-7B-Instruct
20
- GROQ_API_KEY β€” Groq vision VQA (free tier)
21
-
22
- No DB_CONNECTION_URL is required β€” the bundled authentic demo employee
23
- SQLite database (employee-sample-database/sqlite/dataset_small/employee.db)
24
- auto-activates as the demo_employee profile.
 
 
 
 
 
 
 
 
 
 
 
25
  """
26
  import os
27
- import sys
28
  import subprocess
 
 
29
  from pathlib import Path
30
 
31
  ROOT = Path(__file__).resolve().parent
 
32
 
33
 
34
  # -----------------------------------------------------------------------------
35
- # Background services (same as start_hf.sh in the Docker image)
 
 
36
  # -----------------------------------------------------------------------------
37
- def _spawn(module: str, port: int, name: str) -> subprocess.Popen:
38
- """Start a uvicorn service; output streams into the Space log viewer."""
 
 
 
 
 
 
 
 
39
  print(f"[LAUNCHER] starting {name} on :{port} ...", flush=True)
40
  return subprocess.Popen(
41
  [sys.executable, "-u", "-m", "uvicorn", module,
@@ -48,25 +74,73 @@ _worker = _spawn("Agent.worker:app", 8700, "backend worker")
48
  _mcp = _spawn("Agent.mcp_server:app", 8701, "MCP tool server")
49
 
50
  # -----------------------------------------------------------------------------
51
- # Web UI: our existing FastAPI app, with a tiny Gradio shell mounted beside it
52
- # so the Gradio-SDK runtime is satisfied.
53
  # -----------------------------------------------------------------------------
54
  import gradio as gr # noqa: E402 (preinstalled in the Gradio-SDK runtime)
55
  from web.app import app as fastapi_app # noqa: E402
56
 
57
- with gr.Blocks() as _lp:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
58
  gr.Markdown(
59
  "**QueryLens** β€” the full sepia UI is served at this Space's root `/`.\n\n"
60
  "This page is only an anchor for the Gradio runtime."
61
  )
62
  gr.HTML('<a href="/" style="text-decoration:none;font-weight:600;">← Open the QueryLens UI</a>')
63
 
64
- app = gr.mount_gradio_app(fastapi_app, _lp, path="/gradio-lp")
65
-
66
 
67
  if __name__ == "__main__":
68
- import uvicorn
69
-
70
- port = int(os.environ.get("PORT", "7860"))
71
- print(f"[LAUNCHER] QueryLens web UI live on http://0.0.0.0:{port}", flush=True)
72
- uvicorn.run(app, host="0.0.0.0", port=port)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  """
2
+ Hugging Face Spaces entrypoint β€” Gradio SDK on the free ZeroGPU tier
3
+ ====================================================================
4
 
5
+ Free personal accounts can only host Gradio Spaces on ZeroGPU hardware
6
+ (CPU-basic requires a paid plan), so this launcher satisfies ZeroGPU's
7
+ startup contract while keeping the exact three-service architecture the
8
+ Docker image used:
9
 
10
  this process (port 7860, public)
11
  β”œβ”€β”€ Agent.worker β†’ spawned uvicorn :8700 (Text2SQL / DocVQA backend)
12
  β”œβ”€β”€ Agent.mcp_server β†’ spawned uvicorn :8701 (MCP tool server)
13
+ └── web.app FastAPI β†’ grafted into Gradio's own server on :7860
14
+
15
+ ZeroGPU contract (the two failure modes cost by the first deploy):
16
+ 1. The platform requires at least one ``@spaces.GPU``-decorated function
17
+ to exist at startup AND a real ``demo.launch()`` call β€” skipping
18
+ ``launch()`` in favour of ``gr.mount_gradio_app`` + ``uvicorn.run``
19
+ triggers ``No @spaces.GPU function detected during startup``.
20
+ 2. Because our app never uses a GPU, the decorated function below is a
21
+ no-op that is never called, so zero GPU quota is ever consumed.
22
+
23
+ Serving strategy (proven pattern for FastAPI-on-ZeroGPU spaces):
24
+ - ``demo.launch(server_port=7860, prevent_thread_lock=True,
25
+ ssr_mode=False)`` makes Gradio own the public port.
26
+ - Our full FastAPI web app (sepia UI + /api proxies) is then grafted
27
+ into ``demo.app.router.routes`` as a Mount inserted in FRONT of
28
+ Gradio's routes, so ``/`` serves the sepia UI (Gradio also registers
29
+ ``get /`` and ``get /static/{path}``, which would otherwise shadow it).
30
+ - ``ssr_mode=False`` is mandatory: Spaces' Node SSR proxy intercepts
31
+ every request it does not recognise as Gradio-owned and never
32
+ forwards it to Python.
33
+ - Mounted sub-apps do not receive lifespan events, so the shared
34
+ httpx client of ``web.app`` is created eagerly after launch.
35
+
36
+ Outside ZeroGPU (local dev / Docker), the previous behaviour is kept:
37
+ ``gr.mount_gradio_app`` + ``uvicorn.run``.
38
  """
39
  import os
40
+ import socket
41
  import subprocess
42
+ import sys
43
+ import time
44
  from pathlib import Path
45
 
46
  ROOT = Path(__file__).resolve().parent
47
+ PORT = int(os.environ.get("PORT", "7860"))
48
 
49
 
50
  # -----------------------------------------------------------------------------
51
+ # Background services (same as start_hf.sh in the Docker image).
52
+ # Port-guarded: the Spaces runtime can re-execute this script inside the same
53
+ # container, and duplicate uvicorn children would just exit with bind errors.
54
  # -----------------------------------------------------------------------------
55
+ def _listening(port: int) -> bool:
56
+ with socket.socket() as s:
57
+ s.settimeout(0.25)
58
+ return s.connect_ex(("127.0.0.1", port)) == 0
59
+
60
+
61
+ def _spawn(module: str, port: int, name: str):
62
+ if _listening(port):
63
+ print(f"[LAUNCHER] {name} already listening on :{port} β€” skipping spawn", flush=True)
64
+ return None
65
  print(f"[LAUNCHER] starting {name} on :{port} ...", flush=True)
66
  return subprocess.Popen(
67
  [sys.executable, "-u", "-m", "uvicorn", module,
 
74
  _mcp = _spawn("Agent.mcp_server:app", 8701, "MCP tool server")
75
 
76
  # -----------------------------------------------------------------------------
77
+ # Gradio shell + ZeroGPU contract
 
78
  # -----------------------------------------------------------------------------
79
  import gradio as gr # noqa: E402 (preinstalled in the Gradio-SDK runtime)
80
  from web.app import app as fastapi_app # noqa: E402
81
 
82
+ try:
83
+ import spaces # noqa: E402 (provided by the HF runtime; absent locally)
84
+ _ZERO_GPU = bool(getattr(getattr(spaces, "config", None), "zero_gpu", False))
85
+ except Exception:
86
+ spaces = None
87
+ _ZERO_GPU = False
88
+
89
+ if spaces is not None:
90
+ @spaces.GPU
91
+ def _zerogpu_platform_check():
92
+ """Never called. HF's ZeroGPU tier statically requires at least one
93
+ @spaces.GPU-decorated function to exist at startup, or it kills the
94
+ container. The decorator only allocates a GPU when the function
95
+ actually runs, so this costs zero quota for our CPU-only app."""
96
+ return None
97
+
98
+ with gr.Blocks(title="QueryLens") as _lp:
99
  gr.Markdown(
100
  "**QueryLens** β€” the full sepia UI is served at this Space's root `/`.\n\n"
101
  "This page is only an anchor for the Gradio runtime."
102
  )
103
  gr.HTML('<a href="/" style="text-decoration:none;font-weight:600;">← Open the QueryLens UI</a>')
104
 
 
 
105
 
106
  if __name__ == "__main__":
107
+ if _ZERO_GPU:
108
+ import httpx
109
+ import web.app as _webapp
110
+ from starlette.routing import Mount
111
+
112
+ try:
113
+ # Gradio owns the public port; prevent_thread_lock returns control
114
+ # so we can graft the web app below, then block_thread holds the
115
+ # main thread (what launch() would have done).
116
+ _lp.launch(
117
+ server_name="0.0.0.0",
118
+ server_port=PORT,
119
+ prevent_thread_lock=True,
120
+ ssr_mode=False,
121
+ )
122
+ except OSError as exc:
123
+ # The Spaces runtime re-executed this script while a first copy is
124
+ # already serving β€” do not touch the port, just idle forever.
125
+ print(f"[LAUNCHER] port {PORT} already held by the first instance ({exc}); idling", flush=True)
126
+ while True:
127
+ time.sleep(3600)
128
+
129
+ # Mounted sub-apps do not receive startup events β€” initialise the
130
+ # shared httpx client eagerly so /api proxies never see client=None.
131
+ if _webapp.client is None:
132
+ _webapp.client = httpx.AsyncClient(timeout=_webapp.WORKER_TIMEOUT)
133
+
134
+ # Serve the full web app in front of Gradio's own routes: the visible
135
+ # UI (root /) becomes the sepia web app, /api/* proxies work as-is.
136
+ _lp.app.router.routes.insert(0, Mount("/", app=fastapi_app, name="querylens_web"))
137
+ print(f"[LAUNCHER] QueryLens web UI live on http://0.0.0.0:{PORT}", flush=True)
138
+ try:
139
+ _lp.block_thread()
140
+ except KeyboardInterrupt:
141
+ pass
142
+ else:
143
+ app = gr.mount_gradio_app(fastapi_app, _lp, path="/gradio-lp")
144
+ print(f"[LAUNCHER] QueryLens web UI live on http://0.0.0.0:{PORT}", flush=True)
145
+ import uvicorn
146
+ uvicorn.run(app, host="0.0.0.0", port=PORT)