Skip to content

Latest commit

History

7 Commits

Folders and files

NameName
Last commit message
Last commit date

Repository files navigation

GenericChatBot

A production-grade RAG (Retrieval-Augmented Generation) chatbot built entirely on local, open-source tooling. No cloud APIs required. Built as a learning project to deeply understand how modern RAG pipelines work — from chunking strategy to hybrid retrieval to cross-encoder re-ranking.


What This Is

An interactive CLI chatbot that answers questions over your own documents. Drop PDFs or text files into refs/, run the indexer once, then chat. The retrieval pipeline implements the full modern RAG stack:

  • Contextual Retrieval (Anthropic, 2024) — chunks enriched with LLM-generated context before embedding
  • Hybrid Search — BM25 (keyword) + semantic (vector) combined via Reciprocal Rank Fusion
  • Cross-encoder Re-ranking — final precision pass over retrieved candidates
  • Three retrieval modes — public (LLM only), private (documents only), hybrid (both)
  • Persistent conversation memory — full history across sessions

Architecture

Indexing Pipeline (run once)

refs/*.pdf / *.txt
│
▼
┌─────────────┐
│ loader │ Extract text from PDFs and text files
└──────┬──────┘
│ documents.json
▼
┌─────────────┐
│ chunker │ Fixed-size sliding window (200 words, 40 overlap)
└──────┬──────┘
│ chunks.json
▼
┌──────────────────┐
│ contextualizer │ LLM generates 1-2 context sentences per chunk
│ (qwen2.5:0.5b) │ Prepended to chunk text before embedding
│ + cache │ MD5 hash cache — only new chunks pay LLM cost
└──────┬───────────┘
│ contextual_chunks.json
▼
┌─────────────┐
│ embedder │ all-MiniLM-L6-v2 → 384-dim vectors
└──────┬──────┘
│ embeddings.json
▼
┌─────────────┐
│ vectorstore │ ChromaDB (persistent SQLite)
└─────────────┘
│
▼
output/chroma_db/

Query Pipeline (every turn)

User question
│
├──────────────────────┬────────────────────────┐
▼ ▼ │
Semantic search BM25 search │
(ChromaDB + embedding) (keyword overlap) │
top-20 candidates top-20 candidates │
│ │ │
└──────────┬───────────┘ │
▼ │
RRF merge │
(Reciprocal Rank Fusion) │
top-20 candidates │
│ │
▼ │
Cross-encoder re-ranker │
(ms-marco-MiniLM-L-6-v2) │
scores each (query, chunk) pair jointly │
threshold gate: score < -3.0 → reject │
top-5 final chunks │
│ │
└──────────────┬─────────────────────┘
▼
Prompt construction
(mode-aware: public / private / hybrid)
│
▼
Ollama: dolphin-llama3
(streamed response)
│
▼
output/memory.json
(full conversation persisted)

Retrieval Modes

ModeRetrievalLLM BehaviourUse When
publicNoneCompletely unrestricted — answers from training knowledge onlyGeneral/public topics. Model knows it better than your docs.
privateFull pipelineStrict — ONLY the indexed documents. Refuses if not found.Internal docs, private data, anything post training cutoff.
hybridFull pipelineFree — uses both context and training knowledge equallyBest of both worlds. Default mode.

Relevance Threshold (private + hybrid)

The cross-encoder scores each (query, chunk) pair. Scores from the MS-MARCO model:

  • > 0 — genuinely relevant
  • -3 to 0 — weakly related
  • < -3 — not relevant (threshold gate)

When the best chunk scores below -3.0, retrieval returns nothing. In private mode this means an immediate "not in documents" response — the LLM never sees the question.


Why Each Technique

Contextual Chunks

Standard chunking strips context at boundaries. A chunk like "It delegates to the parent using super." has no idea it's about Java inheritance — the embedding is blind to origin. Prepending LLM-generated context ("This excerpt is from Chapter 5 on Inheritance...") makes the embedding capture both the concept and where it came from.

Hybrid Search (BM25 + Semantic)

Pure semantic search misses exact keyword matches. BM25 finds AbstractBeanFactory exactly; semantic finds related concepts. Neither alone is best. RRF merges both ranked lists without needing to normalise scores (which would be comparing apples to oranges).

RRF formula:score = 1/(rank_semantic + 60) + 1/(rank_bm25 + 60)

Cross-encoder Re-ranking

Bi-encoders encode query and chunk independently — the relevance signal between them is lost. A cross-encoder feeds both together through a transformer, so every query token can attend to every chunk token. Far more accurate, but too slow to run on all chunks. Solution: run it only on the 20 RRF candidates (~400ms extra per query).


Project Structure

GenericChatBot/
├── indexer/
│ ├── run.py # Orchestrates full indexing pipeline
│ ├── loader.py # Load documents from refs/
│ ├── chunker.py # Fixed-size sliding window chunking
│ ├── contextualizer.py # LLM context enrichment + cache
│ ├── embedder.py # Sentence-transformer embeddings
│ └── vectorstore.py # ChromaDB storage
│
├── chatbot/
│ ├── main.py # CLI entrypoint + conversation loop
│ ├── retriever.py # Hybrid retrieval + RRF + re-ranking
│ ├── generator.py # Prompt construction + Ollama generation
│ └── memory.py # Conversation history (short + long term)
│
├── refs/ # Drop your documents here
│ └── .refignore # Exclude patterns (like .gitignore)
│
├── output/ # Generated — do not edit manually
│ ├── documents.json
│ ├── chunks.json
│ ├── contextual_chunks.json
│ ├── embeddings.json
│ ├── context_cache.json # Contextualizer cache (hash → context)
│ ├── memory.json # Conversation history
│ └── chroma_db/ # ChromaDB vector store
│
└── config.yaml # ChromaDB server config (optional)

Setup

Prerequisites

  • Python 3.10+
  • Ollama installed and running

Install dependencies

python -m venv .venv
source .venv/bin/activate
pip install chromadb sentence-transformers rank-bm25 pypdf requests

Pull required Ollama models

ollama pull dolphin-llama3 # main chat LLM
ollama pull qwen2.5:0.5b # contextualizer (lightweight, fast)

Start Ollama with parallel inference

OLLAMA_NUM_PARALLEL=4 ollama serve

Usage

1. Add your documents

Drop PDFs or text files into refs/. Supported formats: .pdf, .txt, .md, .py, .java, .ts, .js, .html, .json, .yaml, .xml

Use .refignore to exclude files (same syntax as .gitignore).

2. Index

cd GenericChatBot
source .venv/bin/activate
PYTHONUNBUFFERED=1 python -u indexer/run.py

This runs once (or when documents change). The contextualizer step takes the longest — one LLM call per chunk. Subsequent runs use the cache and are near-instant for unchanged chunks.

3. Chat

# Default: hybrid mode
python chatbot/main.py
# LLM answers from its own training knowledge only (no retrieval)
python chatbot/main.py --mode public
# Strict: only answer from your indexed documents
python chatbot/main.py --mode private
# Best of both worlds: context + LLM knowledge combined
python chatbot/main.py --mode hybrid

Type your question and press Enter. Type exit to quit (conversation saved automatically).


Configuration

Key constants you may want to tune:

FileConstantDefaultEffect
contextualizer.pyWORKERS4Parallel Ollama calls during indexing
contextualizer.pyDOC_TRUNCATE_WORDS1000Max doc words passed to LLM per chunk
contextualizer.pyNUM_CTX2048Ollama context window for contextualizer
retriever.pyCANDIDATES_K20Candidates per retriever before RRF
retriever.pyRRF_K60RRF damping constant
retriever.pyRERANKER_THRESHOLD-3.0Min cross-encoder score to consider relevant
chatbot/main.pyTOP_K5Final chunks injected into LLM prompt

Re-indexing

The contextualizer caches results by MD5 hash of each chunk's text in output/context_cache.json. This means:

  • Add a new document → only new chunks pay the LLM cost. Existing 567 chunks: instant cache hits.
  • Edit an existing document → only changed chunks (new hash) are re-contextualised.
  • No document changes → entire contextualizer step is instant.

ChromaDB uses upsert — existing chunk IDs are updated in place, new ones are added.


Tech Stack

ComponentChoiceWhy
LLMdolphin-llama3 via OllamaLocal, uncensored, no API cost
Contextualizer LLMqwen2.5:0.5b via OllamaLightweight — only needs to write 2 sentences
Embedding modelall-MiniLM-L6-v2384D, fast, free, runs locally
Re-rankercross-encoder/ms-marco-MiniLM-L-6-v2Fine-tuned on 1M real search queries
Vector DBChromaDBLocal, no setup, SQLite-backed
Keyword searchrank-bm25BM25Okapi, in-memory, zero config
PDF parsingpypdfPure Python, no system deps

Roadmap

  • Dockerize (indexer + chatbot as separate services)
  • REST API wrapper (FastAPI)
  • Web UI
  • Groq API support for faster contextualisation
  • Sentence-window retrieval
  • Query expansion / HyDE

About

No description, website, or topics provided.

Resources

Stars

2 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages