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 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,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (84.2.1)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,2 @@
1
+ sdca
2
+ src
src/__init__.py ADDED
@@ -0,0 +1,6 @@
1
+ """SDCA Research Project Source Package."""
2
+
3
+ import os
4
+
5
+ # Prevent OpenMP runtime conflict between faiss-cpu and torch on macOS
6
+ os.environ.setdefault("KMP_DUPLICATE_LIB_OK", "TRUE")
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
+