A newer version of the Gradio SDK is available: 6.24.0
title: NOVA
emoji: ✨
colorFrom: purple
colorTo: indigo
sdk: gradio
sdk_version: 6.20.0
app_file: nova_app.py
python_version: '3.12'
pinned: false
short_description: Frame a research idea, find the papers, chat with them.
NOVA — Research, guided by SONIC
A Gradio app that stitches two projects together, unchanged:
app/— the research pipeline: an INTENT graph that frames your messy idea into Problem / Objective / Additional Context, then a SEARCH + CLUSTER graph that fetches from arXiv + Semantic Scholar + OpenAlex, reranks with SPECTER, and clusters by approach.chatbot_core/— single-PDF Q&A:vectorizeer.pychunks a paper by section and embeds it,Qa.pyanswers over it with page citations.
NOVA is the product. SONIC is the assistant persona that talks you through it.
your raw idea
-> INTENT agent frames it
-> you review/edit the framing
-> SEARCH agent fetches + reranks + clusters
-> paper cards, grouped by approach
-> "Chat it out" on any card -> PDF fetched, vectorized, Q&A with citations
Layout
| Path | What it is |
|---|---|
nova_app.py |
The whole UI: stage machine, event wiring, entrypoint |
ui/ |
Presentation + glue — theme, SONIC, paper/PDF helpers, model loading |
app/, chatbot_core/ |
Backend. Imported and driven, never modified |
ui/paths.py must be imported first — it does the sys.path + chdir + .env wiring
that makes import app... and from vectorizeer import ... resolve.
Run it
cd NOVA
python -m venv .venv && source .venv/bin/activate
# CPU-only box: install the CPU torch wheel FIRST, or pip resolves the CUDA build
# and drags in ~2.5 GB of nvidia-*/cuda-* wheels you'll never execute. NOVA pins
# device="cpu" and never asks for a GPU.
pip install torch --index-url https://download.pytorch.org/whl/cpu
pip install -r requirements.txt
cp .env.example .env # then fill it in
python nova_app.py # http://localhost:7860
Verified end-to-end on Python 3.14 with torch 2.13.0+cpu. Anything ≥3.10 works. Doing the CPU-torch line first pulls in zero nvidia packages and lands at a 2.0 GB venv; skip it and you get roughly double that, for a GPU the app never touches.
Keys
GROQ_API_KEY, SECOND_GROQ_API_KEY and TAVILY_API_KEY are required — both graphs
read them at import time, so a missing key fails the boot splash, not the first search.
The rest in .env.example are optional and only raise rate limits.
First boot is slow, once
NOVA loads three models totalling ~2 GB:
| Model | Loaded by | Size |
|---|---|---|
allenai-specter |
the search graph, at import | 440 MB |
BAAI/bge-base-en-v1.5 |
the chatbot's embeddings | 438 MB |
BAAI/bge-reranker-base |
the chatbot's cross-encoder | 1.1 GB |
They download from HuggingFace once and cache in ~/.cache/huggingface. Set HF_TOKEN
to avoid the anonymous rate limit on that first pull.
Only the search graph loads synchronously behind the splash — the two chatbot models warm on a background thread, since you can't click "Chat it out" until you've framed an intent and run a search anyway. If the thread hasn't finished by then, the click simply blocks on the same lock rather than loading twice.
Deploying to Hugging Face Spaces
The YAML front-matter at the top of this file is the Space config — app_file points
at nova_app.py, so nothing needs renaming. The free CPU tier (16 GB RAM) is the target.
1. Create the Space. huggingface.co/new-space → SDK Gradio, hardware CPU basic (free). Don't initialize it with anything.
2. Push this folder as the Space's repo root. nova_app.py must land at the top
level, not inside a NOVA/ subfolder:
cd NOVA
git init && git branch -M main
git remote add space https://huggingface.co/spaces/<your-username>/NOVA
git add -A && git commit -m "NOVA on Gradio"
git push --force space main
Push over HTTPS and use an access token with write scope as the password — your account password won't work.
3. Add the keys under Settings → Variables and secrets, as Secrets (not public
variables): GROQ_API_KEY, SECOND_GROQ_API_KEY, TAVILY_API_KEY. Spaces injects them
as env vars, which is exactly what the code reads — load_dotenv() finding no .env is
fine and expected. Add HF_TOKEN too, to lift the anonymous rate limit on the first
model pull.
Miss a key and the Space still boots, then shows a named error on the splash telling you which one — it won't hang.
4. First boot takes a few minutes while ~2 GB of weights download. The server binds
the port before loading anything (models load on first visit, via demo.load), so the
Space goes green early and the splash does the waiting.
Hardware: ZeroGPU or CPU basic
This repo is configured for ZeroGPU, and runs unmodified on CPU basic too — it
detects which it's on via SPACES_ZERO_GPU and adapts. Note HF only lets free accounts
pick the free tier at Space creation; you can't downgrade into it later.
ZeroGPU imposes two hard rules, and both shape the code:
- torch must be 2.11.0 / 2.10.0 / 2.9.1 / 2.8.0, plain CUDA build — the
+cpulocal version is rejected. Hence the exact pin inrequirements.txt. - At least one
@spaces.GPUfunction must exist at import, or the Space dies with "No @spaces.GPU function detected during startup".
A GPU exists only inside an @spaces.GPU call. Everything outliving that window must
be CPU-resident, which is why get_embeddings, get_retriever and the SPECTER model now
take an explicit device (defaulting to cpu) instead of auto-detecting. Auto-detect is
the trap: torch reports a GPU at import on ZeroGPU, so a model would load onto cuda and
then fail on first use out in a LangGraph node.
So exactly one thing runs on the GPU — the bulk chunk embedding in ui/gpu.py, the slowest step in the app. It builds and persists the vectorstore, then the caller re-opens it on CPU; what crosses the GPU boundary is the file on disk, never a cuda-resident object.
Verified
sdk_version: 6.20.0— the code needs Gradio 6 (launch()takestheme/css/js;Chatbotdroppedtype=), and 6.20.0 is what Spaces currently serves.python_version: "3.12"— the full dependency set resolves cleanly there withtorch==2.11.0.- Off ZeroGPU,
@spaces.GPUis a transparent no-op, so local runs and CPU hardware are unaffected — the CPU path keeps the threaded progress animation, and only the ZeroGPU path calls the GPU inline (it must run on the caller's thread, not one we spawn).
Why not Streamlit Community Cloud
That's what this rewrite escapes. It caps at roughly 1 GB of RAM, so ~2 GB of weights got the container OOM-killed mid-load; it restarted, re-entered boot, and sat on the splash forever. Any host needs ≥ 4 GB RAM.
Notes for the next person
- State. Streamlit re-ran the script top-to-bottom and branched on
session_state.stage. Gradio builds the component graph once, so a stage is agr.Columnand every handler returns thevisibleflag for all seven. Same state machine, declared instead of re-derived. - The card grid is inside
@gr.renderbecause each card's button needs a real handler closing over its paper key. It redraws offclusters_state.change— agr.Statereassigned to a fresh list, sincegr.rendercan't see a dict mutated in place. - Models are process-global, not per-session, behind a double-checked lock in
ui/agents.py. Gradio serves from a thread pool, so without the lock two simultaneous first-visitors would each kick off a 2 GB load. - The two graphs share
SECOND_GROQ_API_KEY(seeapp/modules/search/graph.py) —GROQ_API_KEYis read but onlysecond_api_keyis actually passed to bothChatGroqinstances. Pre-existing upstream behaviour, left alone. Set both keys.