TrifectaRAG
Local multi-modal RAG: HNSW (vectors) + BM25 (keywords) + a knowledge graph, fused with Reciprocal Rank Fusion and optionally reranked with MMR.
Install the Python SDK and CLI from PyPI, or run the optional browser tutor from this repo.
pip install trifectarag[pdf]
from trifecta import TrifectaClient, PDFIngestor
client = TrifectaClient(device="cpu")
PDFIngestor(client, mode="page").ingest_pdf("textbook.pdf", output_dir="extracted")
client.save_snapshot("my.index")
hits = client.get_results(client.query("Newton's method", top_k=5))
for row in hits:
print(row["global_id"], row["score"], row["metadata"].get("page"))
trifecta ingest textbook.pdf --index ./my.index
trifecta query "Newton's method" --index ./my.index --top-k 5
trifecta info --index ./my.index
Install
Requires Python 3.10+ and a C++17 toolchain only when building from source. PyPI wheels ship the compiled trifecta_py extension.
| Extra | What it adds |
|---|---|
| (none) | Core SDK + CLI (hash embeddings, no PyTorch) |
[pdf] |
PyMuPDF ingestion + vector-figure extraction |
[ml] |
CLIP embeddings via torch / transformers |
[mcp] |
Model Context Protocol server |
[agent] |
Ollama tutor agent helpers |
[all] |
Everything above |
pip install trifectarag # library + CLI
pip install trifectarag[pdf] # + PDF ingestion
pip install trifectarag[all] # + CLIP, MCP, agent
From a clone of this repository:
pip install -e ".[pdf,ml]"
Default index path is ./trifecta.index (or $TRIFECTA_INDEX). Snapshots are two files: <stem>.trifecta (C++ engine) and <stem>.meta.gz (Python metadata).
Python SDK
from trifecta import TrifectaClient, PDFIngestor
from trifecta import trifecta_py as tr
client = TrifectaClient(device="cpu")
gid = client.add_document(
"Lagrange interpolation uses basis polynomials L_i(x).",
metadata={"source": "notes", "page": 1},
)
client.add_image("figure.png", caption="Bisection interval", metadata={"source": "notes", "page": 2})
client.add_edge(gid, other_gid, tr.EdgeType.RELATES_TO) # or EXPLAINS / DEPICTS
# PDF: page mode = one chunk per page + cropped figures. HNSW + BM25 are filled together.
stats = PDFIngestor(client, mode="page").ingest_pdf("textbook.pdf", output_dir="extracted")
print(stats) # pages, text_chunks, images, kg_edges
client.save_snapshot("my.index")
client = TrifectaClient.from_snapshot("my.index", device="cpu")
mode="classical" uses overlapping word chunks instead of one page per node.
Query
hits = client.query(text="Lagrange interpolation", top_k=8)
for row in client.get_results(hits):
meta = row["metadata"]
print(row["global_id"], row["score"], meta.get("source"), meta.get("page"))
# Keyword only / vector only
client.query(text="bisection", top_k=5, use_hnsw=False, use_bm25=True)
client.query(text="bisection", top_k=5, use_hnsw=True, use_bm25=False)
# Image + text late fusion (CLIP, or hash embeddings if [ml] is not installed)
client.query(text="root finding figure", image="scan.png", top_k=5)
Page index
client.list_sources()
client.list_pages("Numerical_Analysis")
client.get_page_chunks("Numerical_Analysis", 36)
client.get_chunk_page(209) # -> ("Numerical_Analysis", 213)
Hybrid search the tutor uses (MMR + provenance)
from trifecta import hybrid_search, serialize_sources
hits = hybrid_search(client, "Lagrange interpolation", top_k=4)
print(serialize_sources(hits))
CLI
trifecta ingest FILE.pdf [FILE.pdf ...] [--mode page|classical] [--pages START:END]
trifecta query TEXT [--image FILE] [--top-k N] [--no-hnsw] [--no-bm25]
trifecta add-text TEXT [--source NAME] [--page N]
trifecta add-image FILE [--caption TEXT]
trifecta info | sources | pages [--source NAME] [--page N]
trifecta get GID
trifecta mcp [--ingest FILE.pdf]
Shared flags: --index PATH, --device cpu|cuda, --json.
trifecta ingest examples/data/Numerical_Analysis.pdf --index ./na.index --pages 11:444
trifecta query "trapezoidal rule" --index ./na.index --json
python -m trifecta info --index ./na.index
How retrieval works
On ingest, HNSW and BM25 are always built together. At query time:
- Optional HNSW (vector / embedding search)
- Optional BM25 (lexical search)
- Knowledge-graph 1-hop expansion from those seeds
- Reciprocal rank fusion, then optional MMR so near-duplicate pages are dropped
Each hit carries source, page, global_id, and RRF score.
Browser tutor (this repo)
The optional UI is not on PyPI. From a clone:
- Python 3.11+
- Node 18+
- Ollama with a tool-capable model (default
qwen2.5:7b)
pip install -e ".[all]"
pip install -r requirements.txt
ollama pull qwen2.5:7b
cd frontend
npm install
# terminal 1 — API (port 8001)
python api.py
# terminal 2 — Vite (port 5172)
cd frontend
npm run dev
Open http://localhost:5172/. On first start the API loads a textbook snapshot under examples/data/ if one exists (*_page_*.trifecta).
| Variable | Default | Meaning |
|---|---|---|
OLLAMA_URL |
http://127.0.0.1:11434 |
Ollama server |
OLLAMA_MODEL / TRIFECTA_OLLAMA_MODEL |
qwen2.5:7b |
Chat model |
OLLAMA_CHAT_TIMEOUT_SECONDS |
150 |
Per agent turn |
TRIFECTA_AGENT_ROUNDS |
6 |
Max tool-call rounds |
Index a file from Library, then ask exam / figure / web questions in the single chat. Retrieval toggles (HNSW / BM25) live in the sidebar.
HTTP API (what the UI calls)
Base URL with Vite: same origin. Direct: http://127.0.0.1:8001.
| Method | Path | Use |
|---|---|---|
| GET | /health |
Chunks, corpus, retriever label |
| POST | /chat |
Agent: { mode, messages, question?, attachments? } |
| POST | /upload |
Index PDF/DOCX/TXT/MD/CSV → { task_id } |
| GET | /ingest-status/{task_id} |
Poll until done / error |
| GET | /corpora |
Indexed + on-disk sources |
| GET/POST | /settings/retrieval |
{ use_hnsw, use_bm25 } |
Example scripts
From the repo root, after pip install -e ".[pdf,ml]":
python examples/00_basic_usage.py
python examples/01_ingest_textbook.py
python examples/02_query_textbook.py
python examples/05_page_index_demo.py
Put a PDF in examples/data/ or set TRIFECTA_TEXTBOOK_PDF.
Publishing
pip install build twine
python -m build
python -m twine check dist/*
python -m twine upload dist/*
GitHub Releases trigger .github/workflows/publish.yml (cibuildwheel + trusted PyPI publishing).
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file trifectarag-0.1.0.tar.gz.
File metadata
- Download URL: trifectarag-0.1.0.tar.gz
- Upload date:
- Size: 371.0 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via:
twine/7.0.0 CPython/3.12.4
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
31bce412b21dd35fb513412b04cb8a58194e792858c4f381d8b2c2826d1c2115
|
|
| MD5 |
2ee1a07e1a9ae83cd16a811020e75eba
|
|
| BLAKE2b-256 |
f73569c711659ab5849186fde5decf6b70469f3140d637ae6308553ec28fd2f5
|
File details
Details for the file trifectarag-0.1.0-cp312-cp312-win_amd64.whl.
File metadata
- Download URL: trifectarag-0.1.0-cp312-cp312-win_amd64.whl
- Upload date:
- Size: 1.2 MB
- Tags: CPython 3.12, Windows x86-64
- Uploaded using Trusted Publishing? No
- Uploaded via:
twine/7.0.0 CPython/3.12.4
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
69eb14540bdfe3f3d18d48dc810ebd9f58d0e0d7b1db563159899dc9fbb431f0
|
|
| MD5 |
4982d244df0b7567c781196947aaf769
|
|
| BLAKE2b-256 |
4c17de45a615d6fd56e362b4d337daaa18fcd22c8cc7f9d428db31cce0c39487
|