| --- |
| 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.py` chunks a paper by section and |
| embeds it, `Qa.py` answers 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 |
| |
| ```bash |
| 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](https://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: |
|
|
| ```bash |
| 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](https://huggingface.co/settings/tokens) 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: |
|
|
| 1. **torch must be 2.11.0 / 2.10.0 / 2.9.1 / 2.8.0**, plain CUDA build — the `+cpu` local |
| version is rejected. Hence the exact pin in `requirements.txt`. |
| 2. **At least one `@spaces.GPU` function 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](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()` takes `theme`/`css`/`js`; |
| `Chatbot` dropped `type=`), and 6.20.0 is what Spaces currently serves. |
| - `python_version: "3.12"` — the full dependency set resolves cleanly there with |
| `torch==2.11.0`. |
| - Off ZeroGPU, `@spaces.GPU` is 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 a |
| `gr.Column` and every handler returns the `visible` flag for all seven. Same state |
| machine, declared instead of re-derived. |
| - **The card grid** is inside `@gr.render` because each card's button needs a real |
| handler closing over its paper key. It redraws off `clusters_state.change` — a |
| `gr.State` reassigned to a fresh list, since `gr.render` can'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`** (see `app/modules/search/graph.py`) — |
| `GROQ_API_KEY` is read but only `second_api_key` is actually passed to both `ChatGroq` |
| instances. Pre-existing upstream behaviour, left alone. Set both keys. |
| |