"""cricket-rag on a Hugging Face Gradio Space (CPU basic): the match centre, Pandit and the API on one port. / the match centre (a pre-built copy of frontend/ui, served as static files) /api/... the same API as `cricrag serve`: Pandit's chat stream, archive, match pages, stats /pandit a plain Gradio chat with Pandit (also callable through the Gradio API) Nothing here needs a GPU: Pandit's LLM calls go to Groq (GROQ_API_KEY secret) and the rest is DuckDB and LanceDB on CPU. Run locally with `python app.py` (port 7860, or PORT). """ from __future__ import annotations import os import sys from pathlib import Path HERE = Path(__file__).resolve().parent sys.path.insert(0, str(HERE / "src")) # the cricrag and bundled cricbot packages os.chdir(HERE) # data/ and .env are read relative to this folder try: # present on ZeroGPU hardware; elsewhere the app runs without it import spaces except Exception: spaces = None import gradio as gr # noqa: E402 (after the sys.path line) import uvicorn # noqa: E402 (after the sys.path line) from fastapi.staticfiles import StaticFiles # noqa: E402 (after the sys.path line) from cricrag import server # noqa: E402 (after the sys.path line) api = server.create_app() if spaces is not None: @spaces.GPU(duration=5) def gpu_check() -> str: """Not used by the app. ZeroGPU expects a Space to declare one GPU function; Pandit runs on CPU and Groq.""" return "ok" def _text(content) -> str: if isinstance(content, str): return content if isinstance(content, list): # [{"type": "text", "text": ...}, ...] return " ".join(part.get("text", "") for part in content if isinstance(part, dict)) return str(content or "") def ask(message, history): """One Pandit answer; history is the chat so far (follow-ups like "and in 2017?" use it).""" past = [{"role": m["role"], "content": _text(m["content"])} for m in history or [] if isinstance(m, dict)] pandit = api.state.agent() # built on first use (takes the lock itself) with api.state.lock: # one question at a time: Pandit holds DuckDB connections answer = pandit.ask(_text(message), past) return answer.text with gr.Blocks(title="Pandit") as demo: gr.Markdown( "### Pandit\nIPL questions answered from 19 seasons of ball-by-ball data. " "The full match centre is at [the Space's home page](/)." ) gr.ChatInterface( ask, examples=[ "Where did the 2025 final turn?", "Who is Rohit Sharma's nemesis?", ], ) # Spaces set GRADIO_SSR_MODE=true. With SSR on, mount_gradio_app starts a Node server on the first free port from # 7860, which then blocks uvicorn below ("address already in use"). The chat needs no SSR, so keep it off. app = gr.mount_gradio_app(api, demo, path="/pandit", ssr_mode=False) app.mount("/", StaticFiles(directory=HERE / "ui", html=True), name="ui") # last: every other path is the UI def zerogpu_startup() -> None: """On ZeroGPU, report the GPU function to the scheduler, as `demo.launch()` would. The `spaces` package runs this report from a patched `gr.Blocks.launch`. This app serves Gradio inside its own FastAPI app and never calls launch(), so without this ZeroGPU stops the Space with "No @spaces.GPU function detected during startup". Elsewhere (CPU basic, your computer) it does nothing. """ if spaces is None or os.environ.get("SPACES_ZERO_GPU", "").lower() not in ("1", "true"): return from spaces import zero startup = getattr(zero, "startup", None) if startup is None: # a `spaces` release that moved the hook: say so instead of failing silently print("ZeroGPU: spaces.zero.startup not found; choose CPU basic hardware for this Space.", flush=True) return startup() if __name__ == "__main__": zerogpu_startup() uvicorn.run(app, host="0.0.0.0", port=int(os.environ.get("PORT", "7860")))