sdca-rag 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- sdca/__init__.py +60 -0
- sdca_rag-0.1.0.dist-info/METADATA +109 -0
- sdca_rag-0.1.0.dist-info/RECORD +35 -0
- sdca_rag-0.1.0.dist-info/WHEEL +5 -0
- sdca_rag-0.1.0.dist-info/top_level.txt +2 -0
- src/__init__.py +6 -0
- src/app/__init__.py +1 -0
- src/app/comparison.py +271 -0
- src/app/streamlit_app.py +1982 -0
- src/baselines/__init__.py +32 -0
- src/baselines/fixed.py +108 -0
- src/baselines/models.py +37 -0
- src/baselines/recursive.py +267 -0
- src/baselines/sentence.py +174 -0
- src/baselines/tokenization.py +160 -0
- src/evaluation/__init__.py +0 -0
- src/experiments/__init__.py +34 -0
- src/experiments/metrics.py +179 -0
- src/experiments/runner.py +347 -0
- src/experiments/runner_v2.py +294 -0
- src/extraction/__init__.py +17 -0
- src/extraction/models.py +100 -0
- src/extraction/pdf_parser.py +148 -0
- src/retrieval/__init__.py +10 -0
- src/retrieval/embeddings.py +142 -0
- src/retrieval/faiss_store.py +111 -0
- src/retrieval/hybrid.py +67 -0
- src/sdca/__init__.py +23 -0
- src/sdca/chunker.py +358 -0
- src/sdca/structure.py +276 -0
- src/sdca/v2/__init__.py +24 -0
- src/sdca/v2/chunker.py +308 -0
- src/sdca/v2/structure.py +297 -0
- src/sdca/v3/__init__.py +9 -0
- src/sdca/v3/chunker.py +185 -0
sdca/__init__.py
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
"""SDCA: Structure-Aware Document Chunking Algorithm for Retrieval-Augmented Generation (RAG) Systems.
|
|
2
|
+
|
|
3
|
+
Developed by Subodh Butt & Ashish Dhillon.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from src.baselines.models import Chunk
|
|
7
|
+
from src.extraction.models import Document
|
|
8
|
+
from src.extraction.pdf_parser import load_pdf
|
|
9
|
+
from src.retrieval.hybrid import BM25, rrf, structure_guided_rrf
|
|
10
|
+
from src.sdca.chunker import chunk_sdca
|
|
11
|
+
from src.sdca.v2.chunker import chunk_sdca_v2
|
|
12
|
+
from src.sdca.v3.chunker import V3Options, chunk_sdca_stair, chunk_sdca_v3
|
|
13
|
+
|
|
14
|
+
__version__ = "0.1.0"
|
|
15
|
+
__all__ = [
|
|
16
|
+
"chunk_document",
|
|
17
|
+
"load_pdf",
|
|
18
|
+
"chunk_sdca_stair",
|
|
19
|
+
"chunk_sdca_v2",
|
|
20
|
+
"chunk_sdca",
|
|
21
|
+
"structure_guided_rrf",
|
|
22
|
+
"SDCATextSplitter",
|
|
23
|
+
"Chunk",
|
|
24
|
+
"Document",
|
|
25
|
+
"V3Options",
|
|
26
|
+
]
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def chunk_document(pdf_path: str, target_tokens: int = 300, **kwargs):
|
|
30
|
+
"""High-level convenience API to load and chunk any PDF using the champion SDCA algorithm.
|
|
31
|
+
|
|
32
|
+
Args:
|
|
33
|
+
pdf_path: Path to the target PDF document.
|
|
34
|
+
target_tokens: Desired target token size per chunk (default: 300).
|
|
35
|
+
|
|
36
|
+
Returns:
|
|
37
|
+
List of layout-aware Chunk objects.
|
|
38
|
+
"""
|
|
39
|
+
doc = load_pdf(pdf_path)
|
|
40
|
+
return chunk_sdca_stair(doc, target_tokens=target_tokens, **kwargs)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class SDCATextSplitter:
|
|
44
|
+
"""Drop-in LangChain-compatible Document Transformer for SDCA."""
|
|
45
|
+
|
|
46
|
+
def __init__(self, target_tokens: int = 300, min_tokens: int = 120, max_tokens: int = 420, **kwargs):
|
|
47
|
+
self.target_tokens = target_tokens
|
|
48
|
+
self.min_tokens = min_tokens
|
|
49
|
+
self.max_tokens = max_tokens
|
|
50
|
+
self.kwargs = kwargs
|
|
51
|
+
|
|
52
|
+
def split_pdf(self, pdf_path: str):
|
|
53
|
+
"""Chunks a PDF document preserving section hierarchies and tables."""
|
|
54
|
+
return chunk_document(
|
|
55
|
+
pdf_path,
|
|
56
|
+
target_tokens=self.target_tokens,
|
|
57
|
+
min_tokens=self.min_tokens,
|
|
58
|
+
max_tokens=self.max_tokens,
|
|
59
|
+
**self.kwargs,
|
|
60
|
+
)
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: sdca-rag
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Structure-Aware Document Chunking Algorithm for Retrieval-Augmented Generation (RAG) Systems
|
|
5
|
+
Author: Subodh Butt, Ashish Dhillon
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Keywords: rag,chunking,llm,information-retrieval,document-ai,pdf-parsing,stair
|
|
8
|
+
Classifier: Development Status :: 4 - Beta
|
|
9
|
+
Classifier: Intended Audience :: Developers
|
|
10
|
+
Classifier: Intended Audience :: Science/Research
|
|
11
|
+
Classifier: Programming Language :: Python :: 3
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
16
|
+
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
|
|
17
|
+
Requires-Python: >=3.9
|
|
18
|
+
Description-Content-Type: text/markdown
|
|
19
|
+
Requires-Dist: pymupdf>=1.24.0
|
|
20
|
+
Requires-Dist: sentence-transformers>=2.2.0
|
|
21
|
+
Requires-Dist: faiss-cpu>=1.7.0
|
|
22
|
+
Requires-Dist: numpy>=1.24.0
|
|
23
|
+
Requires-Dist: pandas>=2.0.0
|
|
24
|
+
|
|
25
|
+
# Structure-Aware Document Chunking Algorithm for Retrieval-Augmented Generation Systems (SDCA)
|
|
26
|
+
|
|
27
|
+
## Overview
|
|
28
|
+
This repository implements **SDCA (Structure-Aware Document Chunking Algorithm)**, an M.Tech research project addressing chunk boundary degradation in PDF-based Retrieval-Augmented Generation (RAG) pipelines.
|
|
29
|
+
|
|
30
|
+
Standard naive chunking strategies (fixed-size windowing, simple character recursive splitting) often cleave paragraphs mid-sentence, ignore section hierarchies, and fragment semantic context, leading to degraded retrieval precision. SDCA provides a deterministic, layout-aware chunking pipeline that leverages PDF typography and document structure extracted via PyMuPDF.
|
|
31
|
+
|
|
32
|
+
---
|
|
33
|
+
|
|
34
|
+
## 5-Phase Implementation Plan
|
|
35
|
+
|
|
36
|
+
1. **Phase 1: PDF Extraction Foundation** (**COMPLETED**)
|
|
37
|
+
- Extract structured text blocks, bounding boxes, font sizes, flags, and page metadata via PyMuPDF (`fitz`).
|
|
38
|
+
2. **Phase 2: Baseline Chunking** (**COMPLETED**)
|
|
39
|
+
- Implement three standard RAG chunking baselines: Fixed-Size, Recursive, and Sentence-based Chunking.
|
|
40
|
+
3. **Phase 3: SDCA Proposed Algorithm** (**COMPLETED**)
|
|
41
|
+
- Implement the deterministic structure-aware chunking algorithm targeting ~400 tokens (range 200–600 tokens) with heading detection, paragraph preservation, and sentence-bounded splitting.
|
|
42
|
+
4. **Phase 4: Retrieval and Controlled Experiments** (**COMPLETED**)
|
|
43
|
+
- Generate embeddings using Sentence Transformers (`all-MiniLM-L6-v2`) and build FAISS vector indexes (`IndexFlatIP`) under strict *Ceteris Paribus* conditions.
|
|
44
|
+
5. **Phase 5 & 5.1: Evaluation Dashboard & Interactive Research Laboratory** (**COMPLETED**)
|
|
45
|
+
- Interactive viva research laboratory for visual inspection, multi-algorithm side-by-side chunking, multi-strategy live FAISS retrieval, and frozen official benchmark presentation.
|
|
46
|
+
6. **Phase V2: SDCA V2 Hierarchical Evolution & Controlled 5-Way Benchmark** (**COMPLETED**)
|
|
47
|
+
- Hierarchical section context (`section_path`), heading-content binding, structure-aware retrieval text (`chunk.retrieval_text`), small-chunk merging, and 5-strategy comparative benchmark.
|
|
48
|
+
|
|
49
|
+
---
|
|
50
|
+
|
|
51
|
+
## Running the SDCA Research Laboratory
|
|
52
|
+
|
|
53
|
+
Launch the interactive Streamlit research laboratory:
|
|
54
|
+
|
|
55
|
+
```bash
|
|
56
|
+
# Direct run via virtual environment
|
|
57
|
+
.venv/bin/streamlit run src/app/streamlit_app.py
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
The application opens at `http://localhost:8501` featuring 5 dedicated research views:
|
|
61
|
+
1. **Overview:** Executive product landing page with capability cards and aggregate dataset metrics.
|
|
62
|
+
2. **SDCA Explorer:** Deep visual inspection of both SDCA V1 and SDCA V2 (modal body font inference, hierarchical breadcrumbs, heading binding, and structure-aware retrieval text).
|
|
63
|
+
3. **Algorithm Comparison:** Side-by-side multi-algorithm execution (`Fixed`, `Recursive`, `Sentence`, `SDCA V1`, `SDCA V2`) on the same PDF with token statistics, distribution charts, and sequential position chunk inspection.
|
|
64
|
+
4. **Live Retrieval Lab:** Multi-algorithm semantic search under identical embedding and FAISS conditions with side-by-side cosine similarity scoring and vector text inspection.
|
|
65
|
+
5. **Official Benchmark:** Tabbed evaluation featuring the full 5-way V2 benchmark, the frozen Phase 4 baseline, and direct head-to-head SDCA V1 vs SDCA V2 evolution analysis.
|
|
66
|
+
6. **About SDCA:** Algorithmic pipeline, typography heuristics, and viva examination defense notes.
|
|
67
|
+
|
|
68
|
+
---
|
|
69
|
+
|
|
70
|
+
## Running the Automated Test Suite
|
|
71
|
+
|
|
72
|
+
Execute the full 100-test automated test suite across all project phases:
|
|
73
|
+
|
|
74
|
+
```bash
|
|
75
|
+
.venv/bin/pytest -v tests/
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
Expected result: `100 passed, 0 failures` (100% pass rate).
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
---
|
|
82
|
+
|
|
83
|
+
## Research Controls (*Ceteris Paribus*)
|
|
84
|
+
To ensure scientific validity, all retrieval benchmarks hold constant:
|
|
85
|
+
- Document corpus (3 landmark academic papers: Attention, RAG, BERT; 50 pages total)
|
|
86
|
+
- Benchmark query set & ground truth (15 verified technical queries)
|
|
87
|
+
- Embedding model (`sentence-transformers/all-MiniLM-L6-v2`, 384 dimensions)
|
|
88
|
+
- Subword tokenizer (injected model tokenizer across all 4 chunkers)
|
|
89
|
+
- Vector index algorithm (FAISS `IndexFlatIP`)
|
|
90
|
+
- Similarity metric (Exact Cosine Similarity via unit $L_2$-normalized inner product)
|
|
91
|
+
- Retrieval depth ($K = 5$)
|
|
92
|
+
- Evaluation metrics (Precision@5, Recall@5, Hit Rate@5, MRR, NDCG@5)
|
|
93
|
+
|
|
94
|
+
*The only independent variable is the chunking algorithm.*
|
|
95
|
+
|
|
96
|
+
---
|
|
97
|
+
|
|
98
|
+
## Technical Documentation
|
|
99
|
+
- [docs/SDCA_ALGORITHM.md](file:///Users/ashishdhillon/DPG/Coordinator/Mtech%20Synopsis/SDCA%20PRoject/Project/docs/SDCA_ALGORITHM.md): Algorithmic specification, pseudocode, and typography heuristics.
|
|
100
|
+
- [docs/EXPERIMENT_PROTOCOL.md](file:///Users/ashishdhillon/DPG/Coordinator/Mtech%20Synopsis/SDCA%20PRoject/Project/docs/EXPERIMENT_PROTOCOL.md): 19-point formal controlled retrieval experiment protocol.
|
|
101
|
+
- [docs/PHASE5_DASHBOARD.md](file:///Users/ashishdhillon/DPG/Coordinator/Mtech%20Synopsis/SDCA%20PRoject/Project/docs/PHASE5_DASHBOARD.md): Demonstration dashboard architecture and viva defense guide.
|
|
102
|
+
|
|
103
|
+
---
|
|
104
|
+
|
|
105
|
+
## Governance Documents
|
|
106
|
+
- [RULES.md](file:///Users/ashishdhillon/DPG/Coordinator/Mtech%20Synopsis/SDCA%20PRoject/Project/RULES.md): Research rules, algorithmic boundaries, and coding standards.
|
|
107
|
+
- [PROJECT_STATE.md](file:///Users/ashishdhillon/DPG/Coordinator/Mtech%20Synopsis/SDCA%20PRoject/Project/PROJECT_STATE.md): Current phase, deliverable tracker, and experiment states.
|
|
108
|
+
- [DECISIONS.md](file:///Users/ashishdhillon/DPG/Coordinator/Mtech%20Synopsis/SDCA%20PRoject/Project/DECISIONS.md): Architectural Decision Records (ADRs).
|
|
109
|
+
- [ERROR_CHANGE_LOG.md](file:///Users/ashishdhillon/DPG/Coordinator/Mtech%20Synopsis/SDCA%20PRoject/Project/ERROR_CHANGE_LOG.md): Historical log of errors, refactorings, and changes.
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
sdca/__init__.py,sha256=LwBzfm-iAO_e0a-em4LHabjODqVjaAyMd_jXNDx0l78,1912
|
|
2
|
+
src/__init__.py,sha256=-Ku49JrDjfoEjl4TAzSYGjbf1jfvi3UNYZ3rWjMk0w0,181
|
|
3
|
+
src/app/__init__.py,sha256=BrSE6pqmBlMYeoKKUX-_HXTAXEAF_b5aaEFv3mkwOhg,73
|
|
4
|
+
src/app/comparison.py,sha256=w5oY1ZeCvSCdnHJVuJ217s4-5uOq8Sd1IkxglXtSYiI,9753
|
|
5
|
+
src/app/streamlit_app.py,sha256=31qGuoRrvyeq8l4NZBoeIVrNj1_KM2bS7dJpIM3Dm4A,86884
|
|
6
|
+
src/baselines/__init__.py,sha256=Rp8lUdoblfKfQCPeRH_1tLQTqpqD2cGhQa3lu8ONAf4,853
|
|
7
|
+
src/baselines/fixed.py,sha256=pUcYnwLG6CLW7jCbOMdq2YHrN6CPCsdnNZW3U6XBmjc,3498
|
|
8
|
+
src/baselines/models.py,sha256=zuofdm8Du-o-6d0TrrGxbSQs8Ch_KunajsDc1QcacQM,1446
|
|
9
|
+
src/baselines/recursive.py,sha256=TNGkOLD100YO4khCzYFGNADMhIEqpUe-LlFGTHaRFnU,8906
|
|
10
|
+
src/baselines/sentence.py,sha256=Bis0lXbXB13aEjagb_GVcVD-kL3mYbyD9OlJfSvbhtM,5409
|
|
11
|
+
src/baselines/tokenization.py,sha256=akowdcyF0IdZdPmGzDaBGAAghScQi9_w7OYCZh8xT8w,4993
|
|
12
|
+
src/evaluation/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
13
|
+
src/experiments/__init__.py,sha256=I-UeScjb1aKPIRMTPfE_dOFqgMiZdpEcAwvZw4BM6x4,779
|
|
14
|
+
src/experiments/metrics.py,sha256=pu5nplIQt95JpWXMJLr2PwHu9D1AhiW_8gxN2eXZrdk,5347
|
|
15
|
+
src/experiments/runner.py,sha256=8qUO_HYHaw3Nw6CWb4wze-1dJTOzQoj9B2OnW1O65V4,14617
|
|
16
|
+
src/experiments/runner_v2.py,sha256=YXyoCidgo7HqB-bdPAduxPrUS1ClLYUGbScoCZ1e6-g,12282
|
|
17
|
+
src/extraction/__init__.py,sha256=bxa9XpuWOpV7UQpuBg6RwdsgNWSWHHI2lJwvuhOOPWI,426
|
|
18
|
+
src/extraction/models.py,sha256=BCYSqrjHgcM5WVUmtvPb519myxzuM4M-Yb-jcx9jfkc,3193
|
|
19
|
+
src/extraction/pdf_parser.py,sha256=N5tH0XRLCipOxekRmQjK--JSvglTXBE-nMDyRlS5YAA,4944
|
|
20
|
+
src/retrieval/__init__.py,sha256=9NHLzqnrpQoHixPVbPeKZlkw20EX4RjSNMXae8tdWmI,388
|
|
21
|
+
src/retrieval/embeddings.py,sha256=gsHBuDmmpt4lqWM8T3WP7teczw_Yp7UajXx5Y6JjaFw,4839
|
|
22
|
+
src/retrieval/faiss_store.py,sha256=6HMTdTMCuRlzqTbgYF4EAAWdwF5ia1v9L35jd7MyZiI,3795
|
|
23
|
+
src/retrieval/hybrid.py,sha256=nZ-H9k0J-EAmvVCgURyeKgFMSTTzC-EQlv0kUhZyACU,2363
|
|
24
|
+
src/sdca/__init__.py,sha256=TBldAdw67mn0yFBDk52ys8rGuC3UuwMVIiRCdFyhdrE,477
|
|
25
|
+
src/sdca/chunker.py,sha256=K4aoGQM90SvO0s9H-QLKJUu_whKCi2F1LRu-RZeyiKg,13300
|
|
26
|
+
src/sdca/structure.py,sha256=ItjQURIDVIeMH54RbM_EMYN2lZ9b-Lc7mwPd547dUQA,9680
|
|
27
|
+
src/sdca/v2/__init__.py,sha256=tS4kxe1l8p4CB5jrL29JCniWF7z95ciYI-e4wg6tNJQ,579
|
|
28
|
+
src/sdca/v2/chunker.py,sha256=MNI-zp-V3L3O_hvyjBG5pEYqTrEHJxBTqtuJihlHM-M,11878
|
|
29
|
+
src/sdca/v2/structure.py,sha256=8q79SyeswSRqIEU6m_U2LR4uK7njMx0TlRB9g3jXF8c,10876
|
|
30
|
+
src/sdca/v3/__init__.py,sha256=0NR5jznB6iqctUJ4s9W5WSiMRBIEFkh_OXcab_vXpuI,375
|
|
31
|
+
src/sdca/v3/chunker.py,sha256=owiMBbqep-w5Gp2ERZNfJWZw3DlGHoDhMtAV5MBfZgM,7314
|
|
32
|
+
sdca_rag-0.1.0.dist-info/METADATA,sha256=KYzpcz4bdmjfIoWDsDw6_e4Hs7rwEYDGURNwHuho-KA,6624
|
|
33
|
+
sdca_rag-0.1.0.dist-info/WHEEL,sha256=mOdXR9C3wMVov7cSY8PY-XCzo3SmmWCHvGQr5csJoao,91
|
|
34
|
+
sdca_rag-0.1.0.dist-info/top_level.txt,sha256=mvEol6p69DleC67l_uPVDipm2RSaP4Rao9PR7Tq2mHQ,9
|
|
35
|
+
sdca_rag-0.1.0.dist-info/RECORD,,
|
src/__init__.py
ADDED
src/app/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Streamlit Demonstration and Evaluation Dashboard Package for SDCA."""
|
src/app/comparison.py
ADDED
|
@@ -0,0 +1,271 @@
|
|
|
1
|
+
"""Comparison and orchestration logic for the SDCA Research Laboratory (Phase 5.1).
|
|
2
|
+
|
|
3
|
+
Coordinates side-by-side chunking and live retrieval across Fixed, Recursive,
|
|
4
|
+
Sentence, and SDCA under strict Ceteris Paribus experimental controls.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
import sys
|
|
9
|
+
from typing import Any, Dict, List, Optional, Tuple
|
|
10
|
+
|
|
11
|
+
import numpy as np
|
|
12
|
+
|
|
13
|
+
# Ensure project root is in sys.path
|
|
14
|
+
PROJECT_ROOT = Path(__file__).resolve().parent.parent.parent
|
|
15
|
+
if str(PROJECT_ROOT) not in sys.path:
|
|
16
|
+
sys.path.insert(0, str(PROJECT_ROOT))
|
|
17
|
+
|
|
18
|
+
from src.baselines.fixed import chunk_fixed
|
|
19
|
+
from src.baselines.models import Chunk
|
|
20
|
+
from src.baselines.recursive import chunk_recursive
|
|
21
|
+
from src.baselines.sentence import chunk_sentence
|
|
22
|
+
from src.extraction.models import Document
|
|
23
|
+
from src.retrieval.embeddings import EmbeddingManager
|
|
24
|
+
from src.retrieval.faiss_store import FAISSVectorStore
|
|
25
|
+
from src.retrieval.hybrid import BM25, structure_guided_rrf
|
|
26
|
+
from src.sdca.chunker import chunk_sdca
|
|
27
|
+
from src.sdca.v2.chunker import chunk_sdca_v2
|
|
28
|
+
from src.sdca.v3 import chunk_sdca_stair
|
|
29
|
+
|
|
30
|
+
AVAILABLE_STRATEGIES: List[str] = ["Fixed", "Recursive", "Sentence", "SDCA"]
|
|
31
|
+
ALL_AVAILABLE_STRATEGIES: List[str] = [
|
|
32
|
+
"Fixed",
|
|
33
|
+
"Recursive",
|
|
34
|
+
"Sentence",
|
|
35
|
+
"SDCA",
|
|
36
|
+
"SDCA V2",
|
|
37
|
+
"SDCA (STAIR)",
|
|
38
|
+
]
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def run_chunking_strategy(
|
|
42
|
+
strategy: str,
|
|
43
|
+
doc: Document,
|
|
44
|
+
tokenizer: Any,
|
|
45
|
+
target_tokens: int = 400,
|
|
46
|
+
) -> List[Chunk]:
|
|
47
|
+
"""Executes a single chunking strategy on the provided document.
|
|
48
|
+
|
|
49
|
+
Args:
|
|
50
|
+
strategy: One of 'Fixed', 'Recursive', 'Sentence', 'SDCA (V1)', 'SDCA (V2)', 'SDCA (STAIR)'.
|
|
51
|
+
doc: The extracted Document instance.
|
|
52
|
+
tokenizer: Tokenizer instance or callable.
|
|
53
|
+
target_tokens: Desired target token size (default 400).
|
|
54
|
+
|
|
55
|
+
Returns:
|
|
56
|
+
List of generated Chunk objects.
|
|
57
|
+
"""
|
|
58
|
+
if strategy in ("Fixed", "fixed"):
|
|
59
|
+
return chunk_fixed(doc, target_tokens=target_tokens, overlap_tokens=50, tokenizer=tokenizer)
|
|
60
|
+
elif strategy in ("Recursive", "recursive"):
|
|
61
|
+
return chunk_recursive(doc, target_tokens=target_tokens, tokenizer=tokenizer)
|
|
62
|
+
elif strategy in ("Sentence", "sentence"):
|
|
63
|
+
return chunk_sentence(doc, target_tokens=target_tokens, tokenizer=tokenizer)
|
|
64
|
+
elif strategy in ("SDCA", "sdca", "SDCA V1", "sdca_v1", "SDCA (V1)"):
|
|
65
|
+
return chunk_sdca(doc, target_tokens=target_tokens, min_tokens=200, max_tokens=600, tokenizer=tokenizer)
|
|
66
|
+
elif strategy in ("SDCA V2", "sdca_v2", "SDCA (V2)"):
|
|
67
|
+
return chunk_sdca_v2(doc, target_tokens=target_tokens, min_tokens=200, max_tokens=600, tokenizer=tokenizer)
|
|
68
|
+
elif strategy in ("SDCA (STAIR)", "SDCA STAIR", "sdca_stair", "STAIR"):
|
|
69
|
+
return chunk_sdca_stair(doc, target_tokens=300, min_tokens=120, max_tokens=420, tokenizer=tokenizer)
|
|
70
|
+
else:
|
|
71
|
+
raise ValueError(
|
|
72
|
+
f"Unknown chunking strategy '{strategy}'. Must be one of {ALL_AVAILABLE_STRATEGIES}"
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def run_comparison(
|
|
78
|
+
doc: Document,
|
|
79
|
+
selected_strategies: List[str],
|
|
80
|
+
tokenizer: Any,
|
|
81
|
+
target_tokens: int = 400,
|
|
82
|
+
) -> Dict[str, List[Chunk]]:
|
|
83
|
+
"""Runs the selected chunking algorithms side-by-side on the same document.
|
|
84
|
+
|
|
85
|
+
Args:
|
|
86
|
+
doc: Document to chunk.
|
|
87
|
+
selected_strategies: Subset of ['Fixed', 'Recursive', 'Sentence', 'SDCA'].
|
|
88
|
+
tokenizer: Tokenizer to use across all chunkers for token measurement invariance.
|
|
89
|
+
target_tokens: Target chunk size (default 400).
|
|
90
|
+
|
|
91
|
+
Returns:
|
|
92
|
+
Mapping of strategy name to its generated list of Chunk objects.
|
|
93
|
+
"""
|
|
94
|
+
results: Dict[str, List[Chunk]] = {}
|
|
95
|
+
for strat in selected_strategies:
|
|
96
|
+
results[strat] = run_chunking_strategy(
|
|
97
|
+
strategy=strat,
|
|
98
|
+
doc=doc,
|
|
99
|
+
tokenizer=tokenizer,
|
|
100
|
+
target_tokens=target_tokens,
|
|
101
|
+
)
|
|
102
|
+
return results
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def calculate_extended_stats(chunks: List[Chunk]) -> Dict[str, Any]:
|
|
106
|
+
"""Calculates comprehensive token and character statistics for a list of chunks.
|
|
107
|
+
|
|
108
|
+
Args:
|
|
109
|
+
chunks: List of Chunk objects.
|
|
110
|
+
|
|
111
|
+
Returns:
|
|
112
|
+
Dictionary containing Chunk Count, Mean Tokens, Median Tokens,
|
|
113
|
+
Min Tokens, Max Tokens, Std Dev, and Mean Characters.
|
|
114
|
+
"""
|
|
115
|
+
if not chunks:
|
|
116
|
+
return {
|
|
117
|
+
"Chunk Count": 0,
|
|
118
|
+
"Mean Tokens": 0.0,
|
|
119
|
+
"Median Tokens": 0.0,
|
|
120
|
+
"Min Tokens": 0,
|
|
121
|
+
"Max Tokens": 0,
|
|
122
|
+
"Std Dev": 0.0,
|
|
123
|
+
"Mean Characters": 0.0,
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
tokens = [c.token_count for c in chunks]
|
|
127
|
+
chars = [len(c.text) for c in chunks]
|
|
128
|
+
|
|
129
|
+
return {
|
|
130
|
+
"Chunk Count": len(chunks),
|
|
131
|
+
"Mean Tokens": round(float(np.mean(tokens)), 2),
|
|
132
|
+
"Median Tokens": round(float(np.median(tokens)), 2),
|
|
133
|
+
"Min Tokens": int(np.min(tokens)),
|
|
134
|
+
"Max Tokens": int(np.max(tokens)),
|
|
135
|
+
"Std Dev": round(float(np.std(tokens)), 2),
|
|
136
|
+
"Mean Characters": round(float(np.mean(chars)), 2),
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def run_retrieval_comparison(
|
|
141
|
+
doc: Document,
|
|
142
|
+
selected_strategies: List[str],
|
|
143
|
+
query: str,
|
|
144
|
+
top_k: int,
|
|
145
|
+
embedding_mgr: EmbeddingManager,
|
|
146
|
+
target_tokens: int = 400,
|
|
147
|
+
) -> Dict[str, List[Tuple[Chunk, float, int]]]:
|
|
148
|
+
"""Runs live dense retrieval across all selected chunking strategies.
|
|
149
|
+
|
|
150
|
+
Follows strict Ceteris Paribus:
|
|
151
|
+
- Same Document
|
|
152
|
+
- Same Tokenizer
|
|
153
|
+
- Same Embedding Model (MiniLM-L6-v2, 384 dimensions)
|
|
154
|
+
- Same FAISS IndexFlatIP (Cosine similarity via L2 normalization)
|
|
155
|
+
- Same Query
|
|
156
|
+
- Same Top-K
|
|
157
|
+
|
|
158
|
+
Args:
|
|
159
|
+
doc: Document instance.
|
|
160
|
+
selected_strategies: List of strategies to evaluate.
|
|
161
|
+
query: User search query.
|
|
162
|
+
top_k: Number of chunks to retrieve per strategy.
|
|
163
|
+
embedding_mgr: Cached EmbeddingManager instance.
|
|
164
|
+
target_tokens: Target chunk tokens.
|
|
165
|
+
|
|
166
|
+
Returns:
|
|
167
|
+
Mapping of strategy name to list of (chunk, cosine_similarity, rank) tuples.
|
|
168
|
+
"""
|
|
169
|
+
if not query.strip() or not selected_strategies:
|
|
170
|
+
return {}
|
|
171
|
+
|
|
172
|
+
query_vec = embedding_mgr.encode([query], normalize_embeddings=True)[0]
|
|
173
|
+
results: Dict[str, List[Tuple[Chunk, float, int]]] = {}
|
|
174
|
+
|
|
175
|
+
for strat in selected_strategies:
|
|
176
|
+
chunks = run_chunking_strategy(
|
|
177
|
+
strategy=strat,
|
|
178
|
+
doc=doc,
|
|
179
|
+
tokenizer=embedding_mgr.tokenizer,
|
|
180
|
+
target_tokens=target_tokens,
|
|
181
|
+
)
|
|
182
|
+
|
|
183
|
+
if not chunks:
|
|
184
|
+
results[strat] = []
|
|
185
|
+
continue
|
|
186
|
+
|
|
187
|
+
chunk_texts = [
|
|
188
|
+
getattr(c, "retrieval_text", None) or c.metadata.get("retrieval_text") or c.text
|
|
189
|
+
for c in chunks
|
|
190
|
+
]
|
|
191
|
+
embeddings = embedding_mgr.encode(chunk_texts, normalize_embeddings=True)
|
|
192
|
+
|
|
193
|
+
if strat in ("SDCA (STAIR)", "SDCA STAIR", "sdca_stair"):
|
|
194
|
+
# Structure-Guided Hybrid Retrieval
|
|
195
|
+
bm = BM25(chunk_texts)
|
|
196
|
+
sb = bm.scores(query)
|
|
197
|
+
sd = list(embeddings @ query_vec)
|
|
198
|
+
sec_paths = [c.metadata.get("section_path_str") or c.section or "" for c in chunks]
|
|
199
|
+
sec_emb = embedding_mgr.encode(sec_paths, normalize_embeddings=True)
|
|
200
|
+
sec_sim = list(sec_emb @ query_vec)
|
|
201
|
+
fused = structure_guided_rrf(sd, sb, sec_sim, alpha_sec=0.003)
|
|
202
|
+
order = sorted(range(len(chunks)), key=lambda i: -fused[i])
|
|
203
|
+
results[strat] = [(chunks[i], float(fused[i]), rank + 1) for rank, i in enumerate(order[:top_k])]
|
|
204
|
+
else:
|
|
205
|
+
store = FAISSVectorStore(dimension=embedding_mgr.dimension)
|
|
206
|
+
store.add_chunks(chunks, embeddings)
|
|
207
|
+
hits = store.search(query_vec, top_k=top_k)
|
|
208
|
+
results[strat] = hits
|
|
209
|
+
|
|
210
|
+
return results
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
def generate_llm_answer(
|
|
214
|
+
query: str,
|
|
215
|
+
chunks: List[Chunk],
|
|
216
|
+
api_key: Optional[str] = None,
|
|
217
|
+
model_name: str = "gemini-2.5-flash",
|
|
218
|
+
) -> Dict[str, Any]:
|
|
219
|
+
"""Generates an answer to the query grounded exclusively on the retrieved chunks.
|
|
220
|
+
|
|
221
|
+
Args:
|
|
222
|
+
query: User search query.
|
|
223
|
+
chunks: List of retrieved Chunk objects.
|
|
224
|
+
api_key: Optional Google Gemini API key. Defaults to GEMINI_API_KEY environment variable.
|
|
225
|
+
model_name: Gemini model name (default: gemini-2.5-flash).
|
|
226
|
+
|
|
227
|
+
Returns:
|
|
228
|
+
Dict with 'answer', 'model', and 'error'.
|
|
229
|
+
"""
|
|
230
|
+
import os
|
|
231
|
+
resolved_key = api_key or os.environ.get("GEMINI_API_KEY")
|
|
232
|
+
|
|
233
|
+
if not chunks:
|
|
234
|
+
return {"answer": "No context chunks were retrieved to answer this query.", "model": None, "error": None}
|
|
235
|
+
|
|
236
|
+
if not resolved_key:
|
|
237
|
+
return {
|
|
238
|
+
"answer": "To generate live answers with Google Gemini, provide an API key in the sidebar or set the GEMINI_API_KEY environment variable.",
|
|
239
|
+
"model": None,
|
|
240
|
+
"error": "MISSING_API_KEY",
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
try:
|
|
244
|
+
from google import genai
|
|
245
|
+
client = genai.Client(api_key=resolved_key)
|
|
246
|
+
context_parts = []
|
|
247
|
+
for i, c in enumerate(chunks):
|
|
248
|
+
sec = c.metadata.get("section_path_str") or c.section or "General"
|
|
249
|
+
context_parts.append(f"--- [Passage {i+1} | Section: {sec} | Page: {c.page_start}] ---\n{c.text}")
|
|
250
|
+
context_str = "\n\n".join(context_parts)
|
|
251
|
+
|
|
252
|
+
prompt = f"""You are a precise technical research assistant.
|
|
253
|
+
Answer the following question based STRICTLY and ONLY on the provided context passages below.
|
|
254
|
+
If the answer cannot be determined directly from the context, state: "The provided context does not contain sufficient information to answer this question."
|
|
255
|
+
Do not extrapolate or assume facts not explicitly stated.
|
|
256
|
+
|
|
257
|
+
Context:
|
|
258
|
+
{context_str}
|
|
259
|
+
|
|
260
|
+
Question:
|
|
261
|
+
{query}
|
|
262
|
+
|
|
263
|
+
Answer:"""
|
|
264
|
+
response = client.models.generate_content(
|
|
265
|
+
model=model_name,
|
|
266
|
+
contents=prompt,
|
|
267
|
+
)
|
|
268
|
+
return {"answer": response.text, "model": model_name, "error": None}
|
|
269
|
+
except Exception as e:
|
|
270
|
+
return {"answer": f"Error generating answer: {str(e)}", "model": model_name, "error": str(e)}
|
|
271
|
+
|