Your research corpus. Your infrastructure. Your intelligence.
A sovereign research intelligence system that transforms a collection of PDFs into an interrogable knowledge engine. Drop your papers in, run the embedder, ask questions. Everything runs on your hardware — no cloud dependency, no subscription, no data leaving your box.
This is not a toy RAG demo. It is a production-grade research tool with hybrid search (vector + keyword), dual-tier synthesis (local GPU + optional cloud), a React frontend with in-browser PDF viewing, and Docker deployment. Built and battle-tested on a corpus of 6,500+ scientific papers.
| Feature | Basic RAG | AxOntos |
|---|---|---|
| Search | Vector only | Hybrid vector + keyword with acronym expansion |
| Models | Single | Dual-tier: local Ollama + optional Anthropic cloud |
| Retrieval | Fixed chunk count | Model-aware: auto-tunes chunks to context window size |
| Chunking | Naive character split | Sentence-boundary aware with hard ceiling |
| Embedding | Cloud API | Sovereign: Nomic v1.5 via local Ollama |
| Pipeline | Fragile | Checkpointed: crash-safe, resumes from failure |
| Frontend | Terminal | React SPA with search, chat, PDF viewer, cost display |
| Deployment | Script | Docker Compose (one command) |
- Python 3.10+
- Node.js 18+ (for frontend)
- Ollama installed and running (ollama.ai)
- GPU recommended (embedding is ~10x faster with CUDA)
git clone https://github.com/ash23x/axontos.git
cd axontos
pip install -r requirements.txt
cp .env.example .env# Embedding model (required — 139 MB)
ollama pull nomic-embed-text
# Chat model (pick one)
ollama pull llama3.1:8b # 4.7 GB — good default
ollama pull qwen3:14b # 8.6 GB — better quality
ollama pull gemma2:9b # 7.2 GB — alternativemkdir corpus
# Copy or symlink your PDF library into the corpus/ folder
# Subdirectories are fine — the crawler searches recursively# Step 1: Crawl your corpus and build the manifest
python -m jarvis.crawler
# Step 2: Embed everything (this takes a while — ~2 mins per 100 PDFs on GPU)
python embed_v3_streaming.pyThe embedder checkpoints per document. If it crashes, restart it — it picks up exactly where it left off.
# Start the API backend
python -m uvicorn jarvis.api:app --host 0.0.0.0 --port 8500
# In another terminal: start the frontend
cd frontend
npm install
npm run devOpen http://localhost:5503 in your browser. Green dot = connected. Search your corpus.
Browser (:5503) → Vite dev server → proxy /api/* → FastAPI (:8500)
│
┌────────┼────────┐
▼ ▼ ▼
ChromaDB Ollama Anthropic
(vectors) (local) (cloud)
│ │
┌────┘ optional
▼
nomic-embed-text
llama3.1 / qwen3
| Port | Service | Purpose |
|---|---|---|
| 8500 | FastAPI backend | REST API: search, chat, stats, PDF serving |
| 5503 | Vite dev server | React frontend with hot-reload (development) |
| 8080 | nginx | Production frontend (Docker only — single port) |
In development, Vite runs on :5503 and proxies /api/* requests to the backend on :8500. The browser only talks to :5503.
In Docker production mode, nginx serves the static React build and proxies API calls — everything through a single port (:8080).
All configuration is via environment variables. Copy .env.example to .env and edit:
# Path to your PDF corpus
SRAG_CORPUS=./corpus
# Ollama connection
SRAG_OLLAMA_URL=http://localhost:11434
# Embedding model (must be pulled in Ollama)
SRAG_EMBED_MODEL=nomic-embed-text:latest
# Chat model (must be pulled in Ollama)
SRAG_CHAT_MODEL=llama3.1:8b
# Optional: Anthropic API key for cloud synthesis
# SRAG_ANTHROPIC_KEY=sk-ant-...If you set SRAG_ANTHROPIC_KEY, the frontend unlocks Claude Sonnet 4.6 ($0.02/query) and Claude Opus 4.6 ($0.08/query) as synthesis options. These provide dramatically better cross-domain reasoning than local models but cost money per query. The cost is displayed in the interface.
Local models work perfectly without any API key. Cloud is a nice-to-have, not a requirement.
All endpoints served from the FastAPI backend on port 8500.
| Method | Endpoint | Description |
|---|---|---|
GET |
/ |
Health check |
GET |
/stats |
Corpus statistics: paper count, chunk count, model info |
GET |
/models |
Available models (local + cloud) with sizes and costs |
POST |
/search |
Hybrid search. Body: {query, n, mode} |
POST |
/chat |
RAG synthesis. Body: {question, model, n_chunks} |
GET |
/pdf?path=... |
Serve PDF from corpus (path-validated) |
POST |
/add |
Ingest a new paper into the corpus |
both(default) — Vector similarity + keyword matching, blended and rankedvector— Pure embedding cosine similaritykeyword— Manifest keyword search only
Embedding models don't understand domain acronyms. "NLP" embeds as three characters, not as "Natural Language Processing." AxOntos maintains an acronym dictionary that transparently expands queries before embedding.
Edit jarvis/search.py to add your domain-specific acronyms:
ACRONYMS = {
"llm": "Large Language Model LLM",
"rag": "Retrieval Augmented Generation RAG",
# Add your domain acronyms here:
# "xyz": "Full Name Of XYZ Concept XYZ",
}This is the single highest-impact customisation you can make. If your corpus uses domain-specific abbreviations, add them here.
For production or sharing with collaborators:
docker-compose up --buildThis starts two containers:
- axontos-api on port 8500 (FastAPI + ChromaDB)
- axontos-frontend on port 8080 (nginx + React build)
Open http://localhost:8080 — single port, no proxy config needed.
volumes:
- ./corpus:/data/pdfs:ro # Your PDF library (read-only)
- rag-chroma:/data/chromadb # Persistent vector database
- ./manifest.json:/data/manifest.json:roDocker containers reach Ollama on the host via host.docker.internal:11434. This works out of the box on Docker Desktop (Windows/Mac). On Linux, add --add-host=host.docker.internal:host-gateway to your Docker run command or use network_mode: host.
- Crawl —
jarvis/crawler.pydiscovers all PDFs, extracts metadata, buildsmanifest.json - Chunk —
embed_v3_streaming.pyextracts text via PyMuPDF, splits into sentence-boundary-aware chunks (target 500 chars, hard max 1000 chars, 50 char overlap) - Embed — Chunks are batched (100 per GPU call) and embedded via Nomic (768 dimensions)
- Store — Vectors are upserted into ChromaDB in two collections: paper-level and chunk-level
The embedder writes a checkpoint (done_docs.txt) after each document. If the process dies — power cut, OOM, VRAM exhaustion — restart it and it resumes from the exact point of failure. No re-embedding of completed documents.
On an NVIDIA RTX 4070 (12 GB VRAM):
- ~2 minutes per 100 PDFs
- ~12 hours for 6,500 papers
- Query latency: ~1.8s cold, ~0.1s warm
CPU-only embedding works but is ~10x slower.
The web interface features:
- Search tab — Free-text query with mode selector (All/Papers/Chunks), similarity percentage bars, clickable PDF links
- Chat tab — Conversational RAG with model dropdown (local/cloud), inline source citations, cost display for cloud models
- PDF viewer — Click any source to view the original PDF in-browser
- Model selector — Switch between local Ollama models and cloud Claude models
- KaTeX — Mathematical notation rendering in scientific answers
The design is a dark theme with gold accent instrumentation. It looks like a research tool, not a toy.
axontos/
├── jarvis/ # Core Python package
│ ├── config.py # Environment-driven configuration
│ ├── search.py # Hybrid search + acronym expansion
│ ├── chat.py # RAG synthesis (local + cloud)
│ ├── cloud_chat.py # Anthropic API bridge
│ ├── api.py # FastAPI REST endpoints
│ ├── embedder.py # Paper-level embedding
│ ├── crawler.py # PDF discovery + manifest
│ └── add.py # Single-paper ingestion
├── frontend/ # React + Vite SPA
│ ├── src/App.jsx # Main application (598 lines)
│ ├── Dockerfile # Multi-stage: build + nginx
│ ├── nginx.conf # Production reverse proxy
│ └── package.json # React 18 + Vite 6 + KaTeX
├── embed_v3_streaming.py # V3 chunk embedder (crash-safe)
├── docker-compose.yml # Two-service deployment
├── Dockerfile # Backend container
├── requirements.txt # Python dependencies
├── .env.example # Configuration template
└── corpus/ # Your PDFs go here
Edit jarvis/chat.py — the SYSTEM_PROMPT variable. Tell the model what kind of researcher it is, what domains the corpus covers, and how you want it to synthesise across disciplines.
In jarvis/config.py, MIN_SIMILARITY controls the cutoff. Default is 0.55. Raise it for precision (fewer but better results), lower it for recall (more results, more noise).
In jarvis/chat.py, the _retrieval_config() function tunes how many chunks and papers are retrieved based on the model's context window. Cloud models with 200K context get 15 chunks; local 8B models with 4K context get 5. Add your own models here.
- chromadb >= 0.4.0
- pymupdf >= 1.23.0
- requests >= 2.28.0
- fastapi >= 0.100.0
- uvicorn >= 0.22.0
- python-dotenv >= 1.0.0
- psutil >= 5.9.0
- react 18
- react-dom 18
- katex >= 0.16
- vite 6
- Ollama (local LLM server)
- GPU with CUDA (recommended, not required)
MIT License — see LICENSE.
Built by Ontos Labs Ltd.
If you use this for research, we'd love to hear about it.