CoolFace
Apppublic

eraxes/rag-chatbot

sourceHugging Faceupdated 3mo agoView on Hugging Face
0likes
App README

RAG Chatbot

๐Ÿš€ Live demo: [huggingface.co/spaces/eraxes/rag-chatbot](https://huggingface.co/spaces/eraxes/rag-chatbot)

A document question-answering web app built with FastAPI, local embeddings, ChromaDB, and a provider-agnostic LLM layer (Groq, Google Gemini, or Claude). Upload your documents (PDF / TXT / MD), then ask questions and get answers grounded in your content โ€” with cited sources โ€” through a clean web UI.

This is a Retrieval-Augmented Generation (RAG) system: instead of letting the LLM answer from memory (and hallucinate), it retrieves the most relevant chunks from your documents and instructs the model to answer only from that context.


Features

  • โ€”๐Ÿ’ฌ Web UI โ€” drag-and-drop upload, chat with streaming-style feedback, source citations
  • โ€”๐Ÿ“„ Ingest PDF, TXT, and Markdown files
  • โ€”๐Ÿ” Semantic search with local embeddings (free, no extra API key, multilingual TR/EN)
  • โ€”๐Ÿค– Grounded answers with inline citations ([1], [2]) and expandable source cards
  • โ€”๐Ÿ”Œ Provider-agnostic LLM โ€” swap between Groq, Gemini, and Claude via one env var
  • โ€”๐Ÿงช Evaluation harness โ€” scores retrieval recall, faithfulness & correctness (LLM-as-a-judge)
  • โ€”๐Ÿ—‚๏ธ Persistent vector store (ChromaDB) โ€” data survives restarts
  • โ€”โšก Auto-generated API docs at /docs (Swagger UI)
  • โ€”โœ… Clean error handling (missing key โ†’ 401, rate limit โ†’ 429/502) and unit tests

How RAG works here

INGESTION (once per document)
  file โ”€โ”€โ–ถ extract text โ”€โ”€โ–ถ chunk โ”€โ”€โ–ถ embed (vector) โ”€โ”€โ–ถ store in ChromaDB

QUERY (every question)
  question โ”€โ”€โ–ถ embed โ”€โ”€โ–ถ find nearest chunks โ”€โ”€โ–ถ feed to LLM as context โ”€โ”€โ–ถ cited answer

The core idea: tell the LLM "don't make things up โ€” answer based on these retrieved passages." This reduces hallucination and makes answers auditable.


Tech stack

LayerChoiceWhy
FrontendHTML + Tailwind (CDN) + vanilla JSSingle page, zero build, served by FastAPI
APIFastAPI + UvicornModern, fast, automatic OpenAPI docs
Embeddingsintfloat/multilingual-e5-small (local)Free, runs offline, supports Turkish + English
Vector DBChromaDBLocal, persistent, stores metadata for citations
LLMGroq / Gemini / Claude (default Groq)Answer generation, swappable via LLM_PROVIDER
PDF parsingpypdfText extraction

Project structure

rag-chatbot/
โ”œโ”€โ”€ app/
โ”‚   โ”œโ”€โ”€ config.py        # Settings (loaded from .env)
โ”‚   โ”œโ”€โ”€ models.py        # Pydantic request/response schemas
โ”‚   โ”œโ”€โ”€ ingestion.py     # Text extraction + chunking
โ”‚   โ”œโ”€โ”€ embeddings.py    # Local embedding model wrapper
โ”‚   โ”œโ”€โ”€ vectorstore.py   # ChromaDB wrapper (add / query / delete)
โ”‚   โ”œโ”€โ”€ llm.py           # Provider-agnostic LLM layer (Groq / Gemini / Claude)
โ”‚   โ”œโ”€โ”€ rag.py           # Retrieval + generation
โ”‚   โ””โ”€โ”€ main.py          # FastAPI app, endpoints, serves the frontend
โ”œโ”€โ”€ static/
โ”‚   โ””โ”€โ”€ index.html       # Single-page web UI
โ”œโ”€โ”€ evals/
โ”‚   โ”œโ”€โ”€ dataset.json     # Golden Q&A test set (2 docs + distractor + refusals)
โ”‚   โ”œโ”€โ”€ metrics.py       # Deterministic metric (context recall)
โ”‚   โ”œโ”€โ”€ judge.py         # LLM-as-a-judge (faithfulness + correctness)
โ”‚   โ””โ”€โ”€ run_eval.py      # Eval runner (rate limit, retry, resume, report)
โ”œโ”€โ”€ tests/
โ”‚   โ””โ”€โ”€ test_chunking.py # Unit tests for the chunker
โ”œโ”€โ”€ examples/
โ”‚   โ”œโ”€โ”€ sirket_politikalari.txt         # Sample document (Acme)
โ”‚   โ””โ”€โ”€ beta_teknoloji_politikalari.txt # Distractor document (Beta)
โ”œโ”€โ”€ requirements.txt
โ”œโ”€โ”€ .env.example
โ””โ”€โ”€ run.sh

Setup

bash
# 1. (Recommended) create a virtual environment
python3 -m venv .venv && source .venv/bin/activate

# 2. Install dependencies
pip install -r requirements.txt

# 3. Configure a provider (pick one โ€” all have a free option)
cp .env.example .env
#   then open .env and set your key(s). See "Choosing a provider" below.

# 4. Run
./run.sh            # or: uvicorn app.main:app --reload --port 8000

Open http://localhost:8000 for the web UI, or /docs for the interactive API.

Choosing a provider

The LLM layer is provider-agnostic; pick one in .env via LLM_PROVIDER:

Provider`.env` keysFree tierWhere
Groq (default)LLM_PROVIDER=groq ยท GROQ_API_KEY=gsk_...Generous daily limitconsole.groq.com
GeminiLLM_PROVIDER=gemini ยท GEMINI_API_KEY=...Low daily cap on some modelsaistudio.google.com/apikey
ClaudeLLM_PROVIDER=claude ยท ANTHROPIC_API_KEY=sk-ant-... ยท CHAT_MODEL=claude-opus-4-8Paidconsole.anthropic.com

The eval judge is configured separately (JUDGE_PROVIDER / JUDGE_MODEL), so you can grade answers with a different / stronger model than the one that generates them.


API

MethodPathDescription
GET/Web UI
POST/ingestUpload a document (multipart file)
POST/chatAsk a question โ†’ answer + sources
GET/documentsList ingested documents
DELETE/documentsClear all documents
GET/healthHealth check + chunk count

Example:

bash
curl -X POST http://localhost:8000/ingest -F "file=@examples/sirket_politikalari.txt"
curl -X POST http://localhost:8000/chat \
  -H "Content-Type: application/json" \
  -d '{"question": "How many vacation days do employees get?"}'

Tests

bash
pytest -q

Evaluation (eval harness)

A RAG system that seems to work isn't enough โ€” you have to measure it. This repo ships a small but real evaluation harness (evals/) that scores the pipeline against a golden dataset and acts as a quality gate.

What it measures

MetricWhat it checksHow
Context recallDid retrieval fetch the chunk that holds the answer?Deterministic keyword check โ€” no LLM, free, repeatable
FaithfulnessIs the answer grounded in the retrieved context (no hallucination)?LLM-as-a-judge
CorrectnessDoes the answer match the reference (ground-truth) answer?LLM-as-a-judge

Two design choices worth calling out:

  • โ€”Judge โ‰  generator. The judge goes through the same provider-agnostic layer but points at a different model (JUDGE_PROVIDER / JUDGE_MODEL). Grading a model with itself invites self-preference bias.
  • โ€”Resilient & cheap. Rate limiting, retry-with-backoff on transient/quota (429/503) errors, and a resumable cache mean a run that hits a free-tier limit picks up where it left off instead of starting over.

Run it

bash
python3 -m evals.run_eval          # resume from cache (skips completed questions)
python3 -m evals.run_eval --fresh  # run everything from scratch

You get a per-question pass/fail table (against thresholds) plus a detailed evals/report.json with every answer and the judge's reasoning.

A real eval-driven fix

The dataset includes a distractor document: two companies (Acme & Beta) with the same topics but different numbers, and questions that force the system to pick the right one. That immediately caught a real bug โ€” the generator answered Acme's training-budget question with Beta's figure, and leaked Beta's health-insurance policy into an Acme answer:

              recall  faith  correct
acme-egitim    1.00    0.00    0.00   โŒ  gave Beta's 7500 โ‚บ instead of Acme's 10000 โ‚บ
acme-saglik      โ€”     0.00    0.00   โŒ  claimed Acme has insurance (that's Beta's)
                                          โ†’ pass rate 80%

recall = 1.00 while faithfulness = 0 pinpointed the failure to generation, not retrieval. Root cause: chunking stripped the company name from later chunks, so the model couldn't tell the two documents apart. The fix was contextual chunking (chunk_text_with_context โ€” prepend every chunk with its document title). Changing only that (same prompt, same model) took the score to:

ORTALAMA       1.00    0.98    1.00   โœ…  pass rate 100%

That loop โ€” measure โ†’ find the weakness โ†’ fix โ†’ re-measure โ€” is the entire point of the harness.


Possible improvements (roadmap)

  • โ€”Streaming responses for the chat endpoint (token-by-token)
  • โ€”Re-ranking retrieved chunks with a cross-encoder for better precision
  • โ€”Bigger eval set โ€” more documents, adversarial / multi-hop questions, a stronger judge model
  • โ€”Hybrid search (keyword + semantic)
  • โ€”Authentication and per-user document isolation
  • โ€”Swap ChromaDB for Qdrant / pgvector for production scale

The interesting parts to read first: [`app/rag.py`](app/rag.py) (retrieval + generation), [`app/ingestion.py`](app/ingestion.py) (contextual chunking), [`app/llm.py`](app/llm.py) (provider abstraction), and [`evals/run_eval.py`](evals/run_eval.py) (the eval harness).