Spaces:
Sleeping
Sleeping
Vamshi Pokala commited on
Commit Β·
ce71763
1
Parent(s): 7d14dfa
Redesigned the README and added Citiation and truthfullness in responses
Browse files- .coverage +0 -0
- .github/workflows/ci.yml +34 -0
- README.md +179 -148
- config.yaml +3 -2
- docker/.env.example +1 -1
- evals/datasets/golden.jsonl +52 -0
- evals/datasets/golden_ci.jsonl +20 -0
- rag-spec-evaluation.md +96 -0
- requirements/base.txt +1 -0
- src/core/document_processor.py +39 -6
- src/ingest.py +11 -2
- src/utils/config.py +3 -2
- tests/unit/test_config.py +5 -4
- tests/unit/test_document_processor.py +10 -9
.coverage
ADDED
|
Binary file (53.2 kB). View file
|
|
|
.github/workflows/ci.yml
CHANGED
|
@@ -108,3 +108,37 @@ jobs:
|
|
| 108 |
with:
|
| 109 |
name: eval-report
|
| 110 |
path: evals/reports/
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 108 |
with:
|
| 109 |
name: eval-report
|
| 110 |
path: evals/reports/
|
| 111 |
+
|
| 112 |
+
evals-golden:
|
| 113 |
+
name: Golden evals (Anthropic Haiku)
|
| 114 |
+
if: ${{ secrets.ANTHROPIC_API_KEY != '' }}
|
| 115 |
+
runs-on: ubuntu-latest
|
| 116 |
+
env:
|
| 117 |
+
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
|
| 118 |
+
steps:
|
| 119 |
+
- uses: actions/checkout@v4
|
| 120 |
+
|
| 121 |
+
- uses: actions/setup-python@v5
|
| 122 |
+
with:
|
| 123 |
+
python-version: "3.13"
|
| 124 |
+
cache: pip
|
| 125 |
+
|
| 126 |
+
- name: Install dependencies
|
| 127 |
+
run: pip install -r requirements/base.txt
|
| 128 |
+
|
| 129 |
+
- name: Run golden evals (live pipeline, Anthropic Haiku)
|
| 130 |
+
run: |
|
| 131 |
+
PYTHONPATH=. python -m evals.run_evals \
|
| 132 |
+
--dataset evals/datasets/golden_ci.jsonl \
|
| 133 |
+
--judge-provider anthropic \
|
| 134 |
+
--judge-model claude-haiku-4-5 \
|
| 135 |
+
--output evals/reports/ \
|
| 136 |
+
--faithfulness-threshold 0.7 \
|
| 137 |
+
--correctness-threshold 0.2
|
| 138 |
+
|
| 139 |
+
- name: Upload golden eval report
|
| 140 |
+
if: always()
|
| 141 |
+
uses: actions/upload-artifact@v4
|
| 142 |
+
with:
|
| 143 |
+
name: eval-report-golden
|
| 144 |
+
path: evals/reports/
|
README.md
CHANGED
|
@@ -12,60 +12,171 @@ license: mit
|
|
| 12 |
|
| 13 |
# Doc-Ingestion
|
| 14 |
|
| 15 |
-
|
| 16 |
|
| 17 |
-
> **[Try the live demo on Hugging Face Spaces](https://huggingface.co/spaces/vampokala/doc-ingestion)**
|
| 18 |
|
| 19 |
-
##
|
| 20 |
|
| 21 |
-
|
| 22 |
-
- Retrieves using Reciprocal Rank Fusion across sparse and dense search, with optional cross-encoder reranking
|
| 23 |
-
- Generates answers via any LLM provider (Ollama, OpenAI, Anthropic, Gemini) with citation tracking
|
| 24 |
-
- Scores every response for **truthfulness** (NLI faithfulness + citation groundedness)
|
| 25 |
-
- Exposes a FastAPI backend and a Streamlit UI
|
| 26 |
|
| 27 |
-
-
|
| 28 |
|
| 29 |
-
##
|
| 30 |
|
| 31 |
-
|
| 32 |
|
| 33 |
-
|
| 34 |
-
In hosted demo mode (`DOC_PROFILE=demo`), Streamlit executes queries in-process through the shared orchestrator so demo usage is not blocked by localhost API startup races.
|
| 35 |
|
| 36 |
-
--
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 37 |
|
| 38 |
-
##
|
| 39 |
|
| 40 |
-
|
| 41 |
-
|
| 42 |
-
|
| 43 |
-
|
| 44 |
-
|
| 45 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 46 |
```
|
| 47 |
|
| 48 |
-
|
| 49 |
|
| 50 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 51 |
|
| 52 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 53 |
|
| 54 |
```bash
|
| 55 |
git clone https://github.com/vampokala/Doc-Ingestion
|
| 56 |
cd Doc-Ingestion
|
| 57 |
-
|
|
|
|
|
|
|
| 58 |
```
|
| 59 |
|
| 60 |
-
|
| 61 |
-
|
|
|
|
| 62 |
|
| 63 |
```bash
|
| 64 |
-
|
| 65 |
-
|
|
|
|
| 66 |
```
|
| 67 |
|
| 68 |
-
|
| 69 |
|
| 70 |
```bash
|
| 71 |
source .venv/bin/activate
|
|
@@ -73,80 +184,16 @@ source .venv/bin/activate
|
|
| 73 |
# API server
|
| 74 |
PYTHONPATH=. uvicorn src.api.main:app --reload --port 8000
|
| 75 |
|
| 76 |
-
# Streamlit UI
|
| 77 |
PYTHONPATH=. streamlit run src/web/streamlit_app.py
|
| 78 |
|
| 79 |
-
#
|
| 80 |
PYTHONPATH=. python -m src.query "What is RAG?"
|
| 81 |
```
|
| 82 |
|
| 83 |
-
|
| 84 |
|
| 85 |
-
|
| 86 |
-
|
| 87 |
-
## Features
|
| 88 |
-
|
| 89 |
-
- Multi-format ingestion (PDF, DOCX, TXT, MD, HTML)
|
| 90 |
-
- Hybrid retrieval β BM25 + vector with weighted RRF fusion
|
| 91 |
-
- Optional cross-encoder reranking (`cross-encoder/ms-marco-MiniLM-L-6-v2`)
|
| 92 |
-
- Multi-provider LLM routing: Ollama (local), OpenAI, Anthropic, Gemini β switchable per request
|
| 93 |
-
- Citation tracking and per-citation verification
|
| 94 |
-
- **Inline truthfulness scoring** on every response (NLI faithfulness + citation groundedness)
|
| 95 |
-
- **Offline eval harness** β RAGAS-style metrics over a golden dataset
|
| 96 |
-
- FastAPI with auth, rate limiting (Redis or in-memory), streaming SSE
|
| 97 |
-
- Streamlit UI with per-request provider/model switching and truthfulness badge
|
| 98 |
-
|
| 99 |
-
---
|
| 100 |
-
|
| 101 |
-
## Architecture
|
| 102 |
-
|
| 103 |
-
<details>
|
| 104 |
-
<summary>System diagram</summary>
|
| 105 |
-
|
| 106 |
-
```mermaid
|
| 107 |
-
flowchart LR
|
| 108 |
-
userClient[UserClient] --> cliLayer[CLI]
|
| 109 |
-
userClient --> apiLayer[FastAPI]
|
| 110 |
-
userClient --> streamlitUi[StreamlitUI]
|
| 111 |
-
cliLayer --> orchestrator[RAGOrchestrator]
|
| 112 |
-
apiLayer --> orchestrator
|
| 113 |
-
streamlitUi --> orchestrator
|
| 114 |
-
orchestrator --> hybridRetriever[HybridRetriever]
|
| 115 |
-
hybridRetriever --> reranker[CrossEncoderReranker]
|
| 116 |
-
reranker --> contextOptimizer[ContextOptimizer]
|
| 117 |
-
contextOptimizer --> generator[RAGGenerator]
|
| 118 |
-
generator --> citationTracker[CitationTracker]
|
| 119 |
-
citationTracker --> citationVerifier[CitationVerifier]
|
| 120 |
-
citationVerifier --> truthfulness[TruthfulnessScorer]
|
| 121 |
-
generator --> llmRouter[LLMProviderRouter]
|
| 122 |
-
llmRouter --> ollamaProvider[Ollama]
|
| 123 |
-
llmRouter --> openaiProvider[OpenAI]
|
| 124 |
-
llmRouter --> anthropicProvider[Claude]
|
| 125 |
-
llmRouter --> geminiProvider[Gemini]
|
| 126 |
-
orchestrator --> bm25Store[BM25Index]
|
| 127 |
-
orchestrator --> vectorStore[ChromaOrQdrant]
|
| 128 |
-
```
|
| 129 |
-
</details>
|
| 130 |
-
|
| 131 |
-
<details>
|
| 132 |
-
<summary>Query flow</summary>
|
| 133 |
-
|
| 134 |
-
```mermaid
|
| 135 |
-
flowchart TD
|
| 136 |
-
q[UserQuery] --> retrieve[HybridRetrieve]
|
| 137 |
-
retrieve --> fuse[RRFWeightedFusion]
|
| 138 |
-
fuse --> rerank[RerankOptional]
|
| 139 |
-
rerank --> prompt[BuildPrompt]
|
| 140 |
-
prompt --> llm[ProviderModelSelectedPerRequest]
|
| 141 |
-
llm --> cite[ExtractAndVerifyCitations]
|
| 142 |
-
cite --> truth[TruthfulnessScore]
|
| 143 |
-
truth --> response[APIorCLIorUIResponse]
|
| 144 |
-
```
|
| 145 |
-
</details>
|
| 146 |
-
|
| 147 |
-
---
|
| 148 |
-
|
| 149 |
-
## API usage
|
| 150 |
|
| 151 |
```bash
|
| 152 |
uvicorn src.api.main:app --reload --port 8000
|
|
@@ -159,7 +206,7 @@ curl -X POST http://127.0.0.1:8000/query \
|
|
| 159 |
-d '{"query": "What is hybrid retrieval?", "provider": "ollama", "model": "qwen2.5:7b"}'
|
| 160 |
```
|
| 161 |
|
| 162 |
-
Response includes a `truthfulness` block:
|
| 163 |
|
| 164 |
```json
|
| 165 |
{
|
|
@@ -170,45 +217,34 @@ Response includes a `truthfulness` block:
|
|
| 170 |
"uncited_claims": 1,
|
| 171 |
"score": 0.89
|
| 172 |
},
|
| 173 |
-
"citations": [
|
| 174 |
}
|
| 175 |
```
|
| 176 |
|
| 177 |
Endpoints: `GET /health`, `GET /metrics`, `POST /query`, `POST /query/stream` (SSE).
|
| 178 |
|
| 179 |
-
---
|
| 180 |
-
|
| 181 |
## Evaluation
|
| 182 |
|
| 183 |
-
|
| 184 |
-
|
| 185 |
-
Every `/query` response includes a `truthfulness` object with:
|
| 186 |
|
| 187 |
| Field | What it measures |
|
| 188 |
-
|-------|-----------------|
|
| 189 |
-
| `nli_faithfulness` | Fraction of response sentences entailed by
|
| 190 |
| `citation_groundedness` | Mean citation verification score |
|
| 191 |
-
| `uncited_claims` | Count of sentences without
|
| 192 |
-
| `score` | Weighted aggregate
|
| 193 |
-
|
| 194 |
-
The Streamlit UI renders a coloured badge: π’ β₯ 0.8 / π‘ 0.5β0.8 / π΄ < 0.5.
|
| 195 |
|
| 196 |
-
|
| 197 |
-
|
| 198 |
-
Run the RAGAS-style harness against the included golden dataset:
|
| 199 |
|
| 200 |
```bash
|
| 201 |
-
# Install eval extras
|
| 202 |
pip install -r requirements/eval.txt
|
| 203 |
|
| 204 |
-
# Run against full dataset (needs a running LLM)
|
| 205 |
PYTHONPATH=. python -m evals.run_evals \
|
| 206 |
-
--dataset evals/datasets/
|
| 207 |
-
--judge-provider
|
| 208 |
-
--judge-model
|
| 209 |
--output evals/reports/
|
| 210 |
|
| 211 |
-
# Smoke test (no LLM required β for CI / quick check)
|
| 212 |
PYTHONPATH=. python -m evals.run_evals \
|
| 213 |
--dataset evals/datasets/smoke.jsonl \
|
| 214 |
--mock \
|
|
@@ -216,26 +252,30 @@ PYTHONPATH=. python -m evals.run_evals \
|
|
| 216 |
--output evals/reports/
|
| 217 |
```
|
| 218 |
|
| 219 |
-
Reports are written to `evals/reports/` as JSON
|
| 220 |
|
| 221 |
-
|
| 222 |
|
| 223 |
-
|
| 224 |
-
|
| 225 |
-
|
| 226 |
-
|
| 227 |
-
|
| 228 |
-
|
| 229 |
-
|
| 230 |
-
|
| 231 |
-
|
| 232 |
-
|
| 233 |
-
| `data/sample/` | Pre-ingested sample documents for demos |
|
| 234 |
-
| `spaces/` | Hugging Face Spaces deployment files |
|
| 235 |
-
| `docker/` | Docker Compose stack (API + Streamlit + Redis + Qdrant) |
|
| 236 |
-
| `Docs/` | Roadmap, runbook, and phase documentation |
|
| 237 |
|
| 238 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 239 |
|
| 240 |
## Development
|
| 241 |
|
|
@@ -250,22 +290,13 @@ Multi-provider API key environment variables:
|
|
| 250 |
export OPENAI_API_KEY=...
|
| 251 |
export ANTHROPIC_API_KEY=...
|
| 252 |
export GEMINI_API_KEY=...
|
|
|
|
| 253 |
```
|
| 254 |
|
| 255 |
-
---
|
| 256 |
-
|
| 257 |
## Troubleshooting
|
| 258 |
|
| 259 |
- **Empty results after ingest:** Run `python -m src.ingest --docs data/documents` and verify `data/embeddings/` exists.
|
| 260 |
- **Embedding model error:** Ensure Ollama is running and `nomic-embed-text` is pulled, or switch to a different embedding provider in `config.yaml`.
|
| 261 |
- **Dimension mismatch after model change:** Re-ingest all documents to rebuild the vector index.
|
| 262 |
- **Cloud provider fails:** Check the relevant `*_API_KEY` env var is set.
|
| 263 |
-
- **Truthfulness score always 0:** The NLI model (`cross-encoder/nli-deberta-v3-small`) downloads on first use
|
| 264 |
-
|
| 265 |
-
---
|
| 266 |
-
|
| 267 |
-
## Documentation
|
| 268 |
-
|
| 269 |
-
- [Production Runbook](Docs/RUNBOOK.md)
|
| 270 |
-
- [Roadmap](Docs/ROADMAP.md)
|
| 271 |
-
- [Project overview](Docs/PROJECT_OVERVIEW.md)
|
|
|
|
| 12 |
|
| 13 |
# Doc-Ingestion
|
| 14 |
|
| 15 |
+
Doc-Ingestion is a citation-aware RAG system that turns private document collections into grounded question-answering experiences. It demonstrates how to ingest documents, retrieve the right evidence, generate answers from that evidence, and return citations plus truthfulness signals through a Streamlit app, FastAPI service, and CLI.
|
| 16 |
|
| 17 |
+
> **[Try the live demo on Hugging Face Spaces](https://huggingface.co/spaces/vampokala/doc-ingestion)** - no install required.
|
| 18 |
|
| 19 |
+
## Why This Project Exists
|
| 20 |
|
| 21 |
+
Most teams have knowledge scattered across PDFs, Word docs, markdown notes, text files, and HTML exports. Traditional search can find matching words, but it does not synthesize answers. Generic LLMs can synthesize answers, but they may not know what is inside your documents and can hallucinate without evidence.
|
|
|
|
|
|
|
|
|
|
|
|
|
| 22 |
|
| 23 |
+
This project solves that gap: ingest your documents, ask natural-language questions, and receive answers grounded in retrieved source chunks with citations and quality signals.
|
| 24 |
|
| 25 |
+
## What It Showcases
|
| 26 |
|
| 27 |
+
For non-technical reviewers, this is a working document Q&A product: load documents, ask questions, inspect answers, and verify sources.
|
| 28 |
|
| 29 |
+
For technical reviewers, this is an end-to-end RAG reference implementation with:
|
|
|
|
| 30 |
|
| 31 |
+
- Multi-format ingestion for `.pdf`, `.docx`, `.txt`, `.md`, and `.html`
|
| 32 |
+
- Token-aware chunking and persistent document indexes
|
| 33 |
+
- Hybrid retrieval using BM25 keyword search plus vector search
|
| 34 |
+
- Weighted Reciprocal Rank Fusion (RRF) across sparse and dense results
|
| 35 |
+
- Optional cross-encoder reranking for stronger final context
|
| 36 |
+
- Multi-provider LLM routing across Ollama, OpenAI, Anthropic, and Gemini
|
| 37 |
+
- Citation tracking, citation verification, and inline truthfulness scoring
|
| 38 |
+
- FastAPI, Streamlit, CLI, Docker, Redis-backed rate limiting, and offline evals
|
| 39 |
|
| 40 |
+
## Product Capabilities
|
| 41 |
|
| 42 |
+
This is the user-facing flow: documents become a searchable knowledge base, and users ask questions against that knowledge base instead of relying on ungrounded model memory.
|
| 43 |
+
|
| 44 |
+
```mermaid
|
| 45 |
+
flowchart LR
|
| 46 |
+
subgraph userLayer [User Experience]
|
| 47 |
+
upload[Upload Or Select Documents]
|
| 48 |
+
ask[Ask Natural Language Questions]
|
| 49 |
+
review[Review Answer With Citations]
|
| 50 |
+
end
|
| 51 |
+
|
| 52 |
+
subgraph knowledgeLayer [Knowledge Base]
|
| 53 |
+
ingest[Ingest Documents]
|
| 54 |
+
stored[Documents Stored And Indexed]
|
| 55 |
+
end
|
| 56 |
+
|
| 57 |
+
subgraph outcomeLayer [Business Outcome]
|
| 58 |
+
grounded[Grounded RAG Answer]
|
| 59 |
+
citations[Source Citations]
|
| 60 |
+
trust[Truthfulness Signal]
|
| 61 |
+
end
|
| 62 |
+
|
| 63 |
+
upload --> ingest --> stored
|
| 64 |
+
stored --> ask
|
| 65 |
+
ask --> grounded
|
| 66 |
+
grounded --> citations
|
| 67 |
+
grounded --> trust
|
| 68 |
+
citations --> review
|
| 69 |
+
trust --> review
|
| 70 |
```
|
| 71 |
|
| 72 |
+
## Under The Hood
|
| 73 |
|
| 74 |
+
The technical pipeline combines ingestion, sparse retrieval, semantic retrieval, rank fusion, reranking, model routing, citation verification, and answer scoring.
|
| 75 |
+
|
| 76 |
+
```mermaid
|
| 77 |
+
flowchart TB
|
| 78 |
+
subgraph ingestionLayer [Ingestion Layer]
|
| 79 |
+
direction LR
|
| 80 |
+
documents[Documents]
|
| 81 |
+
ingest[Ingest]
|
| 82 |
+
chunk[Chunk]
|
| 83 |
+
embed[Create Embeddings]
|
| 84 |
+
vectorStore[Chroma Or Qdrant]
|
| 85 |
+
keywordIndex[BM25 Keyword Index]
|
| 86 |
+
end
|
| 87 |
+
|
| 88 |
+
subgraph retrievalLayer [Retrieval Layer]
|
| 89 |
+
direction LR
|
| 90 |
+
query[User Query]
|
| 91 |
+
keyword[Keyword Retrieval]
|
| 92 |
+
semantic[Semantic Retrieval]
|
| 93 |
+
rrf[Weighted RRF Fusion]
|
| 94 |
+
rerank[Cross Encoder Rerank]
|
| 95 |
+
end
|
| 96 |
+
|
| 97 |
+
subgraph generationLayer [Generation And Trust Layer]
|
| 98 |
+
direction LR
|
| 99 |
+
context[Context Optimizer]
|
| 100 |
+
aggregator[LLM Provider Aggregator]
|
| 101 |
+
answer[Answer]
|
| 102 |
+
citations[Citation Verification]
|
| 103 |
+
truth[Truthfulness Score]
|
| 104 |
+
end
|
| 105 |
+
|
| 106 |
+
documents --> ingest
|
| 107 |
+
ingest --> chunk
|
| 108 |
+
chunk --> embed --> vectorStore
|
| 109 |
+
chunk --> keywordIndex
|
| 110 |
+
|
| 111 |
+
query --> keyword
|
| 112 |
+
query --> semantic
|
| 113 |
+
keywordIndex --> keyword
|
| 114 |
+
vectorStore --> semantic
|
| 115 |
+
keyword --> rrf
|
| 116 |
+
semantic --> rrf
|
| 117 |
+
|
| 118 |
+
rrf --> rerank --> context --> aggregator --> answer
|
| 119 |
+
answer --> citations --> truth
|
| 120 |
+
```
|
| 121 |
+
|
| 122 |
+
## How Answer Quality Is Protected
|
| 123 |
+
|
| 124 |
+
Doc-Ingestion is designed around a grounding contract: retrieve evidence first, generate from that evidence, then report how well the answer is supported.
|
| 125 |
|
| 126 |
+
- **Hybrid retrieval:** BM25 catches exact terms, acronyms, and names; vector search catches semantic matches. The results are fused with weighted RRF in [`src/core/hybrid_retriever.py`](src/core/hybrid_retriever.py).
|
| 127 |
+
- **Reranking:** A cross-encoder reranker narrows the final context before generation in [`src/core/reranker.py`](src/core/reranker.py).
|
| 128 |
+
- **Context control:** Retrieved chunks are packed into the prompt within a configured token budget in [`src/core/context_optimizer.py`](src/core/context_optimizer.py).
|
| 129 |
+
- **Provider routing:** The same query path can route to Ollama, OpenAI, Anthropic, or Gemini through [`src/core/llm_provider.py`](src/core/llm_provider.py).
|
| 130 |
+
- **Citations:** Generated citation markers are mapped back to retrieved chunks by [`src/core/citation_tracker.py`](src/core/citation_tracker.py) and verified by [`src/core/citation_verifier.py`](src/core/citation_verifier.py).
|
| 131 |
+
- **Truthfulness:** Each response can include NLI faithfulness and citation groundedness from [`src/evaluation/truthfulness.py`](src/evaluation/truthfulness.py).
|
| 132 |
+
|
| 133 |
+
## What You Can Try
|
| 134 |
+
|
| 135 |
+
- Use the hosted [Hugging Face Spaces demo](https://huggingface.co/spaces/vampokala/doc-ingestion) with preloaded sample documents.
|
| 136 |
+
- Upload or ingest your own files locally.
|
| 137 |
+
- Ask questions through Streamlit, FastAPI, or the CLI.
|
| 138 |
+
- Inspect answers, citations, source evidence, and truthfulness scores.
|
| 139 |
+
- Switch LLM providers and models per request when credentials are configured.
|
| 140 |
+
|
| 141 |
+
In hosted demo mode (`DOC_PROFILE=demo`), Streamlit executes queries in-process through the shared orchestrator so the demo is not blocked by localhost API startup races. Local non-demo mode uses the standard split architecture where Streamlit calls FastAPI over HTTP.
|
| 142 |
+
|
| 143 |
+
## Tech Stack Snapshot
|
| 144 |
+
|
| 145 |
+
- **App and API:** Streamlit, FastAPI, Pydantic, Uvicorn
|
| 146 |
+
- **Document processing:** PyPDF2, python-docx, BeautifulSoup, markdown parsing, token-aware chunking
|
| 147 |
+
- **Retrieval:** BM25, Chroma, Qdrant, sentence-transformers, Ollama embeddings
|
| 148 |
+
- **Ranking:** weighted RRF fusion, `cross-encoder/ms-marco-MiniLM-L-6-v2`
|
| 149 |
+
- **Generation:** Ollama, OpenAI, Anthropic, Gemini
|
| 150 |
+
- **Evaluation:** NLI faithfulness, citation groundedness, golden datasets, RAGAS-style offline harness
|
| 151 |
+
- **Operations:** Docker Compose, Redis-backed rate limiting with in-memory fallback, Hugging Face Spaces deployment
|
| 152 |
+
|
| 153 |
+
## Quickstart
|
| 154 |
+
|
| 155 |
+
### Try Online
|
| 156 |
+
|
| 157 |
+
Open the [Hugging Face Spaces demo](https://huggingface.co/spaces/vampokala/doc-ingestion). Sample documents about RAG, vector databases, and BM25 are preloaded. Paste your OpenAI, Anthropic, or Gemini key in the sidebar if you want to use a cloud provider.
|
| 158 |
+
|
| 159 |
+
### Run Locally With Docker
|
| 160 |
|
| 161 |
```bash
|
| 162 |
git clone https://github.com/vampokala/Doc-Ingestion
|
| 163 |
cd Doc-Ingestion
|
| 164 |
+
cp docker/.env.example docker/.env
|
| 165 |
+
# Edit docker/.env to add your API keys if needed.
|
| 166 |
+
docker compose -f docker/docker-compose.yml up
|
| 167 |
```
|
| 168 |
|
| 169 |
+
Open `http://localhost:8501` for Streamlit or `http://localhost:8000` for the API.
|
| 170 |
+
|
| 171 |
+
### Run From Source
|
| 172 |
|
| 173 |
```bash
|
| 174 |
+
git clone https://github.com/vampokala/Doc-Ingestion
|
| 175 |
+
cd Doc-Ingestion
|
| 176 |
+
bash scripts/bootstrap_demo.sh
|
| 177 |
```
|
| 178 |
|
| 179 |
+
The bootstrap script creates a virtual environment, installs dependencies, ingests sample documents, and pulls Ollama models when Ollama is installed.
|
| 180 |
|
| 181 |
```bash
|
| 182 |
source .venv/bin/activate
|
|
|
|
| 184 |
# API server
|
| 185 |
PYTHONPATH=. uvicorn src.api.main:app --reload --port 8000
|
| 186 |
|
| 187 |
+
# Streamlit UI in a second terminal
|
| 188 |
PYTHONPATH=. streamlit run src/web/streamlit_app.py
|
| 189 |
|
| 190 |
+
# CLI query
|
| 191 |
PYTHONPATH=. python -m src.query "What is RAG?"
|
| 192 |
```
|
| 193 |
|
| 194 |
+
For a full local and Docker runbook, see [`Docs/RUNBOOK.md`](Docs/RUNBOOK.md).
|
| 195 |
|
| 196 |
+
## API Usage
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 197 |
|
| 198 |
```bash
|
| 199 |
uvicorn src.api.main:app --reload --port 8000
|
|
|
|
| 206 |
-d '{"query": "What is hybrid retrieval?", "provider": "ollama", "model": "qwen2.5:7b"}'
|
| 207 |
```
|
| 208 |
|
| 209 |
+
Response includes answer text, citations, retrieved evidence, and a `truthfulness` block:
|
| 210 |
|
| 211 |
```json
|
| 212 |
{
|
|
|
|
| 217 |
"uncited_claims": 1,
|
| 218 |
"score": 0.89
|
| 219 |
},
|
| 220 |
+
"citations": []
|
| 221 |
}
|
| 222 |
```
|
| 223 |
|
| 224 |
Endpoints: `GET /health`, `GET /metrics`, `POST /query`, `POST /query/stream` (SSE).
|
| 225 |
|
|
|
|
|
|
|
| 226 |
## Evaluation
|
| 227 |
|
| 228 |
+
Every `/query` response can include a `truthfulness` object:
|
|
|
|
|
|
|
| 229 |
|
| 230 |
| Field | What it measures |
|
| 231 |
+
|-------|------------------|
|
| 232 |
+
| `nli_faithfulness` | Fraction of response sentences entailed by retrieved chunks |
|
| 233 |
| `citation_groundedness` | Mean citation verification score |
|
| 234 |
+
| `uncited_claims` | Count of answer sentences without citation markers |
|
| 235 |
+
| `score` | Weighted aggregate of faithfulness and groundedness |
|
|
|
|
|
|
|
| 236 |
|
| 237 |
+
Run the offline harness against the included datasets:
|
|
|
|
|
|
|
| 238 |
|
| 239 |
```bash
|
|
|
|
| 240 |
pip install -r requirements/eval.txt
|
| 241 |
|
|
|
|
| 242 |
PYTHONPATH=. python -m evals.run_evals \
|
| 243 |
+
--dataset evals/datasets/golden.jsonl \
|
| 244 |
+
--judge-provider anthropic \
|
| 245 |
+
--judge-model claude-haiku-4-5 \
|
| 246 |
--output evals/reports/
|
| 247 |
|
|
|
|
| 248 |
PYTHONPATH=. python -m evals.run_evals \
|
| 249 |
--dataset evals/datasets/smoke.jsonl \
|
| 250 |
--mock \
|
|
|
|
| 252 |
--output evals/reports/
|
| 253 |
```
|
| 254 |
|
| 255 |
+
Reports are written to `evals/reports/` as JSON and Markdown.
|
| 256 |
|
| 257 |
+
## Project Map
|
| 258 |
|
| 259 |
+
- [`src/core/`](src/core/) - retrieval, reranking, generation, citations, orchestration
|
| 260 |
+
- [`src/api/`](src/api/) - FastAPI models and routes
|
| 261 |
+
- [`src/web/`](src/web/) - Streamlit UI and ingestion service
|
| 262 |
+
- [`src/evaluation/`](src/evaluation/) - truthfulness scorer, generation metrics, retrieval metrics
|
| 263 |
+
- [`src/utils/`](src/utils/) - config, logging, and vector database integrations
|
| 264 |
+
- [`evals/`](evals/) - offline eval harness, golden datasets, RAGAS adapter
|
| 265 |
+
- [`data/sample/`](data/sample/) - preloaded sample documents for demos
|
| 266 |
+
- [`spaces/`](spaces/) - Hugging Face Spaces deployment files
|
| 267 |
+
- [`docker/`](docker/) - Docker Compose stack for API, Streamlit, Redis, and Qdrant
|
| 268 |
+
- [`Docs/`](Docs/) - architecture notes, runbook, roadmap, phase documentation
|
|
|
|
|
|
|
|
|
|
|
|
|
| 269 |
|
| 270 |
+
## Where To Go Deeper
|
| 271 |
+
|
| 272 |
+
- [`Docs/PROJECT_OVERVIEW.md`](Docs/PROJECT_OVERVIEW.md) - system architecture and reader-friendly project overview
|
| 273 |
+
- [`Docs/RUNBOOK.md`](Docs/RUNBOOK.md) - local setup, Docker setup, API keys, rate limiting, troubleshooting
|
| 274 |
+
- [`Docs/phase2_hybrid_retrieval.md`](Docs/phase2_hybrid_retrieval.md) - hybrid retrieval and RRF design
|
| 275 |
+
- [`Docs/phase3_reranking_generation.md`](Docs/phase3_reranking_generation.md) - reranking, generation, and context optimization
|
| 276 |
+
- [`Docs/phase4_citation_api.md`](Docs/phase4_citation_api.md) - citation and API design
|
| 277 |
+
- [`Docs/performance_baseline.md`](Docs/performance_baseline.md) - FastAPI overhead baseline
|
| 278 |
+
- [`Docs/ROADMAP.md`](Docs/ROADMAP.md) - delivery status and planned improvements
|
| 279 |
|
| 280 |
## Development
|
| 281 |
|
|
|
|
| 290 |
export OPENAI_API_KEY=...
|
| 291 |
export ANTHROPIC_API_KEY=...
|
| 292 |
export GEMINI_API_KEY=...
|
| 293 |
+
export DOC_API_KEYS=dev-key-1
|
| 294 |
```
|
| 295 |
|
|
|
|
|
|
|
| 296 |
## Troubleshooting
|
| 297 |
|
| 298 |
- **Empty results after ingest:** Run `python -m src.ingest --docs data/documents` and verify `data/embeddings/` exists.
|
| 299 |
- **Embedding model error:** Ensure Ollama is running and `nomic-embed-text` is pulled, or switch to a different embedding provider in `config.yaml`.
|
| 300 |
- **Dimension mismatch after model change:** Re-ingest all documents to rebuild the vector index.
|
| 301 |
- **Cloud provider fails:** Check the relevant `*_API_KEY` env var is set.
|
| 302 |
+
- **Truthfulness score always 0:** The NLI model (`cross-encoder/nli-deberta-v3-small`) downloads on first use. Check internet access or set `evaluation.inline_enabled: false` in `config.yaml` to disable.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
config.yaml
CHANGED
|
@@ -1,6 +1,7 @@
|
|
| 1 |
# Document processing
|
| 2 |
-
chunk_size:
|
| 3 |
-
overlap:
|
|
|
|
| 4 |
|
| 5 |
# File paths
|
| 6 |
data_dir: data/documents
|
|
|
|
| 1 |
# Document processing
|
| 2 |
+
chunk_size: 600
|
| 3 |
+
overlap: 100
|
| 4 |
+
chunk_tokenizer: gpt2
|
| 5 |
|
| 6 |
# File paths
|
| 7 |
data_dir: data/documents
|
docker/.env.example
CHANGED
|
@@ -10,7 +10,7 @@ OLLAMA_BASE_URL=http://host.docker.internal:11434
|
|
| 10 |
|
| 11 |
# Optional cloud provider keys
|
| 12 |
OPENAI_API_KEY=
|
| 13 |
-
ANTHROPIC_API_KEY=
|
| 14 |
GEMINI_API_KEY=
|
| 15 |
|
| 16 |
# Hugging Face cache/offline toggles
|
|
|
|
| 10 |
|
| 11 |
# Optional cloud provider keys
|
| 12 |
OPENAI_API_KEY=
|
| 13 |
+
ANTHROPIC_API_KEY=replace-with-anthropic-api-key
|
| 14 |
GEMINI_API_KEY=
|
| 15 |
|
| 16 |
# Hugging Face cache/offline toggles
|
evals/datasets/golden.jsonl
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{"user_input": "What is Retrieval-Augmented Generation?", "reference": "Retrieval-Augmented Generation (RAG) grounds model answers by retrieving relevant external context before generation.", "reference_contexts": ["Retrieval-Augmented Generation (RAG) enhances LLM answers by retrieving relevant context before generating a response."]}
|
| 2 |
+
{"user_input": "What are the two main phases in RAG?", "reference": "RAG has ingestion and query phases: documents are indexed first, then relevant chunks are retrieved at query time.", "reference_contexts": ["Ingestion prepares chunks and embeddings.", "Query-time retrieval fetches relevant chunks for generation."]}
|
| 3 |
+
{"user_input": "What is BM25 used for?", "reference": "BM25 is a sparse retrieval ranking function that scores lexical relevance between query terms and documents.", "reference_contexts": ["BM25 ranks documents using term frequency and document length normalization."]}
|
| 4 |
+
{"user_input": "What does BM25 k1 control?", "reference": "The k1 parameter controls how quickly BM25 term-frequency gains saturate.", "reference_contexts": ["k1 controls term frequency saturation in BM25."]}
|
| 5 |
+
{"user_input": "What is BM25 b parameter?", "reference": "The b parameter controls document length normalization strength in BM25.", "reference_contexts": ["b controls how strongly BM25 normalizes for document length."]}
|
| 6 |
+
{"user_input": "What is a common BM25 weakness?", "reference": "BM25 struggles with synonym and paraphrase matching because it depends on lexical overlap.", "reference_contexts": ["BM25 cannot reliably match semantic synonyms without shared keywords."]}
|
| 7 |
+
{"user_input": "What is dense retrieval?", "reference": "Dense retrieval compares embedding vectors to find semantically similar chunks.", "reference_contexts": ["Dense retrieval uses vector similarity between query and document embeddings."]}
|
| 8 |
+
{"user_input": "What is hybrid retrieval?", "reference": "Hybrid retrieval combines sparse BM25 and dense vector retrieval to improve robustness across query types.", "reference_contexts": ["BM25 and dense retrieval are complementary and often fused together."]}
|
| 9 |
+
{"user_input": "What is Reciprocal Rank Fusion?", "reference": "Reciprocal Rank Fusion combines rankings by summing 1/(k+rank) contributions from each retriever.", "reference_contexts": ["RRF score is computed as the sum of reciprocal rank terms across retrieval methods."]}
|
| 10 |
+
{"user_input": "Why rerank after retrieval?", "reference": "Reranking improves final top-k quality by re-scoring candidate chunks with a stronger cross-encoder model.", "reference_contexts": ["Cross-encoder reranking refines candidate ordering after initial retrieval."]}
|
| 11 |
+
{"user_input": "What is a cross-encoder reranker?", "reference": "A cross-encoder reranker jointly encodes query and chunk text to score relevance more precisely.", "reference_contexts": ["Cross-encoders evaluate query-document pairs directly for relevance."]}
|
| 12 |
+
{"user_input": "Why keep citations in RAG answers?", "reference": "Citations make answers auditable and help detect unsupported claims.", "reference_contexts": ["Citation tracking is used to verify whether claims are grounded in retrieved evidence."]}
|
| 13 |
+
{"user_input": "What does faithfulness measure?", "reference": "Faithfulness measures whether answer statements are supported by retrieved evidence.", "reference_contexts": ["Faithfulness checks if generated claims are entailed by retrieval context."]}
|
| 14 |
+
{"user_input": "What is NLI-based faithfulness?", "reference": "NLI-based faithfulness uses entailment models to estimate if answer statements are supported by context.", "reference_contexts": ["Natural Language Inference can score support versus contradiction for generated statements."]}
|
| 15 |
+
{"user_input": "What is citation groundedness?", "reference": "Citation groundedness reflects how well cited evidence supports the associated answer statements.", "reference_contexts": ["Groundedness scores estimate support strength from cited chunks."]}
|
| 16 |
+
{"user_input": "Why chunk documents before indexing?", "reference": "Chunking creates retrieval-sized units that improve recall and context precision.", "reference_contexts": ["Large documents are split into smaller chunks for better retrieval granularity."]}
|
| 17 |
+
{"user_input": "What is token-based chunking?", "reference": "Token-based chunking splits text by model token counts instead of characters.", "reference_contexts": ["Token-aware chunking aligns chunk boundaries with LLM/embedding tokenization."]}
|
| 18 |
+
{"user_input": "Why use chunk overlap?", "reference": "Overlap preserves context continuity across adjacent chunks and reduces boundary loss.", "reference_contexts": ["Chunk overlap keeps neighboring context that might otherwise be truncated."]}
|
| 19 |
+
{"user_input": "What is a vector database?", "reference": "A vector database stores embeddings and supports nearest-neighbor similarity search.", "reference_contexts": ["Vector databases index high-dimensional vectors for semantic retrieval."]}
|
| 20 |
+
{"user_input": "What is HNSW in vector search?", "reference": "HNSW is a graph-based approximate nearest-neighbor index optimized for fast high-recall search.", "reference_contexts": ["HNSW provides efficient ANN search for vector retrieval systems."]}
|
| 21 |
+
{"user_input": "How does ingestion use metadata?", "reference": "Ingestion metadata like title and file type can improve filtering, ranking, and diagnostics.", "reference_contexts": ["Metadata is attached to chunks for filtering, ranking features, and traceability."]}
|
| 22 |
+
{"user_input": "What does duplicate detection prevent?", "reference": "Duplicate detection prevents re-indexing identical content and reduces noisy retrieval redundancy.", "reference_contexts": ["Content hashing can skip duplicate documents during ingestion."]}
|
| 23 |
+
{"user_input": "Why combine BM25 and vectors instead of one?", "reference": "Combining both captures exact keywords and semantic intent, reducing each method's blind spots.", "reference_contexts": ["Sparse retrieval handles lexical precision, dense retrieval handles semantic similarity."]}
|
| 24 |
+
{"user_input": "What is context precision?", "reference": "Context precision measures how many retrieved chunks are relevant among the returned set.", "reference_contexts": ["Precision evaluates the fraction of retrieved context that is relevant."]}
|
| 25 |
+
{"user_input": "What is context recall?", "reference": "Context recall measures how much of the relevant reference context appears in retrieval results.", "reference_contexts": ["Recall evaluates whether needed supporting context was retrieved."]}
|
| 26 |
+
{"user_input": "What does answer relevancy evaluate?", "reference": "Answer relevancy evaluates semantic alignment between the user question and generated answer.", "reference_contexts": ["Relevancy checks if the answer addresses the asked question."]}
|
| 27 |
+
{"user_input": "What does ROUGE-L indicate in evals?", "reference": "ROUGE-L estimates overlap with a reference answer using longest common subsequence matching.", "reference_contexts": ["ROUGE-L is commonly used to compare generated and reference answer similarity."]}
|
| 28 |
+
{"user_input": "Why have a smoke eval dataset?", "reference": "A smoke dataset validates evaluation harness plumbing quickly with low runtime.", "reference_contexts": ["Smoke tests check that the eval pipeline executes end to end."]}
|
| 29 |
+
{"user_input": "Why maintain a golden dataset?", "reference": "A golden dataset provides stable regression coverage for retrieval and generation quality over time.", "reference_contexts": ["Golden eval sets are used to detect quality regressions across releases."]}
|
| 30 |
+
{"user_input": "Why gate CI on quality thresholds?", "reference": "Quality gates prevent merges that degrade faithfulness or answer correctness below acceptable levels.", "reference_contexts": ["CI thresholds enforce minimum evaluation quality before merge."]}
|
| 31 |
+
{"user_input": "What is an API key auth pattern in this app?", "reference": "Protected endpoints require an X-API-Key that matches configured server keys.", "reference_contexts": ["API key middleware validates incoming keys against configured allowed keys."]}
|
| 32 |
+
{"user_input": "Why add rate limiting to RAG APIs?", "reference": "Rate limiting protects service stability and controls abuse under high request bursts.", "reference_contexts": ["Rate limits are applied per client to reduce overload and misuse."]}
|
| 33 |
+
{"user_input": "What is SSE in query streaming?", "reference": "Server-Sent Events stream partial response chunks over a long-lived HTTP response.", "reference_contexts": ["SSE enables token-by-token or chunk-by-chunk response streaming."]}
|
| 34 |
+
{"user_input": "What is provider routing in generation?", "reference": "Provider routing allows selecting Ollama, OpenAI, Anthropic, or Gemini per request.", "reference_contexts": ["A provider router dispatches generation calls to configured LLM backends."]}
|
| 35 |
+
{"user_input": "Why use fallback caching?", "reference": "Fallback caching preserves responsiveness when external dependencies are slow or unavailable.", "reference_contexts": ["Response cache layers can reduce repeated generation latency."]}
|
| 36 |
+
{"user_input": "What does deterministic retrieval behavior mean?", "reference": "Deterministic retrieval returns stable rankings for identical inputs and corpus state.", "reference_contexts": ["Stable ranking behavior is important for reproducible evaluation."]}
|
| 37 |
+
{"user_input": "Why include health endpoints?", "reference": "Health endpoints expose service readiness and dependency status for operations monitoring.", "reference_contexts": ["/health endpoints are used by orchestrators and uptime checks."]}
|
| 38 |
+
{"user_input": "What is metrics endpoint purpose?", "reference": "Metrics endpoints expose counters and latency series for observability systems.", "reference_contexts": ["/metrics provides telemetry for monitoring and alerting."]}
|
| 39 |
+
{"user_input": "What is citation verification?", "reference": "Citation verification checks whether cited chunks actually support the linked answer claims.", "reference_contexts": ["Verification distinguishes supported citations from weak or unsupported ones."]}
|
| 40 |
+
{"user_input": "Why track uncited claims?", "reference": "Uncited claims indicate potential hallucination risk and lower answer trustworthiness.", "reference_contexts": ["Claims lacking citations are higher-risk for unsupported output."]}
|
| 41 |
+
{"user_input": "What is context window optimization?", "reference": "Context window optimization packs the most relevant chunks within token budget limits.", "reference_contexts": ["Retrieved chunks are selected and trimmed to fit model max context tokens."]}
|
| 42 |
+
{"user_input": "Why use configurable models?", "reference": "Configurable models let teams balance quality, cost, and latency per environment.", "reference_contexts": ["Model selection by config supports portability and controlled rollouts."]}
|
| 43 |
+
{"user_input": "Why keep cloud providers optional?", "reference": "Optional cloud providers preserve local-first operation while enabling production elasticity.", "reference_contexts": ["The system can run locally without mandatory external APIs."]}
|
| 44 |
+
{"user_input": "What does local-first RAG imply?", "reference": "Local-first RAG prioritizes local indexing/retrieval and optional external services.", "reference_contexts": ["Core retrieval can run on local infrastructure without cloud lock-in."]}
|
| 45 |
+
{"user_input": "Why test integration in addition to unit tests?", "reference": "Integration tests validate end-to-end behavior across components and contracts.", "reference_contexts": ["Unit tests check isolated logic, integration tests validate composed workflows."]}
|
| 46 |
+
{"user_input": "Why use markdown and JSON eval reports?", "reference": "JSON enables automation while markdown improves human review in CI artifacts.", "reference_contexts": ["Dual report formats support machine processing and stakeholder readability."]}
|
| 47 |
+
{"user_input": "How does CI skip missing optional secrets?", "reference": "Conditional job execution can skip provider-specific evaluations when required secrets are absent.", "reference_contexts": ["GitHub Actions job conditions can check secret presence before running."]}
|
| 48 |
+
{"user_input": "What does a faithfulness threshold represent?", "reference": "A faithfulness threshold is the minimum acceptable average support score for generated answers.", "reference_contexts": ["Thresholds define pass/fail criteria for evaluation gating."]}
|
| 49 |
+
{"user_input": "Why keep architecture simple here?", "reference": "Simple architecture reduces maintenance overhead and keeps behavior understandable and testable.", "reference_contexts": ["Avoiding unnecessary abstractions improves reliability and delivery speed."]}
|
| 50 |
+
{"user_input": "How does chunk overlap affect retrieval?", "reference": "Appropriate overlap reduces context fragmentation and can improve answer completeness.", "reference_contexts": ["Overlap ensures boundary sentences remain retrievable in adjacent chunks."]}
|
| 51 |
+
{"user_input": "What is the role of orchestrator in RAG?", "reference": "The orchestrator coordinates retrieval, reranking, context building, generation, and citations.", "reference_contexts": ["RAG orchestration composes retrieval and generation stages into one request pipeline."]}
|
| 52 |
+
{"user_input": "Why monitor evaluation latency?", "reference": "Monitoring eval latency helps control CI runtime and detect regressions in model responsiveness.", "reference_contexts": ["Latency tracking complements quality metrics in evaluation operations."]}
|
evals/datasets/golden_ci.jsonl
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{"user_input": "What is Retrieval-Augmented Generation?", "reference": "Retrieval-Augmented Generation (RAG) grounds model answers by retrieving relevant external context before generation.", "reference_contexts": ["Retrieval-Augmented Generation (RAG) enhances LLM answers by retrieving relevant context before generating a response."]}
|
| 2 |
+
{"user_input": "What are the two main phases in RAG?", "reference": "RAG has ingestion and query phases: documents are indexed first, then relevant chunks are retrieved at query time.", "reference_contexts": ["Ingestion prepares chunks and embeddings.", "Query-time retrieval fetches relevant chunks for generation."]}
|
| 3 |
+
{"user_input": "What is BM25 used for?", "reference": "BM25 is a sparse retrieval ranking function that scores lexical relevance between query terms and documents.", "reference_contexts": ["BM25 ranks documents using term frequency and document length normalization."]}
|
| 4 |
+
{"user_input": "What does BM25 k1 control?", "reference": "The k1 parameter controls how quickly BM25 term-frequency gains saturate.", "reference_contexts": ["k1 controls term frequency saturation in BM25."]}
|
| 5 |
+
{"user_input": "What is BM25 b parameter?", "reference": "The b parameter controls document length normalization strength in BM25.", "reference_contexts": ["b controls how strongly BM25 normalizes for document length."]}
|
| 6 |
+
{"user_input": "What is a common BM25 weakness?", "reference": "BM25 struggles with synonym and paraphrase matching because it depends on lexical overlap.", "reference_contexts": ["BM25 cannot reliably match semantic synonyms without shared keywords."]}
|
| 7 |
+
{"user_input": "What is dense retrieval?", "reference": "Dense retrieval compares embedding vectors to find semantically similar chunks.", "reference_contexts": ["Dense retrieval uses vector similarity between query and document embeddings."]}
|
| 8 |
+
{"user_input": "What is hybrid retrieval?", "reference": "Hybrid retrieval combines sparse BM25 and dense vector retrieval to improve robustness across query types.", "reference_contexts": ["BM25 and dense retrieval are complementary and often fused together."]}
|
| 9 |
+
{"user_input": "What is Reciprocal Rank Fusion?", "reference": "Reciprocal Rank Fusion combines rankings by summing 1/(k+rank) contributions from each retriever.", "reference_contexts": ["RRF score is computed as the sum of reciprocal rank terms across retrieval methods."]}
|
| 10 |
+
{"user_input": "Why rerank after retrieval?", "reference": "Reranking improves final top-k quality by re-scoring candidate chunks with a stronger cross-encoder model.", "reference_contexts": ["Cross-encoder reranking refines candidate ordering after initial retrieval."]}
|
| 11 |
+
{"user_input": "What is a cross-encoder reranker?", "reference": "A cross-encoder reranker jointly encodes query and chunk text to score relevance more precisely.", "reference_contexts": ["Cross-encoders evaluate query-document pairs directly for relevance."]}
|
| 12 |
+
{"user_input": "Why keep citations in RAG answers?", "reference": "Citations make answers auditable and help detect unsupported claims.", "reference_contexts": ["Citation tracking is used to verify whether claims are grounded in retrieved evidence."]}
|
| 13 |
+
{"user_input": "What does faithfulness measure?", "reference": "Faithfulness measures whether answer statements are supported by retrieved evidence.", "reference_contexts": ["Faithfulness checks if generated claims are entailed by retrieval context."]}
|
| 14 |
+
{"user_input": "What is NLI-based faithfulness?", "reference": "NLI-based faithfulness uses entailment models to estimate if answer statements are supported by context.", "reference_contexts": ["Natural Language Inference can score support versus contradiction for generated statements."]}
|
| 15 |
+
{"user_input": "What is citation groundedness?", "reference": "Citation groundedness reflects how well cited evidence supports the associated answer statements.", "reference_contexts": ["Groundedness scores estimate support strength from cited chunks."]}
|
| 16 |
+
{"user_input": "Why chunk documents before indexing?", "reference": "Chunking creates retrieval-sized units that improve recall and context precision.", "reference_contexts": ["Large documents are split into smaller chunks for better retrieval granularity."]}
|
| 17 |
+
{"user_input": "What is token-based chunking?", "reference": "Token-based chunking splits text by model token counts instead of characters.", "reference_contexts": ["Token-aware chunking aligns chunk boundaries with LLM/embedding tokenization."]}
|
| 18 |
+
{"user_input": "Why use chunk overlap?", "reference": "Overlap preserves context continuity across adjacent chunks and reduces boundary loss.", "reference_contexts": ["Chunk overlap keeps neighboring context that might otherwise be truncated."]}
|
| 19 |
+
{"user_input": "What is a vector database?", "reference": "A vector database stores embeddings and supports nearest-neighbor similarity search.", "reference_contexts": ["Vector databases index high-dimensional vectors for semantic retrieval."]}
|
| 20 |
+
{"user_input": "What is HNSW in vector search?", "reference": "HNSW is a graph-based approximate nearest-neighbor index optimized for fast high-recall search.", "reference_contexts": ["HNSW provides efficient ANN search for vector retrieval systems."]}
|
rag-spec-evaluation.md
ADDED
|
@@ -0,0 +1,96 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# RAG Spec Evaluation Report
|
| 2 |
+
|
| 3 |
+
## Context
|
| 4 |
+
|
| 5 |
+
This is a gap analysis of the Doc-Ingestion app against a production-grade RAG spec across three phases. The goal is to identify what is built, what partially meets the spec, and what is missing or misaligned.
|
| 6 |
+
|
| 7 |
+
---
|
| 8 |
+
|
| 9 |
+
## Spec vs. Implementation: Detailed Evaluation
|
| 10 |
+
|
| 11 |
+
### Phase 1 β Fundamentals
|
| 12 |
+
|
| 13 |
+
| Requirement | Status | Detail |
|
| 14 |
+
|---|---|---|
|
| 15 |
+
| Ingest documents | β
Built | PDF, DOCX, TXT, MD, HTML via `src/core/document_processor.py` |
|
| 16 |
+
| 500β800 **token** chunks | β οΈ Partial / Misaligned | Chunking uses **characters** (default: 1000 chars, 200 overlap), not tokens. Spec requires token-based chunking (500β800 tokens). No tokenizer is applied during chunking. |
|
| 17 |
+
| 100-token overlap | β οΈ Partial / Misaligned | Overlap is 200 characters, not 100 tokens. Same root issue: characters vs. tokens. |
|
| 18 |
+
| Vector store (Chroma or Qdrant) | β
Built | ChromaDB for dev (`data/embeddings/chroma`), Qdrant optional for prod. Both present in `src/utils/database.py`. |
|
| 19 |
+
|
| 20 |
+
**Gap to fix:**
|
| 21 |
+
- `src/core/document_processor.py` `chunk_text()` method uses character sliding window. Needs to be replaced with a tokenizer-aware splitter (e.g., `tiktoken` or `transformers` tokenizer) targeting 500β800 tokens with 100-token overlap.
|
| 22 |
+
- `config.yaml` `chunk_size: 1000` and `overlap: 200` need to change to token units.
|
| 23 |
+
|
| 24 |
+
---
|
| 25 |
+
|
| 26 |
+
### Phase 2 β Hybrid Retrieval + Re-ranking
|
| 27 |
+
|
| 28 |
+
| Requirement | Status | Detail |
|
| 29 |
+
|---|---|---|
|
| 30 |
+
| BM25 keyword search | β
Built | `src/core/bm25_search.py`, `BM25Search` using `rank-bm25` |
|
| 31 |
+
| Vector semantic search | β
Built | `src/core/vector_search.py`, `VectorSearch` with ChromaDB |
|
| 32 |
+
| Hybrid retrieval combining both | β
Built | `src/core/hybrid_retriever.py`, `HybridRetriever` with Reciprocal Rank Fusion (RRF), parallel execution |
|
| 33 |
+
| Cross-encoder re-ranker | β
Built | `src/core/reranker.py`, `CrossEncoderReranker` using `cross-encoder/ms-marco-MiniLM-L-6-v2` |
|
| 34 |
+
|
| 35 |
+
**Phase 2 is fully built and exceeds the spec** (adds RRF fusion, LRU caching, confidence scoring, configurable weights).
|
| 36 |
+
|
| 37 |
+
---
|
| 38 |
+
|
| 39 |
+
### Phase 3 β Evaluation Dataset + CI/CD
|
| 40 |
+
|
| 41 |
+
| Requirement | Status | Detail |
|
| 42 |
+
|---|---|---|
|
| 43 |
+
| Golden dataset of 50β200 Q&A pairs | β οΈ Missing | Only `evals/datasets/smoke.jsonl` (~few entries, 1 KB) and `evals/datasets/sample.jsonl` (~6 KB, estimated ~10β15 pairs). Neither meets the 50β200 pair threshold. |
|
| 44 |
+
| Offline evaluation script | β
Built | `evals/run_evals.py` (504 lines) with 8+ metrics: answer_relevancy, context_precision, context_recall, ROUGE-L, citation_rate, faithfulness |
|
| 45 |
+
| CI/CD pipeline integration | β οΈ Partial | `.github/workflows/ci.yml` has `evals-smoke` job, but it runs with `--mock` flag (MockPipeline). It does **not** measure real faithfulness β it tests the eval harness, not the RAG pipeline. |
|
| 46 |
+
| Measure faithfulness | β οΈ Partial | NLI faithfulness via `src/evaluation/truthfulness.py` exists. RAGAS integration exists in `evals/adapters/ragas_llm_adapter.py` but is optional (requires `ragas>=0.2`, `langchain-core` extra deps) and not wired into CI. |
|
| 47 |
+
|
| 48 |
+
---
|
| 49 |
+
|
| 50 |
+
### Recommended Tech Stack Alignment
|
| 51 |
+
|
| 52 |
+
| Recommendation | Status | Detail |
|
| 53 |
+
|---|---|---|
|
| 54 |
+
| LangChain or LangGraph | β Not used | Core pipeline uses direct HTTP API calls (`src/core/llm_provider.py`). LangChain only appears as a thin adapter in `evals/adapters/ragas_llm_adapter.py` to satisfy RAGAS interface β it is not the orchestration framework. |
|
| 55 |
+
| ChromaDB or Qdrant | β
Built | Both present |
|
| 56 |
+
| Ragas for evaluation | β οΈ Optional / Incomplete | Present as optional adapter, not enforced. CI runs MockPipeline without RAGAS. |
|
| 57 |
+
|
| 58 |
+
---
|
| 59 |
+
|
| 60 |
+
## Summary: What Is Missing
|
| 61 |
+
|
| 62 |
+
### Must-Fix (spec violations)
|
| 63 |
+
|
| 64 |
+
1. **Token-based chunking** β `src/core/document_processor.py:chunk_text()` uses character counts, not tokens. Replace with tokenizer-aware chunking (e.g., `tiktoken`) targeting 500β800 tokens, 100-token overlap. Update `config.yaml` units accordingly.
|
| 65 |
+
|
| 66 |
+
2. **Golden evaluation dataset (50β200 pairs)** β `evals/datasets/` only has smoke (~few entries) and sample (~10β15 pairs). Need to create a curated dataset of at least 50 ground-truth Q&A pairs with reference contexts, authored against real ingested documents.
|
| 67 |
+
|
| 68 |
+
3. **CI/CD runs real faithfulness evaluation** β The `evals-smoke` GitHub Actions job uses `--mock`. A CI job that runs `LivePipeline` against the golden dataset (or a representative subset) and gates on a faithfulness threshold is required by the spec.
|
| 69 |
+
|
| 70 |
+
### Should-Fix (partial alignment)
|
| 71 |
+
|
| 72 |
+
4. **RAGAS made non-optional** β RAGAS faithfulness should be a hard dependency in `evals/`, not a conditional import behind `try/except`. The eval report should always include RAGAS faithfulness score.
|
| 73 |
+
|
| 74 |
+
5. **LangChain / LangGraph adoption** β The spec recommends LangChain/LangGraph as the orchestration layer. Currently the pipeline is custom HTTP. This is a tech stack deviation, not a functional gap β worth noting but lower priority than items 1β3.
|
| 75 |
+
|
| 76 |
+
---
|
| 77 |
+
|
| 78 |
+
## What Is Already Production-Grade (exceeds spec)
|
| 79 |
+
|
| 80 |
+
- Full hybrid retrieval with RRF fusion, parallel execution, and LRU caching
|
| 81 |
+
- Cross-encoder reranking with batch scoring and threshold filtering
|
| 82 |
+
- Multi-provider LLM routing (Ollama, OpenAI, Anthropic, Gemini) with streaming
|
| 83 |
+
- NLI-based inline truthfulness scoring in the serving path
|
| 84 |
+
- Citation tracking and verification
|
| 85 |
+
- Response caching (Redis + in-memory fallback)
|
| 86 |
+
- Rate limiting, API key auth, audit logging
|
| 87 |
+
- Comprehensive IR metrics module (P@K, R@K, MRR, MAP, NDCG)
|
| 88 |
+
- Unit + integration test coverage with CI
|
| 89 |
+
|
| 90 |
+
---
|
| 91 |
+
|
| 92 |
+
## Verification Steps (after fixes)
|
| 93 |
+
|
| 94 |
+
1. After token-based chunking: ingest a known document, query it, verify chunk boundaries fall within 500β800 token range using `tiktoken` inspection script.
|
| 95 |
+
2. After golden dataset: run `python -m evals.run_evals --dataset evals/datasets/golden.jsonl --live` and confirm dataset size β₯ 50.
|
| 96 |
+
3. After CI wiring: confirm GitHub Actions `evals-golden` job fails when faithfulness drops below threshold (e.g., `nli_faithfulness < 0.7`).
|
requirements/base.txt
CHANGED
|
@@ -24,6 +24,7 @@ transformers>=4.30.0
|
|
| 24 |
torch>=2.0.0
|
| 25 |
accelerate>=0.20.0
|
| 26 |
tokenizers>=0.13.0
|
|
|
|
| 27 |
rouge-score>=0.1.2
|
| 28 |
bert-score>=0.3.13
|
| 29 |
sacrebleu>=2.3.0
|
|
|
|
| 24 |
torch>=2.0.0
|
| 25 |
accelerate>=0.20.0
|
| 26 |
tokenizers>=0.13.0
|
| 27 |
+
tiktoken>=0.5.0
|
| 28 |
rouge-score>=0.1.2
|
| 29 |
bert-score>=0.3.13
|
| 30 |
sacrebleu>=2.3.0
|
src/core/document_processor.py
CHANGED
|
@@ -12,14 +12,40 @@ from datetime import datetime
|
|
| 12 |
from typing import Dict, List, Optional
|
| 13 |
|
| 14 |
import PyPDF2
|
|
|
|
| 15 |
from bs4 import BeautifulSoup
|
| 16 |
from docx import Document
|
| 17 |
|
| 18 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 19 |
class DocumentProcessor:
|
| 20 |
-
def __init__(self, chunk_size: int =
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 21 |
self.chunk_size = chunk_size
|
| 22 |
self.overlap = overlap
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 23 |
self._seen_hashes: set = set()
|
| 24 |
|
| 25 |
def process_document(self, file_path: str) -> Optional[Dict]:
|
|
@@ -76,17 +102,24 @@ class DocumentProcessor:
|
|
| 76 |
return text.strip()
|
| 77 |
|
| 78 |
def chunk_text(self, text: str) -> List[str]:
|
| 79 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 80 |
step = self.chunk_size - self.overlap
|
| 81 |
start = 0
|
| 82 |
-
while start < len(
|
| 83 |
-
end = min(start + self.chunk_size, len(
|
| 84 |
-
chunk =
|
| 85 |
-
if chunk:
|
| 86 |
chunks.append(chunk)
|
| 87 |
start += step
|
| 88 |
return chunks
|
| 89 |
|
|
|
|
|
|
|
|
|
|
| 90 |
# --- private extractors ---
|
| 91 |
|
| 92 |
def _extract_pdf_text(self, file_path: str) -> str:
|
|
|
|
| 12 |
from typing import Dict, List, Optional
|
| 13 |
|
| 14 |
import PyPDF2
|
| 15 |
+
import tiktoken
|
| 16 |
from bs4 import BeautifulSoup
|
| 17 |
from docx import Document
|
| 18 |
|
| 19 |
|
| 20 |
+
class _RegexTokenizer:
|
| 21 |
+
"""Offline fallback tokenizer when tiktoken encoding cannot be loaded."""
|
| 22 |
+
|
| 23 |
+
_token_pattern = re.compile(r"\w+|[^\w\s]", re.UNICODE)
|
| 24 |
+
|
| 25 |
+
def encode(self, text: str) -> List[str]:
|
| 26 |
+
return self._token_pattern.findall(text)
|
| 27 |
+
|
| 28 |
+
def decode(self, token_ids: List[str]) -> str:
|
| 29 |
+
if not token_ids:
|
| 30 |
+
return ""
|
| 31 |
+
return " ".join(token_ids)
|
| 32 |
+
|
| 33 |
+
|
| 34 |
class DocumentProcessor:
|
| 35 |
+
def __init__(self, chunk_size: int = 600, overlap: int = 100, tokenizer_name: str = "gpt2"):
|
| 36 |
+
if chunk_size <= 0:
|
| 37 |
+
raise ValueError("chunk_size must be > 0")
|
| 38 |
+
if overlap < 0:
|
| 39 |
+
raise ValueError("overlap must be >= 0")
|
| 40 |
+
if overlap >= chunk_size:
|
| 41 |
+
raise ValueError("overlap must be smaller than chunk_size")
|
| 42 |
self.chunk_size = chunk_size
|
| 43 |
self.overlap = overlap
|
| 44 |
+
self.tokenizer_name = tokenizer_name
|
| 45 |
+
try:
|
| 46 |
+
self._tokenizer = tiktoken.get_encoding(tokenizer_name)
|
| 47 |
+
except Exception:
|
| 48 |
+
self._tokenizer = _RegexTokenizer()
|
| 49 |
self._seen_hashes: set = set()
|
| 50 |
|
| 51 |
def process_document(self, file_path: str) -> Optional[Dict]:
|
|
|
|
| 102 |
return text.strip()
|
| 103 |
|
| 104 |
def chunk_text(self, text: str) -> List[str]:
|
| 105 |
+
token_ids = self._tokenizer.encode(text)
|
| 106 |
+
if not token_ids:
|
| 107 |
+
return []
|
| 108 |
+
|
| 109 |
+
chunks: List[str] = []
|
| 110 |
step = self.chunk_size - self.overlap
|
| 111 |
start = 0
|
| 112 |
+
while start < len(token_ids):
|
| 113 |
+
end = min(start + self.chunk_size, len(token_ids))
|
| 114 |
+
chunk = self._tokenizer.decode(token_ids[start:end])
|
| 115 |
+
if chunk.strip():
|
| 116 |
chunks.append(chunk)
|
| 117 |
start += step
|
| 118 |
return chunks
|
| 119 |
|
| 120 |
+
def count_tokens(self, text: str) -> int:
|
| 121 |
+
return len(self._tokenizer.encode(text))
|
| 122 |
+
|
| 123 |
# --- private extractors ---
|
| 124 |
|
| 125 |
def _extract_pdf_text(self, file_path: str) -> str:
|
src/ingest.py
CHANGED
|
@@ -37,10 +37,19 @@ def collect_files(path: str) -> list[str]:
|
|
| 37 |
def ingest(docs_path: str) -> tuple[BM25Index, VectorDatabase]:
|
| 38 |
# ββ 1. Config ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 39 |
cfg = load_config("config.yaml")
|
| 40 |
-
logger.info(
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 41 |
|
| 42 |
# ββ 2. Components βββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 43 |
-
processor = DocumentProcessor(
|
|
|
|
|
|
|
|
|
|
|
|
|
| 44 |
index = BM25Index()
|
| 45 |
db = VectorDatabase(mode="dev", chroma_path="data/embeddings/chroma")
|
| 46 |
db.create_collection(COLLECTION_NAME)
|
|
|
|
| 37 |
def ingest(docs_path: str) -> tuple[BM25Index, VectorDatabase]:
|
| 38 |
# ββ 1. Config ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 39 |
cfg = load_config("config.yaml")
|
| 40 |
+
logger.info(
|
| 41 |
+
"Config loaded: chunk_size=%d overlap=%d tokenizer=%s",
|
| 42 |
+
cfg.chunk_size,
|
| 43 |
+
cfg.overlap,
|
| 44 |
+
cfg.chunk_tokenizer,
|
| 45 |
+
)
|
| 46 |
|
| 47 |
# ββ 2. Components βββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 48 |
+
processor = DocumentProcessor(
|
| 49 |
+
chunk_size=cfg.chunk_size,
|
| 50 |
+
overlap=cfg.overlap,
|
| 51 |
+
tokenizer_name=cfg.chunk_tokenizer,
|
| 52 |
+
)
|
| 53 |
index = BM25Index()
|
| 54 |
db = VectorDatabase(mode="dev", chroma_path="data/embeddings/chroma")
|
| 55 |
db.create_collection(COLLECTION_NAME)
|
src/utils/config.py
CHANGED
|
@@ -135,8 +135,9 @@ class APISettings(BaseModel):
|
|
| 135 |
|
| 136 |
|
| 137 |
class Config(BaseModel):
|
| 138 |
-
chunk_size: int = Field(
|
| 139 |
-
overlap: int = Field(
|
|
|
|
| 140 |
data_dir: str = Field("data", description="Directory for input files")
|
| 141 |
output_dir: str = Field("output", description="Directory for processed output")
|
| 142 |
log_level: str = Field("INFO", description="Logging level")
|
|
|
|
| 135 |
|
| 136 |
|
| 137 |
class Config(BaseModel):
|
| 138 |
+
chunk_size: int = Field(600, description="Chunk size in tokens")
|
| 139 |
+
overlap: int = Field(100, description="Chunk overlap in tokens")
|
| 140 |
+
chunk_tokenizer: str = Field("gpt2", description="Tokenizer encoding used for ingestion chunking")
|
| 141 |
data_dir: str = Field("data", description="Directory for input files")
|
| 142 |
output_dir: str = Field("output", description="Directory for processed output")
|
| 143 |
log_level: str = Field("INFO", description="Logging level")
|
tests/unit/test_config.py
CHANGED
|
@@ -16,8 +16,9 @@ def _write_config(data: dict) -> str:
|
|
| 16 |
class TestConfigDefaults:
|
| 17 |
def test_default_values(self):
|
| 18 |
cfg = Config()
|
| 19 |
-
assert cfg.chunk_size ==
|
| 20 |
-
assert cfg.overlap ==
|
|
|
|
| 21 |
assert cfg.log_level == "INFO"
|
| 22 |
|
| 23 |
def test_custom_values(self):
|
|
@@ -45,7 +46,7 @@ class TestLoadConfig:
|
|
| 45 |
path = _write_config({"chunk_size": 256})
|
| 46 |
try:
|
| 47 |
cfg = load_config(path)
|
| 48 |
-
assert cfg.overlap ==
|
| 49 |
finally:
|
| 50 |
os.unlink(path)
|
| 51 |
|
|
@@ -53,7 +54,7 @@ class TestLoadConfig:
|
|
| 53 |
path = _write_config({})
|
| 54 |
try:
|
| 55 |
cfg = load_config(path)
|
| 56 |
-
assert cfg.chunk_size ==
|
| 57 |
finally:
|
| 58 |
os.unlink(path)
|
| 59 |
|
|
|
|
| 16 |
class TestConfigDefaults:
|
| 17 |
def test_default_values(self):
|
| 18 |
cfg = Config()
|
| 19 |
+
assert cfg.chunk_size == 600
|
| 20 |
+
assert cfg.overlap == 100
|
| 21 |
+
assert cfg.chunk_tokenizer == "gpt2"
|
| 22 |
assert cfg.log_level == "INFO"
|
| 23 |
|
| 24 |
def test_custom_values(self):
|
|
|
|
| 46 |
path = _write_config({"chunk_size": 256})
|
| 47 |
try:
|
| 48 |
cfg = load_config(path)
|
| 49 |
+
assert cfg.overlap == 100 # default
|
| 50 |
finally:
|
| 51 |
os.unlink(path)
|
| 52 |
|
|
|
|
| 54 |
path = _write_config({})
|
| 55 |
try:
|
| 56 |
cfg = load_config(path)
|
| 57 |
+
assert cfg.chunk_size == 600
|
| 58 |
finally:
|
| 59 |
os.unlink(path)
|
| 60 |
|
tests/unit/test_document_processor.py
CHANGED
|
@@ -7,7 +7,7 @@ from src.core.document_processor import DocumentProcessor
|
|
| 7 |
|
| 8 |
@pytest.fixture
|
| 9 |
def processor():
|
| 10 |
-
return DocumentProcessor(chunk_size=
|
| 11 |
|
| 12 |
|
| 13 |
def _write_temp_file(content: str, suffix: str) -> str:
|
|
@@ -69,22 +69,23 @@ class TestChunkText:
|
|
| 69 |
assert chunks[0] == "short text"
|
| 70 |
|
| 71 |
def test_long_text_produces_multiple_chunks(self, processor):
|
| 72 |
-
text = "
|
| 73 |
chunks = processor.chunk_text(text)
|
| 74 |
assert len(chunks) > 1
|
| 75 |
|
| 76 |
def test_chunk_size_respected(self, processor):
|
| 77 |
-
text = "
|
| 78 |
chunks = processor.chunk_text(text)
|
| 79 |
-
assert all(
|
| 80 |
|
| 81 |
def test_overlap_creates_shared_content(self):
|
| 82 |
-
proc = DocumentProcessor(chunk_size=
|
| 83 |
-
text = "
|
| 84 |
chunks = proc.chunk_text(text)
|
| 85 |
-
|
| 86 |
-
|
| 87 |
-
|
|
|
|
| 88 |
|
| 89 |
def test_no_empty_chunks(self, processor):
|
| 90 |
chunks = processor.chunk_text("hello world " * 20)
|
|
|
|
| 7 |
|
| 8 |
@pytest.fixture
|
| 9 |
def processor():
|
| 10 |
+
return DocumentProcessor(chunk_size=50, overlap=10)
|
| 11 |
|
| 12 |
|
| 13 |
def _write_temp_file(content: str, suffix: str) -> str:
|
|
|
|
| 69 |
assert chunks[0] == "short text"
|
| 70 |
|
| 71 |
def test_long_text_produces_multiple_chunks(self, processor):
|
| 72 |
+
text = "token " * 300
|
| 73 |
chunks = processor.chunk_text(text)
|
| 74 |
assert len(chunks) > 1
|
| 75 |
|
| 76 |
def test_chunk_size_respected(self, processor):
|
| 77 |
+
text = "word " * 250
|
| 78 |
chunks = processor.chunk_text(text)
|
| 79 |
+
assert all(processor.count_tokens(c) <= processor.chunk_size for c in chunks)
|
| 80 |
|
| 81 |
def test_overlap_creates_shared_content(self):
|
| 82 |
+
proc = DocumentProcessor(chunk_size=20, overlap=5)
|
| 83 |
+
text = "alpha beta gamma delta epsilon " * 40
|
| 84 |
chunks = proc.chunk_text(text)
|
| 85 |
+
assert len(chunks) >= 2
|
| 86 |
+
first_tokens = proc._tokenizer.encode(chunks[0])
|
| 87 |
+
second_tokens = proc._tokenizer.encode(chunks[1])
|
| 88 |
+
assert first_tokens[-proc.overlap:] == second_tokens[:proc.overlap]
|
| 89 |
|
| 90 |
def test_no_empty_chunks(self, processor):
|
| 91 |
chunks = processor.chunk_text("hello world " * 20)
|