PromtEngineer/localGPT

Chat with your documents on your local device using GPT models. No data leaves your device and 100% private.

22,202

stars

366

commits

Python

primary language

Aug 26, 2026

updated

README

LocalGPT - Private Document Intelligence Platform

πŸš€ What is LocalGPT?

LocalGPT is a fully private, on-premise Document Intelligence platform. Ask questions, summarise, and uncover insights from your files with state-of-the-art AIβ€”no data ever leaves your machine.

More than a traditional RAG (Retrieval-Augmented Generation) tool, LocalGPT features a hybrid search engine that fuses dense vector search with LanceDB's native full-text search, arbitrated by a calibrated cross-encoder reranker. A smart router picks between RAG and direct LLM answering for every query, while contextual enrichment and sentence-level Context Pruning surface only the most relevant content. Optional passes β€” Late Chunking, an independent answer verification step, and experimental multi-vector (late-interaction) retrieval β€” can be switched on per config; the defaults ship with exactly the components that earned their place in measured evaluations (see eval/decisions/).

The architecture is modular and lightweightβ€”enable only the components you need. The RAG core is plain Python built on the standard library's HTTP server, with no web framework and no agent framework in the way.

▢️ Video

Watch this video to get started with LocalGPT.

HomeCreate IndexChat

✨ Features

  • Utmost Privacy: Your data remains on your computer, ensuring 100% security.
  • Versatile Model Support: Swap generation models freely via Ollama.
  • Diverse Embeddings: HuggingFace embedding models (harrier-oss-v1, the Qwen3-Embedding family) or any Ollama embedding tag.
  • Reuse Your LLM: Once downloaded, reuse your LLM without the need for repeated downloads.
  • API: A REST gateway on port 8000 and the RAG API on port 8001 for building your own applications.
  • CUDA, MPS & CPU: Embedding and reranking pick CUDA, then Apple MPS, then CPU automatically.

πŸ“– Document Processing

  • Formats: PDF, DOCX, HTML/HTM, Markdown, and TXT, parsed by Docling
  • OCR fallback: PDFs with no text layer are re-run through Docling's OCR pipeline; the engine is chosen from whatever is installed (OcrMac on macOS, then EasyOCR, RapidOCR, tesserocr, or the tesseract CLI)
  • Contextual Enrichment: Chunk-level context generated by a small LLM, inspired by Contextual Retrieval
  • Late Chunking (off by default): A second, document-level embedding pass stored in a companion <table>_lc table. The 2026-08-18 component ablation measured its removal at the noise floor on single-turn quality while it doubles the vectors written per index, so it now ships disabled; one flag (retrieval.latechunk.enabled) re-enables both the index-time build and the query-time leg β€” multi-turn conversations with drifting phrasing are where it earns its cost
  • Document Overviews: A short per-document summary written to index_store/overviews/<id>.jsonl and used by the router

πŸ€– AI-Powered Chat

  • Natural Language Queries: Ask questions in plain English
  • Source Attribution: Answers come back with the chunks they were grounded in
  • Smart Routing: Chooses between RAG and a direct LLM answer per query
  • Query Decomposition: Splits complex questions into sub-questions, retrieves per sub-question, then pools the candidates for one rerank and one synthesis pass (per-sub-answer composition is available as an option)
  • Reciprocal Rank Fusion: Vector and full-text hits are fused with RRF β€” no weights to tune
  • Reranking: A cross-encoder pass over the fused candidate set, on by default with calibrated score-based selection (eval/DECISIONS.md)
  • Sentence Pruning: Optional Provence pruning drops irrelevant sentences from each chunk
  • Semantic Caching: TTL cache with a 0.98 similarity threshold, scoped to the session
  • Answer Verification (off by default): A second pass that appends [Confidence: N%] to the answer. Ablation measured zero verdict flips from disabling it β€” it annotates rather than changes answers β€” so it ships disabled; re-enable with verification.enabled

πŸ› οΈ Developer-Friendly

  • RESTful APIs: Every UI action is a documented HTTP call
  • Streaming phases: Server-Sent Events expose each pipeline stage as it runs
  • Flexible Configuration: Models, chunk size, retrieval mode and toggles per request
  • One master config: rag_system/main.py holds every default, overridable by environment variable

🎨 Modern Interface

  • Intuitive Web UI: Clean, responsive design
  • Session Management: Organize conversations by topic
  • Index Management: Easy document collection management
  • Live Progress: Retrieval, reranking and synthesis stages stream into the chat as they happen

πŸš€ Quick Start

Prerequisites

  • Python 3.10+ (3.11 recommended β€” the Docker images use python:3.11-slim)
  • Node.js 20+ and npm
  • Docker (optional, for containerized deployment)
  • 8GB+ RAM (16GB+ recommended)
  • Ollama (required for both deployment approaches)

Option 1: Docker Deployment

# Clone the repository
git clone https://github.com/PromtEngineer/localGPT.git
cd localGPT

# Install Ollama locally (recommended even for Docker)
curl -fsSL https://ollama.ai/install.sh | sh
ollama pull qwen3.5:9b
ollama pull qwen3.5:4b

# Start Ollama
ollama serve

# Start with Docker (in a new terminal)
./start-docker.sh

# Access the application
open http://localhost:3000

If you would rather not install Ollama on the host, run it as a container instead:

./start-docker.sh container
# then pull the models inside the container
docker compose --profile with-ollama exec ollama ollama pull qwen3.5:9b
docker compose --profile with-ollama exec ollama ollama pull qwen3.5:4b

./start-docker.sh (with no argument) uses local Ollama. If nothing is listening on port 11434 it offers to switch to the containerized Ollama; add -y (or set NONINTERACTIVE=1) to take that fallback without a prompt in scripts and CI.

Docker Management Commands:

# Check container status
docker compose ps

# View logs
docker compose logs -f

# Stop containers
./start-docker.sh stop
# Clone the repository
git clone https://github.com/PromtEngineer/localGPT.git
cd localGPT

# Install Python dependencies
pip install -r requirements.txt

# Key dependencies installed:
# - torch==2.4.1, transformers==4.51.0 (embedding + reranker models)
# - lancedb (vector store and full-text search)
# - rerankers (cross-encoder reranking)
# - docling (document parsing)

# Install Node.js dependencies
npm install

# Install and start Ollama
curl -fsSL https://ollama.ai/install.sh | sh
ollama pull qwen3.5:9b
ollama pull qwen3.5:4b
ollama serve

# Start the system (in a new terminal)
python run_system.py

# Access the application
open http://localhost:3000

System Management:

# Check system health (loads the models and runs a sample query)
python system_health_check.py

# Real HTTP health checks against each service; exits non-zero if one is unhealthy
python run_system.py --health

# Start in production mode (runs `npm run build` before `next start`)
python run_system.py --mode prod

# Skip frontend (Ollama + RAG API + backend only)
python run_system.py --no-frontend

# Tail logs/*.log from another shell
python run_system.py --logs-only

# Stop everything recorded in logs/run_system.pid
python run_system.py --stop
# Or press Ctrl+C in the terminal running python run_system.py

Service Architecture: The run_system.py launcher manages four services and writes their PIDs to logs/run_system.pid:

  • Ollama Server (port 11434): model serving β€” reused if already running
  • RAG API Server (port 8001): indexing, retrieval and the agent loop
  • Backend Server (port 8000): sessions, indexes, uploads, chat history
  • Frontend Server (port 3000): Next.js web interface (optional β€” skipped if npm is missing)

On startup the launcher checks that qwen3.5:9b and qwen3.5:4b are present and runs ollama pull for anything missing.

Option 3: Manual Component Startup

# Terminal 1: Start Ollama
ollama serve

# Terminal 2: Start RAG API
python -m rag_system.api_server
# equivalently: python -m rag_system.main api --port 8001

# Terminal 3: Start Backend
python backend/server.py

# Terminal 4: Start Frontend
npm run dev

# Access at http://localhost:3000

Run every command from the repository root. Relative paths (backend/chat_data.db, lancedb/, index_store/, shared_uploads/) resolve against the current working directory, so cd backend && python server.py would create a second database at backend/backend/chat_data.db.


Detailed Installation

1. Install System Dependencies

Ubuntu/Debian:

sudo apt update
sudo apt install python3.11 python3-pip nodejs npm docker.io docker-compose-plugin

macOS:

brew install python@3.11 node docker

Windows:

# Install Python 3.10+, Node.js 20+, and Docker Desktop
# Then use PowerShell or WSL2

2. Install AI Models

Only the two Ollama models need an explicit pull. The embedding model (microsoft/harrier-oss-v1-0.6b, 1.2 GB) is downloaded from HuggingFace the first time it is used; the reranker (~7.5 GB) is loaded lazily β€” downloaded on the first reranked query.

# Install Ollama
curl -fsSL https://ollama.ai/install.sh | sh

# Pull the default models
ollama pull qwen3.5:9b          # answer generation
ollama pull qwen3.5:4b          # routing, triage, enrichment, verification

3. Configure Environment (optional)

Every setting has a working default, so LocalGPT runs with no .env at all. To override one, create a .env in the repository root (rag_system/main.py calls load_dotenv() at import, before its config constants are evaluated; the factory calls it again defensively). .env.example lists the same variables with their code defaults.

VariableDefaultRead by
OLLAMA_HOSThttp://localhost:11434rag_system/main.py, backend/ollama_client.py
RAG_API_URLhttp://localhost:8001backend/server.py (builds /chat and /index)
NEXT_PUBLIC_API_URLhttp://localhost:8000src/lib/api.ts β€” inlined at npm run build
NEXT_PUBLIC_RAG_API_URLhttp://localhost:8001src/lib/api.ts β€” inlined at npm run build
DB_PATHbackend/chat_data.dbbackend/database.py
LANCEDB_PATHstorage.lancedb_uri (./lancedb)rag_system/main.py (pipeline profiles), backend/database.py, system_health_check.py
GENERATION_MODELqwen3.5:9brag_system/main.py, backend/server.py, run_system.py
ENRICHMENT_MODELqwen3.5:4brag_system/main.py, backend/server.py, run_system.py
EMBEDDING_MODELmicrosoft/harrier-oss-v1-0.6brag_system/main.py
RERANKER_MODELQwen/Qwen3-Reranker-4B (loaded lazily on the first reranked query)rag_system/main.py
RAG_CONFIG_MODEdefaultrag_system/api_server.py (default or fast)
RAG_API_TIMEOUT600backend/server.py (seconds to wait for a chat answer)
RAG_API_INDEX_TIMEOUT3600backend/server.py (seconds to wait for an indexing run)
LLM_BACKENDollamarag_system/main.py (ollama or watsonx)
HF_TOKENunsetHuggingFace, for gated model downloads

NEXT_PUBLIC_* values are baked into the frontend bundle by next build. Changing them requires a rebuild (npm run build, or docker compose build frontend).

Changing EMBEDDING_MODEL invalidates existing indexes. Vector width is read from the loaded model, and appending vectors of a different width to an existing LanceDB table raises an error telling you to rebuild. Re-create your indexes after switching embedding models.

4. Initialize the System

# Run system health check
python system_health_check.py

# Initialize the SQLite database
python -c "from backend.database import ChatDatabase; ChatDatabase().init_database()"

# Test the RAG imports
python -c "from rag_system.factory import get_agent; print('βœ… Installation successful!')"

# Validate the running services
python run_system.py --health

🎯 Getting Started

1. Create Your First Index

An index is a collection of processed documents that you can chat with.

Using the Web Interface:

  1. Open http://localhost:3000
  2. Click "Create New Index"
  3. Upload your documents (PDF, DOCX, TXT, MD, HTML)
  4. Configure processing options
  5. Click "Build Index"

Using the CLI:

# Index a single file or a whole directory with the 'default' profile
python -m rag_system.main index ./my_documents

# Use the speed-optimised profile instead
python -m rag_system.main index ./my_documents --mode fast

# Ask one question and print the JSON result
python -m rag_system.main chat "What are the key findings?"

index walks a directory for .pdf, .docx, .html, .htm, .md and .txt files. It writes into the profile's storage.text_table_name (text_pages_v4), which is not the per-index table the web UI creates.

Using the interactive script (creates a UI-visible index):

# Guided prompts: name, documents, chunk size, models
python create_index_script.py

# Non-interactive, from a JSON file
python create_index_script.py --create-sample    # writes index_config.sample.json
python create_index_script.py --batch index_config.sample.json

Using the HTTP API:

# Create index
curl -X POST http://localhost:8000/indexes \
  -H "Content-Type: application/json" \
  -d '{"name": "My Index", "description": "My documents"}'

# Upload documents (form field name must be "files")
curl -X POST http://localhost:8000/indexes/INDEX_ID/upload \
  -F "files=@document.pdf"

# Build index
curl -X POST http://localhost:8000/indexes/INDEX_ID/build \
  -H "Content-Type: application/json" \
  -d '{"chunk_size": 512, "enable_enrich": true, "enable_latechunk": true}'

2. Start Chatting

Once your index is built:

  1. Create a Chat Session: Click "New Chat" or use an existing session
  2. Select Your Index: Choose which document collection to query
  3. Ask Questions: Type natural language questions about your documents
  4. Get Answers: Receive AI-generated responses with source citations

3. Advanced Features

Per-session and per-request model choice

# The session's default generation model
curl -X POST http://localhost:8000/sessions \
  -H "Content-Type: application/json" \
  -d '{"title": "High Quality Session", "model": "qwen3.6:27b"}'

# Override it for one message
curl -X POST http://localhost:8000/sessions/SESSION_ID/messages \
  -H "Content-Type: application/json" \
  -d '{"message": "Summarise section 3", "model": "qwen3.5:4b"}'

The embedding model is a property of the index, not the session β€” choose it when you build the index.

API Integration

import requests

# Talk to the RAG API directly
response = requests.post('http://localhost:8001/chat', json={
    'query': 'What are the key findings in the research papers?',
    'session_id': 'your-session-id',
    'retrieval_mode': 'hybrid',
    'retrieval_k': 20,
})

print(response.json()['answer'])

πŸ”§ Configuration

All defaults live in rag_system/main.py. Every model name there can be overridden with the environment variables listed above.

Model Configuration

RoleDefaultDocumented options
Generation (answers)qwen3.5:9bqwen3.6:27b (high-end, ~17GB), qwen3.5:4b (light)
Enrichment / utility (routing, triage, decomposition, verification)qwen3.5:4bqwen3.5:2b (light)
Embeddingmicrosoft/harrier-oss-v1-0.6b (MIT, 1024 dims)Qwen/Qwen3-Embedding-4B (2560 dims, 32K context, for multilingual / long-context corpora), Qwen/Qwen3-Embedding-0.6B (1024 dims)
Reranker (on by default)Qwen/Qwen3-Reranker-4BBAAI/bge-reranker-v2-m3 (low latency), answerdotai/answerai-colbert-small-v1, Qwen/Qwen3-Reranker-0.6B
# rag_system/main.py
OLLAMA_CONFIG = {
    "host": os.getenv("OLLAMA_HOST", "http://localhost:11434"),
    "generation_model": os.getenv("GENERATION_MODEL", "qwen3.5:9b"),
    "enrichment_model": os.getenv("ENRICHMENT_MODEL", "qwen3.5:4b"),
}

EXTERNAL_MODELS = {
    "embedding_model": os.getenv("EMBEDDING_MODEL", "microsoft/harrier-oss-v1-0.6b"),
    "reranker_model": os.getenv("RERANKER_MODEL", "Qwen/Qwen3-Reranker-4B"),
}

Embedding dimensions are never hardcoded β€” they are measured from the vectors the loaded model produces. If the reranker fails to load, the pipeline logs a warning and continues without reranking rather than falling back to another model.

Vision / multimodal models are not part of the pipeline. PDF parsing and OCR are handled by Docling. Models such as GLM-OCR or Qwen3-VL could be added as a pre-processing step, but they are not integrated today.

Pipeline Configuration

PIPELINE_CONFIGS has exactly two profiles. Select one with RAG_CONFIG_MODE (RAG API) or --mode (CLI).

Default Pipeline (Production-Ready)

"default": {
    "description": "Production-ready pipeline with hybrid search, query decomposition, and verification",
    "storage": {
        "lancedb_uri": "./lancedb",
        "text_table_name": "text_pages_v4"
    },
    "retrieval": {
        "search_type": "hybrid",
        # Off since the 2026-08-18 component ablation; one flag covers the
        # index-time build and the query-time leg.
        "latechunk": {"enabled": False},
        "dense": {"enabled": True},
        "retry": {"enabled": True, "min_top_score": 0.12, "max_attempts": 1},
        # Phase-4 features, all off until benchmarked:
        "document_escalation": {"enabled": False, "max_documents": 1, "token_budget": 6000},
        "crossref_hop": {"enabled": False, "max_hops": 1, "chunks_per_hop": 3},
        "overview_prefilter": {"enabled": False, "top_documents": 5, "mode": "boost"}
    },
    "embedding_model_name": EXTERNAL_MODELS["embedding_model"],
    # On since arm G (2026-08-14): min_score keeps only candidates the
    # calibrated Qwen scorer marks relevant (min_keep is the floor).
    "reranker": {
        "enabled": True,
        "model_type": "cross-encoder",
        "strategy": "rerankers-lib",
        "model_name": EXTERNAL_MODELS["reranker_model"],
        "top_k": 10,
        "min_score": 0.5,
        "min_keep": 3
    },
    # Arm H (2026-08-15): per-sub-query retrieval, pooled + deduped
    # candidates, ONE rerank + ONE synthesis over the union context. The
    # compose path (answer each sub-question, then compose) remains
    # available via compose_from_sub_answers / the UI toggle.
    # Two-variant decomposer: single-turn questions use a frozen prompt;
    # multi-turn requests get a history-aware variant that resolves
    # references to earlier turns. resolve_only skips splitting and uses
    # only the resolved query (measured neutral; kept as an option).
    "query_decomposition": {
        "enabled": True,
        "compose_from_sub_answers": False,
        "pooled_first_stage": True,
        "resolve_only": False
    },
    # Off by default: measured annotate-only (zero verdict flips in ablation).
    "verification": {"enabled": False},
    "retrieval_k": 20,
    "context_window_size": 0,
    "semantic_cache_threshold": 0.98,
    "cache_scope": "session",
    "contextual_enricher": {"enabled": True, "window_size": 1},
    "indexing": {
        "embedding_batch_size": 50,
        "enrichment_batch_size": 10,
        "extract_crossrefs": True
    }
}

Fast Pipeline (Speed-Optimized)

"fast": {
    "description": "Speed-optimized pipeline with minimal overhead",
    "retrieval": {
        "search_type": "vector_only",
        "latechunk": {"enabled": False},
        "dense": {"enabled": True}
    },
    "reranker": {"enabled": False},
    "query_decomposition": {"enabled": False},
    "verification": {"enabled": False},
    "retrieval_k": 10,
    "contextual_enricher": {"enabled": False},
    "indexing": {
        "embedding_batch_size": 100,
        "enrichment_batch_size": 50
    }
}

Retrieval Modes

retrieval_mode (wire name; search_type inside the pipeline config) accepts:

ValueBehaviour
hybrid (default)Vector and LanceDB full-text legs run in parallel and are fused with Reciprocal Rank Fusion
vector_onlyDense vector search only
fts_onlyLanceDB full-text search only

Anything else is rejected with HTTP 400 by the RAG API. There is no dense_weight / denseWeight knob β€” RRF needs no weights.

Experimental: multi-vector (late-interaction) retrieval

The repo carries env-gated hooks for ColBERT-style multi-vector retrieval, served by an out-of-process sidecar (SentenceTransformers v6 needs a newer torch/transformers stack than the pinned in-repo one). All measured, none default β€” the full study is in eval/decisions/multivector-retrieval-2026-08-19.md, paraphrase-robustness-2026-08-20.md and union-fusion-2026-08-20.md:

EnvBehaviourMeasured verdict
MV_RETRIEVAL_ENDPOINTMulti-vector MaxSim replaces the dense legLoses on both document-phrased and paraphrased queries
+ MV_RRF_LEG=1Multi-vector runs as a third RRF legBreak-even; small gain only on paraphrased queries
+ MV_UNION=1All three legs' candidates are unioned (no RRF cut) and the reranker arbitratesBest config for paraphrase-heavy / conversational queries (+4/120 real); costs βˆ’3/120 on document-phrased queries

Rule of thumb: if your users quote the documents' own vocabulary, keep the default 2-leg hybrid; if they ask in their own words, MV_UNION=1 is the measured winner (at ~30–40% extra query latency plus the sidecar process).

πŸ”¬ Evaluation

Every retrieval component in the default profile earned its place in a measured A/B β€” and several plausible features are off because they measurably didn't (late chunking, verification, cross-ref hops, document escalation, multi-vector retrieval). The harness lives in eval/:

  • eval/goldset/ β€” five 24-question corpora (technical RFCs, M&A documents, a service manual, HR policy, this project's docs), a 12-conversation multi-turn set (multiturn.jsonl), and paraphrases.jsonl β€” verified same-meaning rewrites of all 120 questions with ~0.21 content-word overlap, for measuring robustness to users who don't phrase queries like the documents.
  • eval/judge.py β€” the groundedness judge (deterministic local model or a stronger LLM via JUDGE_MODEL); judged comparisons use blind multi-voter panels on every changed row.
  • eval/decisions/ β€” one dated record per experiment: setup, numbers, flip-level panel verdicts, and the decision. If you want to know why a default is what it is, the answer is in there.

πŸ› οΈ Troubleshooting

Common Issues

Installation Problems

# Check Python version
python --version  # 3.10+ required, 3.11 recommended

# Check dependencies
pip list | grep -E "(torch|transformers|lancedb|docling|rerankers)"

# Reinstall dependencies
pip install -r requirements.txt --force-reinstall

Model Loading Issues

# Check Ollama status
ollama list
curl http://localhost:11434/api/tags

# Pull missing models
ollama pull qwen3.5:9b
ollama pull qwen3.5:4b

Database Issues

# Check database connectivity
python -c "from backend.database import ChatDatabase; db = ChatDatabase(); print('βœ… Database OK')"

# Reset database (WARNING: This deletes all sessions, messages and index metadata)
rm backend/chat_data.db
python -c "from backend.database import ChatDatabase; ChatDatabase().init_database()"

Dimension mismatch after changing the embedding model

ValueError: ... changing the embedding model requires rebuilding the index

Delete the affected index in the UI (or DELETE /indexes/{id}) and rebuild it.

Performance Issues

# Check system resources
python system_health_check.py

# Monitor memory usage
htop  # or Task Manager on Windows

# Use lighter models (the default embedder is already the small one at 1.2GB)
export GENERATION_MODEL=qwen3.5:4b

Getting Help

  1. Check Logs: run_system.py writes structured logs to logs/:

    • logs/system.log: launcher events
    • logs/ollama.log, logs/rag-api.log, logs/backend.log, logs/frontend.log: per-service output
    • logs/run_system.pid: PIDs used by --stop
  2. System Health: Run diagnostics:

    python system_health_check.py  # loads models, runs a sample query
    python run_system.py --health  # HTTP checks, non-zero exit on failure
    
  3. Health Endpoints:

    • Backend: http://localhost:8000/health
    • RAG API: http://localhost:8001/health
    • Ollama: http://localhost:11434/api/tags
  4. Documentation: See Documentation/system_overview.md, and Documentation/design_rationale.md for why each component is built the way it is β€” with the evidence and the eval numbers behind every default, plus a "deliberately not implemented" list

  5. GitHub Issues: Report bugs and request features

  6. Community: Join our Discord


πŸ”— API Reference

Two HTTP services. The backend gateway on :8000 owns sessions, indexes, uploads and chat history; the RAG API on :8001 owns retrieval and indexing. Both accept snake_case and camelCase spellings of every option and normalise them to one canonical key.

Backend gateway β€” http://localhost:8000

GET    /health                              # {status, ollama_running, available_models, database_stats}
GET    /models                              # {generation_models, embedding_models}

GET    /sessions                            # {sessions, total}
POST   /sessions                            # {title?, model?} -> 201 {session, session_id}
GET    /sessions/{id}                       # {session, messages}
DELETE /sessions/{id}                       # {deleted: true}
GET    /sessions/cleanup                    # removes empty sessions
POST   /sessions/{id}/rename                # {title} -> {message, session}
GET    /sessions/{id}/documents             # {session, files, file_count}
GET    /sessions/{id}/indexes               # {indexes, total}
POST   /sessions/{id}/indexes/{index_id}    # link an index to a session
POST   /sessions/{id}/upload                # multipart/form-data, field "files"
POST   /sessions/{id}/index                 # index this session's uploads
POST   /sessions/{id}/messages              # chat (see below)

GET    /indexes                             # {indexes, total}
POST   /indexes                             # {name, description?, metadata?} -> 201 {index_id}
GET    /indexes/{id}
DELETE /indexes/{id}                        # also drops the LanceDB table
POST   /indexes/{id}/upload                 # multipart/form-data, field "files"
POST   /indexes/{id}/build                  # build/rebuild from uploaded documents

POST   /chat                                # session-less Ollama chat, no retrieval

Session chat

POST /sessions/{session_id}/messages
Content-Type: application/json

{
  "message": "What are the main topics discussed?",
  "model": "qwen3.5:9b",
  "retrieval_mode": "hybrid",
  "retrieval_k": 20,
  "reranker_top_k": 10,
  "context_window_size": 1,
  "ai_rerank": true,
  "context_expand": true,
  "query_decompose": true,
  "compose_sub_answers": true,
  "verify": true,
  "provence_prune": false,
  "provence_threshold": 0.1,
  "force_rag": false
}

Response:

{
  "response": "…",
  "session": { "...": "updated session row" },
  "source_documents": [],
  "used_rag": true
}

The backend decides per message whether to answer directly with Ollama or to forward to the RAG API. force_rag: true skips that decision and always calls the RAG API. Both the user message and the answer are written to SQLite on this path.

RAG API β€” http://localhost:8001

GET  /health          # {"status": "ok"}
GET  /models          # {generation_models, embedding_models}
POST /chat            # {answer, source_documents}
POST /chat/stream     # Server-Sent Events, terminated by a "complete" event
POST /index           # run the indexing pipeline over file_paths

POST /chat and POST /chat/stream

{
  "query": "Explain the methodology",
  "session_id": "uuid",
  "table_name": "text_pages_<index_id>",
  "model": "qwen3.5:9b",
  "retrieval_mode": "hybrid",
  "retrieval_k": 20,
  "context_window_size": 1,
  "reranker_top_k": 10,
  "ai_rerank": true,
  "context_expand": true,
  "query_decompose": true,
  "compose_sub_answers": true,
  "verify": true,
  "force_rag": false,
  "provence_prune": false,
  "provence_threshold": 0.1
}

/chat returns {"answer": "...", "source_documents": [...]}. There is no top-level confidence field β€” when verification runs it appends [Confidence: N%] (and a low-confidence warning) to the answer text itself.

/chat/stream emits data: {"type": "<phase>", "data": {...}} lines and ends with a complete event carrying the same object /chat would return.

force_rag: true skips the agent's triage step so the query always goes through retrieval; verify, ai_rerank, query_decompose and compose_sub_answers still apply. An unsupported retrieval_mode is rejected with HTTP 400.

POST /index

{
  "file_paths": ["/abs/path/doc1.pdf", "/abs/path/doc2.pdf"],
  "session_id": "uuid",
  "table_name": "text_pages_<index_id>",
  "chunk_size": 512,
  "window_size": 2,
  "retrieval_mode": "hybrid",
  "enable_enrich": true,
  "enable_latechunk": false,
  "enable_docling_chunk": true,
  "embedding_model": "microsoft/harrier-oss-v1-0.6b",
  "enrich_model": "qwen3.5:4b",
  "overview_model_name": "qwen3.5:4b",
  "batch_size_embed": 50,
  "batch_size_enrich": 25
}

file_paths is required; the values above are the defaults applied when a field is omitted. Response:

{
  "message": "Indexing process for 2 file(s) completed successfully.",
  "table_name": "text_pages_<index_id>",
  "latechunk": false,
  "docling_chunk": true,
  "indexing_config": {
    "chunk_size": 512,
    "retrieval_mode": "hybrid",
    "window_size": 2,
    "enable_enrich": true,
    "embedding_model": "microsoft/harrier-oss-v1-0.6b",
    "enrich_model": "qwen3.5:4b",
    "overview_model_name": "qwen3.5:4b",
    "batch_size_embed": 50,
    "batch_size_enrich": 25
  }
}

retrieval_mode at index time is validated and recorded with the index config; it takes effect at query time. enable_docling_chunk defaults to true (Docling structure-aware chunking); sending false selects the legacy chunker. Indexing is synchronous β€” the call returns when the pipeline finishes, which is why the backend allows up to RAG_API_INDEX_TIMEOUT (default 3600s) for it.

For the full route table see Documentation/api_reference.md.

Known limitations

  • The RAG API is single-threaded. Requests are serialised: one chat or indexing run at a time. The backend gateway is threaded, so it stays responsive, but a long RAG call blocks the next one.
  • Streamed turns are persisted after the fact. The chat UI streams from :8001/chat/stream directly and, when the stream completes, saves the finished turn through the gateway (POST /sessions/{id}/messages/save). If the browser is closed mid-stream, that turn is not saved.
  • Index metadata is per index, not per session. Choosing a different embedding model requires rebuilding the index.

πŸ—οΈ Architecture

graph TB
    UI[Next.js UI :3000] --> API[Backend gateway :8000]
    UI -. "SSE /chat/stream" .-> RAGAPI
    API --> RAGAPI[RAG API :8001]
    RAGAPI --> Agent[RAG Agent]
    Agent --> Retrieval[Retrieval Pipeline]
    Agent --> Ollama[Ollama :11434]

    Retrieval --> Vector[Vector search]
    Retrieval --> FTS[LanceDB full-text search]
    Vector --> RRF[Reciprocal Rank Fusion]
    FTS --> RRF
    RRF --> Rerank["Cross-encoder rerank (on by default)"]

    Vector --> LanceDB[(LanceDB)]
    FTS --> LanceDB

    API --> SQLite[(SQLite: sessions, messages, indexes)]
    RAGAPI --> SQLite

Overview of the Retrieval Agent

graph TD
    classDef llmcall fill:#e6f3ff,stroke:#007bff;
    classDef pipeline fill:#e6ffe6,stroke:#28a745;
    classDef cache fill:#fff3e0,stroke:#fd7e14;

    A(Start: Agent.run) --> C{_run_async};

    C --> C1[Get chat history];
    C1 --> T0{force_rag?};
    T0 -- Yes --> RAG_Path;
    T0 -- No --> T1[Route via document overviews];
    T1 --> T2["LLM triage fallback:<br/>rag_query | direct_answer"]; class T2 llmcall;
    T2 --> T3{Decision?};

    T3 -- rag_query --> RAG_Path;
    T3 -- direct_answer --> LLM_Path;

    subgraph RAG Path
        RAG_Path --> R1[Format query + history];
        R1 --> R2[Embed query]; class R2 pipeline;
        R2 --> R3{{Semantic cache<br/>threshold 0.98, session-scoped}}; class R3 cache;
        R3 -- Hit --> FinalResult;
        R3 -- Miss --> R4{Decomposition enabled?};

        R4 -- Yes --> R5[Decompose query]; class R5 llmcall;
        R5 --> R6{{Retrieve per sub-query, then pool + dedupe the candidates}}; class R6 pipeline;
        R6 --> R8[One rerank + one synthesis over the pooled context]; class R8 llmcall;
        R8 --> V1(RAG answer);

        R4 -- No --> R9[Run single query through the retrieval pipeline]; class R9 pipeline;
        R9 --> V1;

        V1 --> V2{{Verification}}; class V2 llmcall;
        V2 --> R_Cache_Store{{Store in semantic cache}}; class R_Cache_Store cache;
        R_Cache_Store --> FinalResult;
    end

    subgraph Direct LLM Path
        LLM_Path --> L2[Generate answer without retrieval]; class L2 llmcall;
        L2 --> FinalResult(Final result);
    end

    FinalResult --> R_Hist_Update(Update in-memory chat history);
    R_Hist_Update --> ZZZ["End: return answer + source_documents"];

Inside the retrieval pipeline a query runs: embed β†’ hybrid retrieve (vector + FTS, fused with RRF) β†’ optional late-chunk leg β†’ cross-encoder rerank (on by default) β†’ context window expansion β†’ optional Provence sentence pruning β†’ synthesis.


🀝 Contributing

We welcome contributions from developers of all skill levels! LocalGPT is an open-source project that benefits from community involvement.

πŸš€ Quick Start for Contributors

# Fork and clone the repository
git clone https://github.com/PromtEngineer/localGPT.git
cd localGPT

# Set up development environment
pip install -r requirements.txt
npm install

# Install Ollama and models
curl -fsSL https://ollama.ai/install.sh | sh
ollama pull qwen3.5:9b
ollama pull qwen3.5:4b

# Verify setup
python system_health_check.py
python run_system.py --mode dev

πŸ“‹ How to Contribute

  1. πŸ› Report Bugs: Use our bug report template
  2. πŸ’‘ Request Features: Use our feature request template
  3. πŸ”§ Submit Code: Follow our development workflow
  4. πŸ“š Improve Docs: Help make our documentation better

πŸ“– Detailed Guidelines

For comprehensive contributing guidelines, including:

  • Development setup and workflow
  • Coding standards and best practices
  • Testing requirements
  • Documentation standards
  • Release process

πŸ‘‰ See our CONTRIBUTING.md guide


πŸ“„ License

This project is licensed under the MIT License - see the LICENSE file for details. For models, please check their respective licenses.


πŸ“ž Support


Star History

Star History Chart

Contributors

(top 30 of 45)

PromtEngineer

210 commits

imjwang

19 commits

LeafmanZ

14 commits

PromtEngineer/localGPT

Chat with your documents on your local device using GPT models. No data leaves your device and 100% private.

22,202

stars

366

commits

Python

primary language

Aug 26, 2026

updated

README

LocalGPT - Private Document Intelligence Platform

πŸš€ What is LocalGPT?

LocalGPT is a fully private, on-premise Document Intelligence platform. Ask questions, summarise, and uncover insights from your files with state-of-the-art AIβ€”no data ever leaves your machine.

More than a traditional RAG (Retrieval-Augmented Generation) tool, LocalGPT features a hybrid search engine that fuses dense vector search with LanceDB's native full-text search, arbitrated by a calibrated cross-encoder reranker. A smart router picks between RAG and direct LLM answering for every query, while contextual enrichment and sentence-level Context Pruning surface only the most relevant content. Optional passes β€” Late Chunking, an independent answer verification step, and experimental multi-vector (late-interaction) retrieval β€” can be switched on per config; the defaults ship with exactly the components that earned their place in measured evaluations (see eval/decisions/).

The architecture is modular and lightweightβ€”enable only the components you need. The RAG core is plain Python built on the standard library's HTTP server, with no web framework and no agent framework in the way.

▢️ Video

Watch this video to get started with LocalGPT.

HomeCreate IndexChat

✨ Features

  • Utmost Privacy: Your data remains on your computer, ensuring 100% security.
  • Versatile Model Support: Swap generation models freely via Ollama.
  • Diverse Embeddings: HuggingFace embedding models (harrier-oss-v1, the Qwen3-Embedding family) or any Ollama embedding tag.
  • Reuse Your LLM: Once downloaded, reuse your LLM without the need for repeated downloads.
  • API: A REST gateway on port 8000 and the RAG API on port 8001 for building your own applications.
  • CUDA, MPS & CPU: Embedding and reranking pick CUDA, then Apple MPS, then CPU automatically.

πŸ“– Document Processing

  • Formats: PDF, DOCX, HTML/HTM, Markdown, and TXT, parsed by Docling
  • OCR fallback: PDFs with no text layer are re-run through Docling's OCR pipeline; the engine is chosen from whatever is installed (OcrMac on macOS, then EasyOCR, RapidOCR, tesserocr, or the tesseract CLI)
  • Contextual Enrichment: Chunk-level context generated by a small LLM, inspired by Contextual Retrieval
  • Late Chunking (off by default): A second, document-level embedding pass stored in a companion <table>_lc table. The 2026-08-18 component ablation measured its removal at the noise floor on single-turn quality while it doubles the vectors written per index, so it now ships disabled; one flag (retrieval.latechunk.enabled) re-enables both the index-time build and the query-time leg β€” multi-turn conversations with drifting phrasing are where it earns its cost
  • Document Overviews: A short per-document summary written to index_store/overviews/<id>.jsonl and used by the router

πŸ€– AI-Powered Chat

  • Natural Language Queries: Ask questions in plain English
  • Source Attribution: Answers come back with the chunks they were grounded in
  • Smart Routing: Chooses between RAG and a direct LLM answer per query
  • Query Decomposition: Splits complex questions into sub-questions, retrieves per sub-question, then pools the candidates for one rerank and one synthesis pass (per-sub-answer composition is available as an option)
  • Reciprocal Rank Fusion: Vector and full-text hits are fused with RRF β€” no weights to tune
  • Reranking: A cross-encoder pass over the fused candidate set, on by default with calibrated score-based selection (eval/DECISIONS.md)
  • Sentence Pruning: Optional Provence pruning drops irrelevant sentences from each chunk
  • Semantic Caching: TTL cache with a 0.98 similarity threshold, scoped to the session
  • Answer Verification (off by default): A second pass that appends [Confidence: N%] to the answer. Ablation measured zero verdict flips from disabling it β€” it annotates rather than changes answers β€” so it ships disabled; re-enable with verification.enabled

πŸ› οΈ Developer-Friendly

  • RESTful APIs: Every UI action is a documented HTTP call
  • Streaming phases: Server-Sent Events expose each pipeline stage as it runs
  • Flexible Configuration: Models, chunk size, retrieval mode and toggles per request
  • One master config: rag_system/main.py holds every default, overridable by environment variable

🎨 Modern Interface

  • Intuitive Web UI: Clean, responsive design
  • Session Management: Organize conversations by topic
  • Index Management: Easy document collection management
  • Live Progress: Retrieval, reranking and synthesis stages stream into the chat as they happen

πŸš€ Quick Start

Prerequisites

  • Python 3.10+ (3.11 recommended β€” the Docker images use python:3.11-slim)
  • Node.js 20+ and npm
  • Docker (optional, for containerized deployment)
  • 8GB+ RAM (16GB+ recommended)
  • Ollama (required for both deployment approaches)

Option 1: Docker Deployment

# Clone the repository
git clone https://github.com/PromtEngineer/localGPT.git
cd localGPT

# Install Ollama locally (recommended even for Docker)
curl -fsSL https://ollama.ai/install.sh | sh
ollama pull qwen3.5:9b
ollama pull qwen3.5:4b

# Start Ollama
ollama serve

# Start with Docker (in a new terminal)
./start-docker.sh

# Access the application
open http://localhost:3000

If you would rather not install Ollama on the host, run it as a container instead:

./start-docker.sh container
# then pull the models inside the container
docker compose --profile with-ollama exec ollama ollama pull qwen3.5:9b
docker compose --profile with-ollama exec ollama ollama pull qwen3.5:4b

./start-docker.sh (with no argument) uses local Ollama. If nothing is listening on port 11434 it offers to switch to the containerized Ollama; add -y (or set NONINTERACTIVE=1) to take that fallback without a prompt in scripts and CI.

Docker Management Commands:

# Check container status
docker compose ps

# View logs
docker compose logs -f

# Stop containers
./start-docker.sh stop
# Clone the repository
git clone https://github.com/PromtEngineer/localGPT.git
cd localGPT

# Install Python dependencies
pip install -r requirements.txt

# Key dependencies installed:
# - torch==2.4.1, transformers==4.51.0 (embedding + reranker models)
# - lancedb (vector store and full-text search)
# - rerankers (cross-encoder reranking)
# - docling (document parsing)

# Install Node.js dependencies
npm install

# Install and start Ollama
curl -fsSL https://ollama.ai/install.sh | sh
ollama pull qwen3.5:9b
ollama pull qwen3.5:4b
ollama serve

# Start the system (in a new terminal)
python run_system.py

# Access the application
open http://localhost:3000

System Management:

# Check system health (loads the models and runs a sample query)
python system_health_check.py

# Real HTTP health checks against each service; exits non-zero if one is unhealthy
python run_system.py --health

# Start in production mode (runs `npm run build` before `next start`)
python run_system.py --mode prod

# Skip frontend (Ollama + RAG API + backend only)
python run_system.py --no-frontend

# Tail logs/*.log from another shell
python run_system.py --logs-only

# Stop everything recorded in logs/run_system.pid
python run_system.py --stop
# Or press Ctrl+C in the terminal running python run_system.py

Service Architecture: The run_system.py launcher manages four services and writes their PIDs to logs/run_system.pid:

  • Ollama Server (port 11434): model serving β€” reused if already running
  • RAG API Server (port 8001): indexing, retrieval and the agent loop
  • Backend Server (port 8000): sessions, indexes, uploads, chat history
  • Frontend Server (port 3000): Next.js web interface (optional β€” skipped if npm is missing)

On startup the launcher checks that qwen3.5:9b and qwen3.5:4b are present and runs ollama pull for anything missing.

Option 3: Manual Component Startup

# Terminal 1: Start Ollama
ollama serve

# Terminal 2: Start RAG API
python -m rag_system.api_server
# equivalently: python -m rag_system.main api --port 8001

# Terminal 3: Start Backend
python backend/server.py

# Terminal 4: Start Frontend
npm run dev

# Access at http://localhost:3000

Run every command from the repository root. Relative paths (backend/chat_data.db, lancedb/, index_store/, shared_uploads/) resolve against the current working directory, so cd backend && python server.py would create a second database at backend/backend/chat_data.db.


Detailed Installation

1. Install System Dependencies

Ubuntu/Debian:

sudo apt update
sudo apt install python3.11 python3-pip nodejs npm docker.io docker-compose-plugin

macOS:

brew install python@3.11 node docker

Windows:

# Install Python 3.10+, Node.js 20+, and Docker Desktop
# Then use PowerShell or WSL2

2. Install AI Models

Only the two Ollama models need an explicit pull. The embedding model (microsoft/harrier-oss-v1-0.6b, 1.2 GB) is downloaded from HuggingFace the first time it is used; the reranker (~7.5 GB) is loaded lazily β€” downloaded on the first reranked query.

# Install Ollama
curl -fsSL https://ollama.ai/install.sh | sh

# Pull the default models
ollama pull qwen3.5:9b          # answer generation
ollama pull qwen3.5:4b          # routing, triage, enrichment, verification

3. Configure Environment (optional)

Every setting has a working default, so LocalGPT runs with no .env at all. To override one, create a .env in the repository root (rag_system/main.py calls load_dotenv() at import, before its config constants are evaluated; the factory calls it again defensively). .env.example lists the same variables with their code defaults.

VariableDefaultRead by
OLLAMA_HOSThttp://localhost:11434rag_system/main.py, backend/ollama_client.py
RAG_API_URLhttp://localhost:8001backend/server.py (builds /chat and /index)
NEXT_PUBLIC_API_URLhttp://localhost:8000src/lib/api.ts β€” inlined at npm run build
NEXT_PUBLIC_RAG_API_URLhttp://localhost:8001src/lib/api.ts β€” inlined at npm run build
DB_PATHbackend/chat_data.dbbackend/database.py
LANCEDB_PATHstorage.lancedb_uri (./lancedb)rag_system/main.py (pipeline profiles), backend/database.py, system_health_check.py
GENERATION_MODELqwen3.5:9brag_system/main.py, backend/server.py, run_system.py
ENRICHMENT_MODELqwen3.5:4brag_system/main.py, backend/server.py, run_system.py
EMBEDDING_MODELmicrosoft/harrier-oss-v1-0.6brag_system/main.py
RERANKER_MODELQwen/Qwen3-Reranker-4B (loaded lazily on the first reranked query)rag_system/main.py
RAG_CONFIG_MODEdefaultrag_system/api_server.py (default or fast)
RAG_API_TIMEOUT600backend/server.py (seconds to wait for a chat answer)
RAG_API_INDEX_TIMEOUT3600backend/server.py (seconds to wait for an indexing run)
LLM_BACKENDollamarag_system/main.py (ollama or watsonx)
HF_TOKENunsetHuggingFace, for gated model downloads

NEXT_PUBLIC_* values are baked into the frontend bundle by next build. Changing them requires a rebuild (npm run build, or docker compose build frontend).

Changing EMBEDDING_MODEL invalidates existing indexes. Vector width is read from the loaded model, and appending vectors of a different width to an existing LanceDB table raises an error telling you to rebuild. Re-create your indexes after switching embedding models.

4. Initialize the System

# Run system health check
python system_health_check.py

# Initialize the SQLite database
python -c "from backend.database import ChatDatabase; ChatDatabase().init_database()"

# Test the RAG imports
python -c "from rag_system.factory import get_agent; print('βœ… Installation successful!')"

# Validate the running services
python run_system.py --health

🎯 Getting Started

1. Create Your First Index

An index is a collection of processed documents that you can chat with.

Using the Web Interface:

  1. Open http://localhost:3000
  2. Click "Create New Index"
  3. Upload your documents (PDF, DOCX, TXT, MD, HTML)
  4. Configure processing options
  5. Click "Build Index"

Using the CLI:

# Index a single file or a whole directory with the 'default' profile
python -m rag_system.main index ./my_documents

# Use the speed-optimised profile instead
python -m rag_system.main index ./my_documents --mode fast

# Ask one question and print the JSON result
python -m rag_system.main chat "What are the key findings?"

index walks a directory for .pdf, .docx, .html, .htm, .md and .txt files. It writes into the profile's storage.text_table_name (text_pages_v4), which is not the per-index table the web UI creates.

Using the interactive script (creates a UI-visible index):

# Guided prompts: name, documents, chunk size, models
python create_index_script.py

# Non-interactive, from a JSON file
python create_index_script.py --create-sample    # writes index_config.sample.json
python create_index_script.py --batch index_config.sample.json

Using the HTTP API:

# Create index
curl -X POST http://localhost:8000/indexes \
  -H "Content-Type: application/json" \
  -d '{"name": "My Index", "description": "My documents"}'

# Upload documents (form field name must be "files")
curl -X POST http://localhost:8000/indexes/INDEX_ID/upload \
  -F "files=@document.pdf"

# Build index
curl -X POST http://localhost:8000/indexes/INDEX_ID/build \
  -H "Content-Type: application/json" \
  -d '{"chunk_size": 512, "enable_enrich": true, "enable_latechunk": true}'

2. Start Chatting

Once your index is built:

  1. Create a Chat Session: Click "New Chat" or use an existing session
  2. Select Your Index: Choose which document collection to query
  3. Ask Questions: Type natural language questions about your documents
  4. Get Answers: Receive AI-generated responses with source citations

3. Advanced Features

Per-session and per-request model choice

# The session's default generation model
curl -X POST http://localhost:8000/sessions \
  -H "Content-Type: application/json" \
  -d '{"title": "High Quality Session", "model": "qwen3.6:27b"}'

# Override it for one message
curl -X POST http://localhost:8000/sessions/SESSION_ID/messages \
  -H "Content-Type: application/json" \
  -d '{"message": "Summarise section 3", "model": "qwen3.5:4b"}'

The embedding model is a property of the index, not the session β€” choose it when you build the index.

API Integration

import requests

# Talk to the RAG API directly
response = requests.post('http://localhost:8001/chat', json={
    'query': 'What are the key findings in the research papers?',
    'session_id': 'your-session-id',
    'retrieval_mode': 'hybrid',
    'retrieval_k': 20,
})

print(response.json()['answer'])

πŸ”§ Configuration

All defaults live in rag_system/main.py. Every model name there can be overridden with the environment variables listed above.

Model Configuration

RoleDefaultDocumented options
Generation (answers)qwen3.5:9bqwen3.6:27b (high-end, ~17GB), qwen3.5:4b (light)
Enrichment / utility (routing, triage, decomposition, verification)qwen3.5:4bqwen3.5:2b (light)
Embeddingmicrosoft/harrier-oss-v1-0.6b (MIT, 1024 dims)Qwen/Qwen3-Embedding-4B (2560 dims, 32K context, for multilingual / long-context corpora), Qwen/Qwen3-Embedding-0.6B (1024 dims)
Reranker (on by default)Qwen/Qwen3-Reranker-4BBAAI/bge-reranker-v2-m3 (low latency), answerdotai/answerai-colbert-small-v1, Qwen/Qwen3-Reranker-0.6B
# rag_system/main.py
OLLAMA_CONFIG = {
    "host": os.getenv("OLLAMA_HOST", "http://localhost:11434"),
    "generation_model": os.getenv("GENERATION_MODEL", "qwen3.5:9b"),
    "enrichment_model": os.getenv("ENRICHMENT_MODEL", "qwen3.5:4b"),
}

EXTERNAL_MODELS = {
    "embedding_model": os.getenv("EMBEDDING_MODEL", "microsoft/harrier-oss-v1-0.6b"),
    "reranker_model": os.getenv("RERANKER_MODEL", "Qwen/Qwen3-Reranker-4B"),
}

Embedding dimensions are never hardcoded β€” they are measured from the vectors the loaded model produces. If the reranker fails to load, the pipeline logs a warning and continues without reranking rather than falling back to another model.

Vision / multimodal models are not part of the pipeline. PDF parsing and OCR are handled by Docling. Models such as GLM-OCR or Qwen3-VL could be added as a pre-processing step, but they are not integrated today.

Pipeline Configuration

PIPELINE_CONFIGS has exactly two profiles. Select one with RAG_CONFIG_MODE (RAG API) or --mode (CLI).

Default Pipeline (Production-Ready)

"default": {
    "description": "Production-ready pipeline with hybrid search, query decomposition, and verification",
    "storage": {
        "lancedb_uri": "./lancedb",
        "text_table_name": "text_pages_v4"
    },
    "retrieval": {
        "search_type": "hybrid",
        # Off since the 2026-08-18 component ablation; one flag covers the
        # index-time build and the query-time leg.
        "latechunk": {"enabled": False},
        "dense": {"enabled": True},
        "retry": {"enabled": True, "min_top_score": 0.12, "max_attempts": 1},
        # Phase-4 features, all off until benchmarked:
        "document_escalation": {"enabled": False, "max_documents": 1, "token_budget": 6000},
        "crossref_hop": {"enabled": False, "max_hops": 1, "chunks_per_hop": 3},
        "overview_prefilter": {"enabled": False, "top_documents": 5, "mode": "boost"}
    },
    "embedding_model_name": EXTERNAL_MODELS["embedding_model"],
    # On since arm G (2026-08-14): min_score keeps only candidates the
    # calibrated Qwen scorer marks relevant (min_keep is the floor).
    "reranker": {
        "enabled": True,
        "model_type": "cross-encoder",
        "strategy": "rerankers-lib",
        "model_name": EXTERNAL_MODELS["reranker_model"],
        "top_k": 10,
        "min_score": 0.5,
        "min_keep": 3
    },
    # Arm H (2026-08-15): per-sub-query retrieval, pooled + deduped
    # candidates, ONE rerank + ONE synthesis over the union context. The
    # compose path (answer each sub-question, then compose) remains
    # available via compose_from_sub_answers / the UI toggle.
    # Two-variant decomposer: single-turn questions use a frozen prompt;
    # multi-turn requests get a history-aware variant that resolves
    # references to earlier turns. resolve_only skips splitting and uses
    # only the resolved query (measured neutral; kept as an option).
    "query_decomposition": {
        "enabled": True,
        "compose_from_sub_answers": False,
        "pooled_first_stage": True,
        "resolve_only": False
    },
    # Off by default: measured annotate-only (zero verdict flips in ablation).
    "verification": {"enabled": False},
    "retrieval_k": 20,
    "context_window_size": 0,
    "semantic_cache_threshold": 0.98,
    "cache_scope": "session",
    "contextual_enricher": {"enabled": True, "window_size": 1},
    "indexing": {
        "embedding_batch_size": 50,
        "enrichment_batch_size": 10,
        "extract_crossrefs": True
    }
}

Fast Pipeline (Speed-Optimized)

"fast": {
    "description": "Speed-optimized pipeline with minimal overhead",
    "retrieval": {
        "search_type": "vector_only",
        "latechunk": {"enabled": False},
        "dense": {"enabled": True}
    },
    "reranker": {"enabled": False},
    "query_decomposition": {"enabled": False},
    "verification": {"enabled": False},
    "retrieval_k": 10,
    "contextual_enricher": {"enabled": False},
    "indexing": {
        "embedding_batch_size": 100,
        "enrichment_batch_size": 50
    }
}

Retrieval Modes

retrieval_mode (wire name; search_type inside the pipeline config) accepts:

ValueBehaviour
hybrid (default)Vector and LanceDB full-text legs run in parallel and are fused with Reciprocal Rank Fusion
vector_onlyDense vector search only
fts_onlyLanceDB full-text search only

Anything else is rejected with HTTP 400 by the RAG API. There is no dense_weight / denseWeight knob β€” RRF needs no weights.

Experimental: multi-vector (late-interaction) retrieval

The repo carries env-gated hooks for ColBERT-style multi-vector retrieval, served by an out-of-process sidecar (SentenceTransformers v6 needs a newer torch/transformers stack than the pinned in-repo one). All measured, none default β€” the full study is in eval/decisions/multivector-retrieval-2026-08-19.md, paraphrase-robustness-2026-08-20.md and union-fusion-2026-08-20.md:

EnvBehaviourMeasured verdict
MV_RETRIEVAL_ENDPOINTMulti-vector MaxSim replaces the dense legLoses on both document-phrased and paraphrased queries
+ MV_RRF_LEG=1Multi-vector runs as a third RRF legBreak-even; small gain only on paraphrased queries
+ MV_UNION=1All three legs' candidates are unioned (no RRF cut) and the reranker arbitratesBest config for paraphrase-heavy / conversational queries (+4/120 real); costs βˆ’3/120 on document-phrased queries

Rule of thumb: if your users quote the documents' own vocabulary, keep the default 2-leg hybrid; if they ask in their own words, MV_UNION=1 is the measured winner (at ~30–40% extra query latency plus the sidecar process).

πŸ”¬ Evaluation

Every retrieval component in the default profile earned its place in a measured A/B β€” and several plausible features are off because they measurably didn't (late chunking, verification, cross-ref hops, document escalation, multi-vector retrieval). The harness lives in eval/:

  • eval/goldset/ β€” five 24-question corpora (technical RFCs, M&A documents, a service manual, HR policy, this project's docs), a 12-conversation multi-turn set (multiturn.jsonl), and paraphrases.jsonl β€” verified same-meaning rewrites of all 120 questions with ~0.21 content-word overlap, for measuring robustness to users who don't phrase queries like the documents.
  • eval/judge.py β€” the groundedness judge (deterministic local model or a stronger LLM via JUDGE_MODEL); judged comparisons use blind multi-voter panels on every changed row.
  • eval/decisions/ β€” one dated record per experiment: setup, numbers, flip-level panel verdicts, and the decision. If you want to know why a default is what it is, the answer is in there.

πŸ› οΈ Troubleshooting

Common Issues

Installation Problems

# Check Python version
python --version  # 3.10+ required, 3.11 recommended

# Check dependencies
pip list | grep -E "(torch|transformers|lancedb|docling|rerankers)"

# Reinstall dependencies
pip install -r requirements.txt --force-reinstall

Model Loading Issues

# Check Ollama status
ollama list
curl http://localhost:11434/api/tags

# Pull missing models
ollama pull qwen3.5:9b
ollama pull qwen3.5:4b

Database Issues

# Check database connectivity
python -c "from backend.database import ChatDatabase; db = ChatDatabase(); print('βœ… Database OK')"

# Reset database (WARNING: This deletes all sessions, messages and index metadata)
rm backend/chat_data.db
python -c "from backend.database import ChatDatabase; ChatDatabase().init_database()"

Dimension mismatch after changing the embedding model

ValueError: ... changing the embedding model requires rebuilding the index

Delete the affected index in the UI (or DELETE /indexes/{id}) and rebuild it.

Performance Issues

# Check system resources
python system_health_check.py

# Monitor memory usage
htop  # or Task Manager on Windows

# Use lighter models (the default embedder is already the small one at 1.2GB)
export GENERATION_MODEL=qwen3.5:4b

Getting Help

  1. Check Logs: run_system.py writes structured logs to logs/:

    • logs/system.log: launcher events
    • logs/ollama.log, logs/rag-api.log, logs/backend.log, logs/frontend.log: per-service output
    • logs/run_system.pid: PIDs used by --stop
  2. System Health: Run diagnostics:

    python system_health_check.py  # loads models, runs a sample query
    python run_system.py --health  # HTTP checks, non-zero exit on failure
    
  3. Health Endpoints:

    • Backend: http://localhost:8000/health
    • RAG API: http://localhost:8001/health
    • Ollama: http://localhost:11434/api/tags
  4. Documentation: See Documentation/system_overview.md, and Documentation/design_rationale.md for why each component is built the way it is β€” with the evidence and the eval numbers behind every default, plus a "deliberately not implemented" list

  5. GitHub Issues: Report bugs and request features

  6. Community: Join our Discord


πŸ”— API Reference

Two HTTP services. The backend gateway on :8000 owns sessions, indexes, uploads and chat history; the RAG API on :8001 owns retrieval and indexing. Both accept snake_case and camelCase spellings of every option and normalise them to one canonical key.

Backend gateway β€” http://localhost:8000

GET    /health                              # {status, ollama_running, available_models, database_stats}
GET    /models                              # {generation_models, embedding_models}

GET    /sessions                            # {sessions, total}
POST   /sessions                            # {title?, model?} -> 201 {session, session_id}
GET    /sessions/{id}                       # {session, messages}
DELETE /sessions/{id}                       # {deleted: true}
GET    /sessions/cleanup                    # removes empty sessions
POST   /sessions/{id}/rename                # {title} -> {message, session}
GET    /sessions/{id}/documents             # {session, files, file_count}
GET    /sessions/{id}/indexes               # {indexes, total}
POST   /sessions/{id}/indexes/{index_id}    # link an index to a session
POST   /sessions/{id}/upload                # multipart/form-data, field "files"
POST   /sessions/{id}/index                 # index this session's uploads
POST   /sessions/{id}/messages              # chat (see below)

GET    /indexes                             # {indexes, total}
POST   /indexes                             # {name, description?, metadata?} -> 201 {index_id}
GET    /indexes/{id}
DELETE /indexes/{id}                        # also drops the LanceDB table
POST   /indexes/{id}/upload                 # multipart/form-data, field "files"
POST   /indexes/{id}/build                  # build/rebuild from uploaded documents

POST   /chat                                # session-less Ollama chat, no retrieval

Session chat

POST /sessions/{session_id}/messages
Content-Type: application/json

{
  "message": "What are the main topics discussed?",
  "model": "qwen3.5:9b",
  "retrieval_mode": "hybrid",
  "retrieval_k": 20,
  "reranker_top_k": 10,
  "context_window_size": 1,
  "ai_rerank": true,
  "context_expand": true,
  "query_decompose": true,
  "compose_sub_answers": true,
  "verify": true,
  "provence_prune": false,
  "provence_threshold": 0.1,
  "force_rag": false
}

Response:

{
  "response": "…",
  "session": { "...": "updated session row" },
  "source_documents": [],
  "used_rag": true
}

The backend decides per message whether to answer directly with Ollama or to forward to the RAG API. force_rag: true skips that decision and always calls the RAG API. Both the user message and the answer are written to SQLite on this path.

RAG API β€” http://localhost:8001

GET  /health          # {"status": "ok"}
GET  /models          # {generation_models, embedding_models}
POST /chat            # {answer, source_documents}
POST /chat/stream     # Server-Sent Events, terminated by a "complete" event
POST /index           # run the indexing pipeline over file_paths

POST /chat and POST /chat/stream

{
  "query": "Explain the methodology",
  "session_id": "uuid",
  "table_name": "text_pages_<index_id>",
  "model": "qwen3.5:9b",
  "retrieval_mode": "hybrid",
  "retrieval_k": 20,
  "context_window_size": 1,
  "reranker_top_k": 10,
  "ai_rerank": true,
  "context_expand": true,
  "query_decompose": true,
  "compose_sub_answers": true,
  "verify": true,
  "force_rag": false,
  "provence_prune": false,
  "provence_threshold": 0.1
}

/chat returns {"answer": "...", "source_documents": [...]}. There is no top-level confidence field β€” when verification runs it appends [Confidence: N%] (and a low-confidence warning) to the answer text itself.

/chat/stream emits data: {"type": "<phase>", "data": {...}} lines and ends with a complete event carrying the same object /chat would return.

force_rag: true skips the agent's triage step so the query always goes through retrieval; verify, ai_rerank, query_decompose and compose_sub_answers still apply. An unsupported retrieval_mode is rejected with HTTP 400.

POST /index

{
  "file_paths": ["/abs/path/doc1.pdf", "/abs/path/doc2.pdf"],
  "session_id": "uuid",
  "table_name": "text_pages_<index_id>",
  "chunk_size": 512,
  "window_size": 2,
  "retrieval_mode": "hybrid",
  "enable_enrich": true,
  "enable_latechunk": false,
  "enable_docling_chunk": true,
  "embedding_model": "microsoft/harrier-oss-v1-0.6b",
  "enrich_model": "qwen3.5:4b",
  "overview_model_name": "qwen3.5:4b",
  "batch_size_embed": 50,
  "batch_size_enrich": 25
}

file_paths is required; the values above are the defaults applied when a field is omitted. Response:

{
  "message": "Indexing process for 2 file(s) completed successfully.",
  "table_name": "text_pages_<index_id>",
  "latechunk": false,
  "docling_chunk": true,
  "indexing_config": {
    "chunk_size": 512,
    "retrieval_mode": "hybrid",
    "window_size": 2,
    "enable_enrich": true,
    "embedding_model": "microsoft/harrier-oss-v1-0.6b",
    "enrich_model": "qwen3.5:4b",
    "overview_model_name": "qwen3.5:4b",
    "batch_size_embed": 50,
    "batch_size_enrich": 25
  }
}

retrieval_mode at index time is validated and recorded with the index config; it takes effect at query time. enable_docling_chunk defaults to true (Docling structure-aware chunking); sending false selects the legacy chunker. Indexing is synchronous β€” the call returns when the pipeline finishes, which is why the backend allows up to RAG_API_INDEX_TIMEOUT (default 3600s) for it.

For the full route table see Documentation/api_reference.md.

Known limitations

  • The RAG API is single-threaded. Requests are serialised: one chat or indexing run at a time. The backend gateway is threaded, so it stays responsive, but a long RAG call blocks the next one.
  • Streamed turns are persisted after the fact. The chat UI streams from :8001/chat/stream directly and, when the stream completes, saves the finished turn through the gateway (POST /sessions/{id}/messages/save). If the browser is closed mid-stream, that turn is not saved.
  • Index metadata is per index, not per session. Choosing a different embedding model requires rebuilding the index.

πŸ—οΈ Architecture

graph TB
    UI[Next.js UI :3000] --> API[Backend gateway :8000]
    UI -. "SSE /chat/stream" .-> RAGAPI
    API --> RAGAPI[RAG API :8001]
    RAGAPI --> Agent[RAG Agent]
    Agent --> Retrieval[Retrieval Pipeline]
    Agent --> Ollama[Ollama :11434]

    Retrieval --> Vector[Vector search]
    Retrieval --> FTS[LanceDB full-text search]
    Vector --> RRF[Reciprocal Rank Fusion]
    FTS --> RRF
    RRF --> Rerank["Cross-encoder rerank (on by default)"]

    Vector --> LanceDB[(LanceDB)]
    FTS --> LanceDB

    API --> SQLite[(SQLite: sessions, messages, indexes)]
    RAGAPI --> SQLite

Overview of the Retrieval Agent

graph TD
    classDef llmcall fill:#e6f3ff,stroke:#007bff;
    classDef pipeline fill:#e6ffe6,stroke:#28a745;
    classDef cache fill:#fff3e0,stroke:#fd7e14;

    A(Start: Agent.run) --> C{_run_async};

    C --> C1[Get chat history];
    C1 --> T0{force_rag?};
    T0 -- Yes --> RAG_Path;
    T0 -- No --> T1[Route via document overviews];
    T1 --> T2["LLM triage fallback:<br/>rag_query | direct_answer"]; class T2 llmcall;
    T2 --> T3{Decision?};

    T3 -- rag_query --> RAG_Path;
    T3 -- direct_answer --> LLM_Path;

    subgraph RAG Path
        RAG_Path --> R1[Format query + history];
        R1 --> R2[Embed query]; class R2 pipeline;
        R2 --> R3{{Semantic cache<br/>threshold 0.98, session-scoped}}; class R3 cache;
        R3 -- Hit --> FinalResult;
        R3 -- Miss --> R4{Decomposition enabled?};

        R4 -- Yes --> R5[Decompose query]; class R5 llmcall;
        R5 --> R6{{Retrieve per sub-query, then pool + dedupe the candidates}}; class R6 pipeline;
        R6 --> R8[One rerank + one synthesis over the pooled context]; class R8 llmcall;
        R8 --> V1(RAG answer);

        R4 -- No --> R9[Run single query through the retrieval pipeline]; class R9 pipeline;
        R9 --> V1;

        V1 --> V2{{Verification}}; class V2 llmcall;
        V2 --> R_Cache_Store{{Store in semantic cache}}; class R_Cache_Store cache;
        R_Cache_Store --> FinalResult;
    end

    subgraph Direct LLM Path
        LLM_Path --> L2[Generate answer without retrieval]; class L2 llmcall;
        L2 --> FinalResult(Final result);
    end

    FinalResult --> R_Hist_Update(Update in-memory chat history);
    R_Hist_Update --> ZZZ["End: return answer + source_documents"];

Inside the retrieval pipeline a query runs: embed β†’ hybrid retrieve (vector + FTS, fused with RRF) β†’ optional late-chunk leg β†’ cross-encoder rerank (on by default) β†’ context window expansion β†’ optional Provence sentence pruning β†’ synthesis.


🀝 Contributing

We welcome contributions from developers of all skill levels! LocalGPT is an open-source project that benefits from community involvement.

πŸš€ Quick Start for Contributors

# Fork and clone the repository
git clone https://github.com/PromtEngineer/localGPT.git
cd localGPT

# Set up development environment
pip install -r requirements.txt
npm install

# Install Ollama and models
curl -fsSL https://ollama.ai/install.sh | sh
ollama pull qwen3.5:9b
ollama pull qwen3.5:4b

# Verify setup
python system_health_check.py
python run_system.py --mode dev

πŸ“‹ How to Contribute

  1. πŸ› Report Bugs: Use our bug report template
  2. πŸ’‘ Request Features: Use our feature request template
  3. πŸ”§ Submit Code: Follow our development workflow
  4. πŸ“š Improve Docs: Help make our documentation better

πŸ“– Detailed Guidelines

For comprehensive contributing guidelines, including:

  • Development setup and workflow
  • Coding standards and best practices
  • Testing requirements
  • Documentation standards
  • Release process

πŸ‘‰ See our CONTRIBUTING.md guide


πŸ“„ License

This project is licensed under the MIT License - see the LICENSE file for details. For models, please check their respective licenses.


πŸ“ž Support


Star History

Star History Chart

Contributors

(top 30 of 45)

PromtEngineer

210 commits

imjwang

19 commits

LeafmanZ

14 commits

Languages

Python

80.3%

TypeScript

18.2%