Advanced · Modular · Adaptive Multi-Session Retrieval-Augmented Generation System
Production-grade · Hybrid Retrieval · Auth + Guest Identity · PostgreSQL + pgvector · Cloud-Native
Built as a deliberate learning project — every component designed, debugged, and understood from first principles. The name comes from the tesseract — a four-dimensional hypercube — representing the multi-dimensional retrieval space: lexical, semantic, contextual, and temporal dimensions intersecting to produce accurate, grounded answers.
TesseractRAG started as a question: what does it actually take to build a production-grade RAG system from scratch?
Not a tutorial. Not a framework wrapper. A real system — with a proper ingestion pipeline, hybrid retrieval, reranking, persistent multi-session state, and a clean layered architecture — built and understood component by component.
v2.0 is a ground-up architectural redesign. Where v1 was a proof of concept with flat modules and file-based storage, v2 is a production-structured system: layered services, abstract base classes, dispatcher routing, PostgreSQL + pgvector replacing FAISS + Backblaze B2, JWT authentication alongside anonymous guest access, and Groq as the primary LLM provider.
- Live Demo
- What's New in v2.0
- Core Capabilities
- Architecture Overview
- How It Works
- Tech Stack
- Project Structure
- Quick Start
- API Reference
- Configuration Reference
- Migrating from v1
- Development Phases
- Known Limitations
- Future Roadmap
- What I Learned Building This
⚠️ Cold Start Notice: The backend may take 30–60 seconds to wake on first request. The frontend retries automatically — this is a free-tier constraint. Subsequent requests are fast.
v2.0 is not an incremental update — it's a full architectural redesign built on lessons learned from v1.
| v1 | v2 | |
|---|---|---|
| Vector Storage | FAISS (in-memory, file-serialized) | pgvector (PostgreSQL extension) |
| Document/Chunk Storage | JSON files on Backblaze B2 | PostgreSQL via SQLAlchemy + Alembic |
| Session Metadata | metadata.json per session on B2 |
PostgreSQL relational schema |
| Embeddings | FAISS binary index per session | pgvector VECTOR columns with cosine/L2 ops |
| v1 | v2 | |
|---|---|---|
| Code Structure | Flat modules per concern | Layered: API → Services → RAG Components → Storage |
| RAG Components | Direct function calls in endpoints | Abstract base classes + Dispatcher routing |
| LLM Provider | HuggingFace Inference Router | Groq (primary) + HF (fallback) |
| DB Migrations | None (manual schema) | Alembic versioned migrations |
| Identity | Anonymous browser UUID only | JWT auth + anonymous guest mode |
| Caching | None | Redis (rate-limit counters via slowapi) |
- Multi-language support — Arabic and English cleaning pipelines with automatic language detection
- Pluggable parsers — PDF, DOCX, and plain text via dispatcher-routed parser classes
- Cross-language retrieval strategy — queries can retrieve across language boundaries
- Evaluation layer — LLM-as-judge evaluation via Cohere, stored per session
| Capability | Detail |
|---|---|
| Multi-Session Isolation | Each session owns its own vector space, chunk store, and conversation history — fully independent, zero cross-contamination |
| Hybrid Retrieval | BM25 lexical search + pgvector semantic search fused via Reciprocal Rank Fusion (RRF) |
| Adaptive Query Routing | Dispatcher selects retrieval strategy per query — lexical for identifiers, semantic for concepts, hybrid by default |
| Cross-Encoder Reranking | BGE cross-encoder re-scores top candidates with joint query-passage attention |
| Cross-Language Retrieval | Arabic/English queries can retrieve from mixed-language document sets |
| Grounded Generation | LLM constrained to retrieved context — hallucination is architecturally minimized |
| JWT + Guest Identity | Full user accounts with JWT auth; anonymous guest mode for frictionless access |
| Rate Limiting | Per-user and per-IP request throttling via slowapi + Redis — counters stored in Redis |
| Evaluation | Async LLM-as-judge evaluation (Cohere) runs in background after each chat turn |
┌──────────────────────────────────────────────────────────────────────┐
│ Frontend │
│ Vanilla JS · HTML/CSS · Cloudflare Workers │
└─────────────────────────────┬────────────────────────────────────────┘
│ REST API · JWT / Guest token
┌─────────────────────────────▼────────────────────────────────────────┐
│ FastAPI Backend │
│ │
│ api/v1/ ──► auth · chat · documents · sessions · admin │
│ │ │
│ services/ (business logic layer) │
│ auth_service · chat_service · document_service · session_service │
│ │ │
│ rag/pipelines/ │
│ IngestionPipeline · RetrievalPipeline · GenerationPipeline │
│ │ │
│ rag/components/ (abstract base + dispatcher) │
│ parsing · chunking · cleaning · embedding │
│ retrievals · reranker · generator · evaluators │
│ query_rewrite · context · lang_detector │
│ │ │
│ storage/ │
│ db/ (PostgreSQL · SQLAlchemy · Alembic · Repositories) │
│ vector_db/ (pgvector · FAISS fallback) │
│ blob/ (pluggable blob storage factory) │
└──────────────────────┬───────────────────────────────────────────────┘
│
┌─────────────┼──────────────┐
▼ ▼ ▼
PostgreSQL Redis Groq API
+ pgvector (rate limit) (LLM inference)
Pattern: Layered service-oriented with dispatcher-routed RAG components
Abstractions: Every RAG component (parser, embedder, chunker, generator, etc.) is an abstract base class — new providers are drop-in additions
Communication: REST API over HTTP/JSON
Identity: JWT (registered users) + anonymous guest UUID
State: PostgreSQL for all persistence — no more file-based storage
Rate Limiting: Redis stores slowapi counters — not used for response caching
Every upload is routed through IngestionPipeline, which orchestrates:
Upload → Parse → Detect Language → Clean → Chunk → Embed → Store (pgvector + Postgres)
| Stage | Component | Detail |
|---|---|---|
| Parse | ParseDispatcher → PdfParser / DocxParser / TextParser |
File type detected at dispatch time — each parser extends BaseParser. Processed in-memory, never written to disk. |
| Language Detection | LangDetector |
Detects Arabic vs. English at the document level — drives the cleaning pipeline selection. |
| Clean | CleanDispatcher → ar_clean / en_clean |
Language-specific text normalization. Arabic: diacritic removal, Unicode normalization. English: whitespace, encoding artifacts. |
| Chunk | ChunkDispatcher → RecursiveChunker |
Recursive character splitting with configurable size and overlap. Extends BaseChunker — swap strategies without touching the pipeline. |
| Embed | EmbedDispatcher → JinaEmbedder |
Chunks encoded to dense vectors via the configured embedding provider. Extends BaseEmbedder. |
| Store | pgvector_store + chunk_repo + embedding_repo |
Vectors stored in PostgreSQL pgvector columns. Chunks and metadata in relational tables. Full ACID semantics. |
Every query passes through RetrievalPipeline:
Query → Rewrite → Detect Language → Route → BM25 + pgvector → RRF Fusion → Rerank → Top-K
Query Rewriting (GroqQueryRewriter extending QueryBase) — reformulates the user's question before retrieval for better recall.
Retrieval Router routes to one of:
| Strategy | User-selectable | Condition | Rationale |
|---|---|---|---|
auto |
✅ | Default | Router picks the best strategy per query |
hybrid |
✅ | Explicit request | BM25 + pgvector fused via RRF |
semantic |
✅ | Long conceptual query | Embedding space captures meaning |
lexical |
✅ | Short query / acronym / error code | Exact token matching on technical identifiers |
| Cross-Language | ❌ internal only | Query language ≠ document language | Overrides the selected strategy to semantic — embeddings are language-agnostic, BM25 is not |
RRF Fusion discards raw scores — fuses by rank position only:
RRF Score(doc) = Σ 1 / (k + rank(doc, list_i)) k = 60
BM25 and pgvector scores live on incompatible scales. RRF bypasses the problem entirely.
Reranking — BGE cross-encoder jointly encodes (query, chunk) pairs through a single transformer forward pass. Runs only on the pre-filtered top candidates — precision where it matters, speed everywhere else.
GenerationPipeline orchestrates:
Top-K Chunks → Context Builder → Prompt Builder → GeneratorDispatcher → Answer
| Component | Detail |
|---|---|
| Context Builder | Deduplicates by MD5 hash, applies source attribution, enforces token budget (MAX_CONTEXT_CHARS=4000) |
| Prompt Builder | Multi-turn prompt: system instruction + last N exchanges + context + current query |
SmartGuard (DEFAULT_GENERATOR_PROVIDER) |
Orchestrates provider selection using Redis to track daily Groq request count (resets at UTC midnight). Three zones: below soft threshold (80%) → always Groq; between soft and hard threshold (80–100%) → round-robin between Groq and HF alternating each request; at hard limit (100%) → always HF. On any provider exception, automatically retries with the other provider before returning a graceful error message. |
| GroqGenerator | Primary provider — llama-3.3-70b-versatile, low latency, free tier |
| HFGenerator | Fallback provider — Llama-3.1-8B-Instruct via HuggingFace Inference Router |
| Evaluation | EvaluationDispatcher → CohereEvaluator — LLM-as-judge runs asynchronously after generation; scores faithfulness, answer relevancy, context precision, context recall |
v2 ships two identity modes that coexist in the same system:
Registered Users (JWT)
POST /auth/register → hashed password stored in users table
POST /auth/login → JWT access token returned
All protected endpoints → Bearer <token> in Authorization header
Anonymous Guests
First request → stable UUID derived from browser fingerprint → stored in localStorage
Every request → X-Owner-ID: <uuid> header
Backend → GuestService creates/fetches guest record in users table
Sessions → scoped to guest UUID, invisible to other browsers
The two modes share the same session and document data model — upgrading a guest to a registered account is a clean migration path.
| Layer | Technology | Why |
|---|---|---|
| Backend API | FastAPI + Uvicorn | Async-first · auto OpenAPI docs · Pydantic v2 |
| Database | PostgreSQL + SQLAlchemy + Alembic | ACID semantics · versioned migrations · repository pattern |
| Vector Storage | pgvector | Vectors co-located with relational data — no separate vector DB to operate |
| Rate Limit State | Redis | Stores rate-limit counters via slowapi — not used for response caching |
| Embeddings | Jina Embedder (via EmbedDispatcher) |
Pluggable — swap models without touching pipelines |
| Lexical Search | BM25 (rank-bm25) |
Zero latency · strong complement to semantic search |
| Reranker | BGE cross-encoder | Joint query-passage attention · strong passage relevance precision |
| LLM (Primary) | Groq API | Low-latency inference · free tier · strong instruction following |
| LLM (Fallback) | HuggingFace Inference Router | Redundancy — same GeneratorBase interface |
| Evaluation | Cohere (LLM-as-judge) | No OpenAI dependency — faithfulness + relevancy scoring |
| Auth | JWT (PyJWT) + bcrypt | Industry-standard · stateless tokens |
| Rate Limiting | slowapi |
Per-IP and per-user limits at the API layer |
| Frontend | Vanilla JS + HTML/CSS on Cloudflare Workers | Zero framework overhead · global edge CDN · no cold starts |
| Containerization | Docker + Docker Compose | Postgres + pgvector + Redis + backend in one up command |
TesseractRAG/
│
├── backend/
│ ├── api/
│ │ ├── main.py # FastAPI entry point · CORS · lifespan
│ │ └── v1/
│ │ ├── auth.py # Register · login · token refresh
│ │ ├── chat.py # POST chat — full RAG pipeline
│ │ ├── documents.py # Upload · list · delete documents
│ │ ├── sessions.py # Create · list · delete sessions
│ │ ├── evaluation.py # Retrieve async evaluation results
│ │ └── admin.py # Superuser management endpoints
│ │
│ ├── core/
│ │ ├── config.py # Pydantic Settings — env var loading
│ │ ├── dependencies.py # FastAPI dependency injection
│ │ ├── limiter.py # slowapi rate limiter config
│ │ ├── logger.py # Structured logging
│ │ └── security/
│ │ ├── jwt.py # Token generation and validation
│ │ ├── jwt_deps.py # FastAPI JWT dependencies
│ │ ├── password.py # bcrypt hashing utilities
│ │ └── seed_superuser.py # Initial superuser creation on startup
│ │
│ ├── models/ # Pydantic request/response models
│ │ ├── auth.py · chat.py · documents.py · sessions.py
│ │ ├── embedding.py · evaluation.py · metadata.py
│ │ ├── admin.py · healthStatus.py
│ │ └── enums/ # Typed enums for strategies, roles, statuses
│ │ ├── retrieval_strategy.py # LEXICAL · SEMANTIC · HYBRID · CROSS_LANGUAGE
│ │ ├── embedding_enum.py
│ │ ├── doc_status.py
│ │ ├── evaluation_enum.py
│ │ ├── message_role.py
│ │ ├── generator_prompts.py
│ │ └── query_prompts.py
│ │
│ ├── rag/
│ │ ├── pipelines/
│ │ │ ├── ingestion_pipeline.py # Parse → Clean → Chunk → Embed → Store
│ │ │ ├── retrieval_pipeline.py # Rewrite → Route → Retrieve → Rerank
│ │ │ └── generation_pipeline.py # Context → Prompt → Generate → Evaluate
│ │ │
│ │ └── components/
│ │ ├── lang_detector.py # Arabic / English detection
│ │ ├── parsing/
│ │ │ ├── base_parser.py # Abstract BaseParser
│ │ │ ├── parse_dispatcher.py # Routes by file extension
│ │ │ └── parsers/
│ │ │ ├── PdfParser.py
│ │ │ ├── DocxParser.py
│ │ │ └── TextParser.py
│ │ ├── cleaning/
│ │ │ ├── clean_dispatcher.py # Routes by detected language
│ │ │ ├── ar_clean.py # Arabic normalization
│ │ │ └── en_clean.py # English normalization
│ │ ├── chunking/
│ │ │ ├── base_chunker.py # Abstract BaseChunker
│ │ │ ├── chunk_dispatcher.py
│ │ │ └── recursive_chunker/RecursiveChunker.py
│ │ ├── embedding/
│ │ │ ├── base_embedder.py # Abstract BaseEmbedder
│ │ │ ├── embed_dispatcher.py
│ │ │ └── providers/jina_embedder.py
│ │ ├── retrievals/
│ │ │ ├── bm25_retrieval.py
│ │ │ ├── hybrid_retrieval.py # RRF fusion
│ │ │ ├── cross_language_strategy.py
│ │ │ └── retrieval_router.py # Strategy dispatcher
│ │ ├── reranker/
│ │ │ └── bge_hf.py # BGE cross-encoder
│ │ ├── generator/
│ │ │ ├── generator_base.py # Abstract GeneratorBase
│ │ │ ├── generator_dispatcher.py
│ │ │ ├── groq_generator.py # Primary LLM provider
│ │ │ ├── hf_generator.py # Fallback LLM provider
│ │ │ └── guard.py # SmartGuard: 3-zone logic (Groq → round-robin → HF) tracked via Redis daily counter
│ │ ├── query_rewrite/
│ │ │ ├── query_base.py # Abstract QueryBase
│ │ │ ├── query_dispatcher.py
│ │ │ └── groq_query_rewriter.py
│ │ ├── evaluators/
│ │ │ ├── base_evaluator.py # Abstract BaseEvaluator
│ │ │ ├── evaluation_dispatcher.py
│ │ │ └── cohere_evaluator.py # LLM-as-judge
│ │ └── context/
│ │ └── context_builder.py # MD5 dedup · source attribution · budget
│ │
│ ├── services/ # Business logic layer
│ │ ├── base_service.py # Abstract BaseService
│ │ ├── auth_service.py
│ │ ├── chat_service.py
│ │ ├── document_service.py
│ │ ├── session_service.py
│ │ ├── evaluation_service.py
│ │ ├── admin_service.py
│ │ ├── guest_service.py # Anonymous guest lifecycle
│ │ └── exceptions.py # Typed domain exceptions
│ │
│ ├── storage/
│ │ ├── db/
│ │ │ ├── postgres/
│ │ │ │ ├── postgres_provider.py
│ │ │ │ ├── repo_factory.py # Repository factory
│ │ │ │ ├── repositories/ # One repo per domain entity
│ │ │ │ │ ├── base_repo.py # Abstract BaseRepository
│ │ │ │ │ ├── user_repo.py · session_repo.py · document_repo.py
│ │ │ │ │ ├── chunk_repo.py · embedding_repo.py · message_repo.py
│ │ │ │ │ └── evaluation_repo.py
│ │ │ │ ├── schemas/ # SQLAlchemy ORM models
│ │ │ │ │ ├── sqlalchemy_base.py
│ │ │ │ │ ├── user.py · session.py · document.py · chunk.py
│ │ │ │ │ ├── embedding.py · message.py · evaluation.py
│ │ │ │ └── alembic/ # Versioned DB migrations
│ │ │ │ └── versions/
│ │ │ ├── db_dispatcher.py
│ │ │ └── session_dispatcher.py
│ │ ├── vector_db/
│ │ │ ├── base.py # Abstract VectorStore
│ │ │ ├── vec_dispatcher.py
│ │ │ ├── pgvector/pgvector_store.py # Primary vector backend
│ │ │ └── faiss/faiss_store.py # Fallback / local dev
│ │ └── blob/
│ │ └── factory.py # Pluggable blob storage factory
│ │
│ └── docker/
│ ├── Dockerfile
│ ├── init.sql # pgvector extension bootstrap
│ └── .env.example
│
├── Frontend/
│ ├── index.html # Vanilla JS app — no build step
│ └── wrangler.jsonc # Cloudflare Workers config
│
├── docker-compose.yml # Postgres + pgvector + Redis + backend
├── requirements.txt
├── .env.example
└── LICENSE
# 1. Clone
git clone https://github.com/zeyadusf/tesseractrag.git
cd tesseractrag
# 2. Configure
cp backend/docker/.env.example backend/docker/.env
# Fill in GROQ_API_KEY, COHERE_API_KEY, JWT_SECRET_KEY (see Configuration Reference)
# 3. Launch (Postgres + pgvector + Redis + backend)
docker-compose upDocker Compose will:
- Start PostgreSQL with the pgvector extension (via
init.sql) - Start Redis
- Start the FastAPI backend
- Run Alembic migrations automatically on startup
- Seed the superuser account
| Service | URL |
|---|---|
| Backend API | http://localhost:8000 |
| Interactive API Docs | http://localhost:8000/docs |
| Health Check | http://localhost:8000/health |
# Prerequisites: PostgreSQL 15+ with pgvector, Redis
# Virtual environment
python -m venv .venv
source .venv/bin/activate # macOS / Linux
.venv\Scripts\activate.bat # Windows CMD
# Install dependencies
pip install -r requirements.txt
# Configure
cp backend/docker/.env.example .env
# Edit .env — add DB connection string, Groq/Cohere keys, JWT secret
# Run migrations
cd backend/storage/db/postgres
alembic upgrade head
# Start
cd backend
uvicorn api.main:app --reload --port 8000All endpoints are under /api/v1/. Every protected endpoint requires Authorization: Bearer <token> (OAuth2 password flow). The full interactive spec is available at http://localhost:8000/docs when running.
| Method | Endpoint | Description | Auth |
|---|---|---|---|
POST |
/api/v1/auth/register |
Register — returns TokenPair (access + refresh) |
Public |
POST |
/api/v1/auth/login |
Login (form: username + password) — returns TokenPair |
Public |
POST |
/api/v1/auth/refresh |
Exchange refresh token for a new TokenPair |
Public |
GET |
/api/v1/auth/me |
Get current user profile | Bearer |
POST |
/api/v1/auth/me/password |
Change password | Bearer |
POST |
/api/v1/auth/logout |
Logout (client discards tokens — JWT is stateless) | Bearer |
| Method | Endpoint | Description |
|---|---|---|
POST |
/api/v1/sessions |
Create a new session (name required, description optional) |
GET |
/api/v1/sessions |
List sessions (?active_only=true to filter) |
GET |
/api/v1/sessions/{session_id} |
Get session detail |
PATCH |
/api/v1/sessions/{session_id} |
Rename or update session metadata |
DELETE |
/api/v1/sessions/{session_id} |
Delete session + all documents + chat history |
| Method | Endpoint | Description |
|---|---|---|
POST |
/api/v1/sessions/{session_id}/documents |
Upload and ingest a document (PDF / DOCX / TXT / MD, max 10MB) |
GET |
/api/v1/sessions/{session_id}/documents |
List documents — paginated (page, page_size) |
GET |
/api/v1/sessions/{session_id}/documents/{doc_id} |
Get document metadata + ingestion status (pending / processing / indexed / failed) |
DELETE |
/api/v1/sessions/{session_id}/documents/{doc_id} |
Delete document and all its vector chunks |
| Method | Endpoint | Description |
|---|---|---|
POST |
/api/v1/sessions/{session_id}/chat |
Ask a question — returns grounded answer with sources, latency, and model info |
GET |
/api/v1/sessions/{session_id}/chat |
List all Q&A turns — paginated |
GET |
/api/v1/sessions/{session_id}/chat/{turn_id} |
Get a single chat turn by ID |
DELETE |
/api/v1/sessions/{session_id}/chat |
Clear all chat history for a session |
Example request:
{
"question": "What is reciprocal rank fusion?",
"strategy": "auto",
"show_context": true,
"metadata_filter": null
}Example response:
{
"message_id": "3fa85f64-...",
"question": "What is reciprocal rank fusion?",
"answer": "Reciprocal Rank Fusion merges ranked lists by position...",
"strategy_used": "hybrid",
"sources": [
{
"chunk_id": "abc123",
"content": "RRF score = sum(1 / (k + rank))...",
"score": 0.94,
"source_doc": "survey_ir.pdf",
"chunk_index": 47
}
],
"retrieval_latency_ms": 212,
"total_latency_ms": 890,
"llm_model": "llama-3.3-70b-versatile",
"embedding_model": "jina-embeddings-v3"
}
strategyaccepts:auto·hybrid·semantic·lexical. Cross-language detection is internal — when the query language differs from the document language, the strategy is overridden tosemanticregardless of what was requested (embeddings are language-agnostic; BM25 is not).
llm_modelandembedding_modelare returned in the API response and stored in the database — they are not displayed in the frontend UI but are available for clients building on the API.
Evaluation is triggered manually per session — it scores all un-evaluated assistant turns in batch using Cohere as LLM-as-judge. Metrics: faithfulness, answer_relevancy, context_precision, context_recall (all 0–100).
| Method | Endpoint | Description |
|---|---|---|
POST |
/api/v1/sessions/{session_id}/evaluation |
Evaluate all un-evaluated turns in a session (up to limit, default 200) |
GET |
/api/v1/sessions/{session_id}/evaluation |
List all evaluation results for a session |
GET |
/api/v1/sessions/{session_id}/evaluation/summary |
Get average scores across the session |
GET |
/api/v1/sessions/{session_id}/evaluation/{turn_id} |
Get evaluation result for a specific turn |
| Method | Endpoint | Description |
|---|---|---|
GET |
/api/v1/admin/dashboard |
System-wide stats: users, sessions, messages, documents, global avg eval scores |
GET |
/api/v1/admin/users |
List all users with session/message/document counts and avg eval scores |
GET |
/api/v1/admin/users/{user_id} |
Get user detail |
DELETE |
/api/v1/admin/users/{user_id} |
Delete user |
PATCH |
/api/v1/admin/users/{user_id}/activate |
Activate user account |
PATCH |
/api/v1/admin/users/{user_id}/deactivate |
Deactivate user account |
PATCH |
/api/v1/admin/users/{user_id}/promote |
Promote user to superuser |
PATCH |
/api/v1/admin/users/{user_id}/demote |
Demote superuser to regular user |
GET |
/api/v1/admin/sessions |
List all sessions — filterable by user_id, is_active; sortable by created_at, document_count, message_count |
PATCH |
/api/v1/admin/sessions/{session_id}/activate |
Activate a session |
PATCH |
/api/v1/admin/sessions/{session_id}/deactivate |
Deactivate a session |
GET |
/api/v1/admin/messages |
List all messages — filterable by user_id, session_id, role; sortable by date or eval scores |
Copy .env.example to .env and fill in the required values. Variables marked required have no default and must be set before startup.
| Variable | Default | Required | Description |
|---|---|---|---|
APP_NAME |
TesseractRAG |
Application name | |
APP_VERSION |
v2.0.0 |
Version string | |
DEBUG |
False |
Enable verbose logging | |
SUPPORTED_LANGUAGES |
["en","ar"] |
Languages for detection + cleaning pipelines | |
CROSS_LANGUAGE_STRATEGY_ENABLED |
True |
Enable automatic cross-language retrieval | |
CROSS_LANGUAGE_STRATEGY_LANGUAGE_CONFIDENCE_THRESHOLD |
0.7 |
Minimum confidence to trigger cross-language mode |
| Variable | Default | Required | Description |
|---|---|---|---|
SECRET_KEY |
— | ✅ | JWT signing key — generate with python -c "import secrets; print(secrets.token_hex(32))" |
JWT_ALGORITHM |
HS256 |
JWT algorithm | |
ACCESS_TOKEN_EXPIRE_MINUTES |
30 |
Access token lifetime | |
REFRESH_TOKEN_EXPIRE_DAYS |
7 |
Refresh token lifetime | |
SUPERUSER_EMAIL |
admin@tesseract.ai |
✅ | Seeded superuser email — change before production |
SUPERUSER_USERNAME |
Admin |
✅ | Seeded superuser username |
SUPERUSER_PASSWORD |
admin@tesseract.ai |
✅ | Seeded superuser password — change before production |
| Variable | Default | Required | Description |
|---|---|---|---|
POSTGRES_USERNAME |
postgres |
✅ | DB username |
POSTGRES_PASSWORD |
localtessrag |
✅ | DB password |
POSTGRES_HOST |
pgvector |
✅ | pgvector for Docker · localhost for local dev · hostname for production |
POSTGRES_PORT |
5432 |
DB port | |
POSTGRES_DATABASE_NAME |
tessrag |
✅ | Database name |
DEFAULT_VECTOR_STORE |
pgvector |
Vector backend — pgvector or faiss (local fallback) |
| Variable | Default | Required | Description |
|---|---|---|---|
JINA_API_KEY |
— | ✅ | Jina AI API key — jina.ai |
JINA_MODEL |
jina-embeddings-v3 |
Embedding model | |
EMBED_DIM |
512 |
Embedding dimension — must match the model | |
BATCH_SIZE |
32 |
Chunks per embedding request | |
LATE_CHUNKING |
False |
Standard embedding encodes each chunk independently — a chunk containing "he said" has no idea who "he" is. Late chunking embeds the full document first, then splits the resulting token embeddings into chunks — so every chunk's vector carries full-document context. Produces more accurate vectors for documents with cross-references, pronouns, and interconnected ideas. Disabled by default: requires the entire document to fit in one API request (max MAX_TOKENS_LATE_CHUNKING=8192 tokens, ~6k words), consumes more quota, and adds latency. Enable for long academic papers, legal documents, or any corpus where retrieval precision is noticeably poor. |
| Variable | Default | Required | Description |
|---|---|---|---|
CHUNK_SIZE |
512 |
Max characters per chunk | |
CHUNK_OVERLAP |
64 |
Overlap characters between adjacent chunks | |
CHUNK_MIN_SIZE |
50 |
Chunks shorter than this are discarded as noise |
| Variable | Default | Required | Description |
|---|---|---|---|
RERANKER_MODEL |
BAAI/bge-reranker-base |
BGE cross-encoder model for reranking |
| Variable | Default | Required | Description |
|---|---|---|---|
DEFAULT_GENERATOR_PROVIDER |
smart_guard |
Active provider: smart_guard (recommended) · groq · hf |
|
GENERATOR_GROQ_API_KEY |
— | ✅ | Groq API key — console.groq.com |
GENERATOR_GROQ_MODEL |
llama-3.3-70b-versatile |
Groq model for answer generation | |
GENERATOR_GROQ_DAILY_LIMIT |
13000 |
Daily request budget for Groq | |
GENERATOR_SOFT_THRESHOLD_PCT |
0.8 |
Below 80%: always Groq. Between 80–100%: round-robin Groq/HF per request. | |
GENERATOR_HARD_THRESHOLD_PCT |
1.0 |
At 100%: always HF until midnight reset | |
GENERATOR_HF_API_TOKEN |
— | ✅ (fallback) | HuggingFace API token — fallback when Groq limit is reached |
GENERATOR_HF_MODEL |
meta-llama/Llama-3.1-8B-Instruct |
HF model used as fallback | |
MAX_CONTEXT_CHARS |
4000 |
Max context characters passed to the LLM | |
GENERATOR_MAX_TOKENS |
500 |
Max tokens in generated answer |
| Variable | Default | Required | Description |
|---|---|---|---|
QUERY_GROQ_API_KEY |
— | ✅ | Groq API key for query rewriting (can be same as GENERATOR_GROQ_API_KEY) |
QUERY_MODEL |
llama-3.1-8b-instant |
Model for query rewriting (fast, small) | |
QUERY_MAX_NEW_TOKENS |
150 |
Token budget for rewritten query |
| Variable | Default | Required | Description |
|---|---|---|---|
COHERE_API_KEY |
— | ✅ | Cohere API key — dashboard.cohere.com |
COHERE_EVAL_MODEL |
command-r-plus-08-2024 |
Model used for LLM-as-judge evaluation |
Security:
.envis in.gitignoreand must never be committed. If a secret is accidentally pushed, revoke it immediately on the provider dashboard and rotate all dependent secrets.
v2 is not backward-compatible with v1's storage format. v1 stored session state in Backblaze B2 as JSON + FAISS binary files. v2 stores everything in PostgreSQL.
There is no automatic migration path — v1 sessions cannot be imported into v2. This is an intentional clean break: the data model changed significantly (relational schema vs. flat JSON, pgvector vs. FAISS binary).
To move from v1 to v2:
- Export any documents you want to keep from your v1 sessions
- Deploy v2 with Docker Compose (Postgres + Redis)
- Re-upload your documents into new v2 sessions
- v1 is available at the tagged release
v1.2.0
Key differences to be aware of:
X-Owner-IDheader is still supported for guest access — existing frontends work without changes- Backblaze B2 credentials are no longer required — remove them from
.env - FAISS is available as a local fallback via
VecDispatcherbut is not the default - The API surface (
/api/v1/...) is unchanged — endpoint paths are backward-compatible
| Phase | Name | Status | Key Deliverable |
|---|---|---|---|
| 0 | Environment & Architecture | ✅ Complete | Layered project structure · config · logger · Docker setup |
| 1 | Database & Migrations | ✅ Complete | PostgreSQL schema · SQLAlchemy models · Alembic migrations · repositories |
| 2 | Auth System | ✅ Complete | JWT register/login · guest identity · superuser seeding · rate limiting |
| 3 | Abstract RAG Layer | ✅ Complete | Base classes + dispatchers for all RAG components |
| 4 | Ingestion Pipeline | ✅ Complete | Parse → Detect → Clean → Chunk → Embed → pgvector store |
| 5 | Retrieval Pipeline | ✅ Complete | Query rewrite → Route → BM25 + pgvector → RRF → BGE rerank |
| 6 | Generation Pipeline | ✅ Complete | Context builder · Groq generator · HF fallback · content guard |
| 7 | Evaluation Layer | ✅ Complete | Async Cohere LLM-as-judge · evaluation repo · evaluation endpoint |
| 8 | Admin & Services | ✅ Complete | Admin endpoints · service layer · typed domain exceptions |
| 9 | Frontend & Deployment | ✅ Complete | Cloudflare Workers UI · Docker Compose orchestration |
| Limitation | Cause | Notes |
|---|---|---|
| pgvector requires Postgres 15+ | Extension compatibility | Docker image handles this automatically |
| No streaming LLM responses | Not yet implemented | Token-by-token SSE streaming in roadmap |
| Groq free tier rate limits | ~30 requests/min on free tier | Acceptable for portfolio/personal use |
| Guest sessions tied to localStorage | Client-side identity | Clearing browser storage loses guest sessions |
| No incremental BM25 update | rank_bm25 library limitation |
Full rebuild on each upload — fast enough at this scale |
- SSE streaming LLM responses — token-by-token output
- Fine-tuned retrieval router — replace rule-based classifier with a trained model
- Knowledge graph integration — entity extraction for graph-augmented retrieval
- Answer feedback loop — user ratings stored for future fine-tuning
- Ollama integration — fully local LLM, zero API dependency
- NCBI/PubMed document connectors
- Image extraction from PDFs via PyMuPDF
- Image understanding via LLaVA / BLIP-2
- Multimodal embeddings (CLIP) — unified text + image retrieval space
- Table and figure extraction with structure-aware chunking
The engineering concepts v2 forced me to confront — beyond what v1 taught:
Architecture
- Why flat modules don't scale — and how dispatcher + abstract base class patterns eliminate entire categories of bugs when adding new providers
- The repository pattern: why mixing SQLAlchemy calls into service logic makes code untestable, and how clean repo interfaces fix this
- Why Alembic migrations matter even on solo projects — schema drift without a migration history is invisible until it isn't
Storage
- pgvector vs. FAISS: when co-locating vectors with relational data is better than a separate vector database — and what you give up
- Why storing FAISS indexes as binary files in object storage is fragile (partial writes = silent corruption), and how a transactional DB solves this
Auth
- How JWT stateless auth and anonymous UUID identity can coexist in one system sharing the same data model
- Why bcrypt work factors matter and how to pick one for your hardware
RAG
- Why a simple "switch at threshold" isn't enough — the round-robin zone between soft and hard threshold smooths the transition instead of a hard cutover, and Redis daily counters with UTC midnight reset give you a stateless, restart-safe budget tracker
- The difference between standard and late chunking — standard embedding encodes each chunk in isolation, so cross-references and pronouns lose their context. Late chunking embeds the full document first, then splits the token embeddings: every chunk's vector carries full-document context. The tradeoff is real: better precision on complex documents, but the entire document must fit in one API request (8192 tokens), higher quota cost, and added latency. Disabled by default because for most straightforward documents the precision gain doesn't justify the cost — but the option is there when it does.
- How cross-language retrieval works — query in one language, documents in another — and where it breaks down
- The difference between LLM-as-judge and metric-based evaluation, and why the former catches failure modes the latter misses
TesseractRAG — Because understanding how something works is more valuable than having it work.