raglens-toolkit 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.
Files changed (86) hide show
  1. raglens/__init__.py +13 -0
  2. raglens/cache/__init__.py +19 -0
  3. raglens/cache/chunk_cache.py +32 -0
  4. raglens/cache/parser_cache.py +71 -0
  5. raglens/chunking/__init__.py +3 -0
  6. raglens/chunking/section_chunker.py +296 -0
  7. raglens/chunking/structure_preserver.py +121 -0
  8. raglens/cli.py +209 -0
  9. raglens/config/__init__.py +9 -0
  10. raglens/config/retrieval_config.py +37 -0
  11. raglens/embedding/__init__.py +4 -0
  12. raglens/embedding/embedding_generator.py +73 -0
  13. raglens/embedding/providers.py +42 -0
  14. raglens/evaluation/__init__.py +18 -0
  15. raglens/evaluation/benchmark_runner.py +73 -0
  16. raglens/evaluation/benchmark_visualizer.py +247 -0
  17. raglens/evaluation/hierarchical_retrieval_evaluator.py +137 -0
  18. raglens/evaluation/neighbor_hierarchical_retrieval_evaluator.py +64 -0
  19. raglens/evaluation/retrieval_evaluator.py +108 -0
  20. raglens/evaluation/retrieval_metrics.py +53 -0
  21. raglens/llm/__init__.py +4 -0
  22. raglens/llm/base.py +13 -0
  23. raglens/llm/providers.py +86 -0
  24. raglens/models/__init__.py +20 -0
  25. raglens/models/chunk.py +29 -0
  26. raglens/models/chunk_embedding.py +13 -0
  27. raglens/models/document.py +25 -0
  28. raglens/models/equation.py +13 -0
  29. raglens/models/flattened_section.py +21 -0
  30. raglens/models/hierarchical_retrieval_result.py +17 -0
  31. raglens/models/neighbor_retrieval_result.py +15 -0
  32. raglens/models/question_sample.py +22 -0
  33. raglens/models/ragas_sample.py +17 -0
  34. raglens/models/retrieval_result.py +13 -0
  35. raglens/models/section.py +32 -0
  36. raglens/models/table.py +18 -0
  37. raglens/normalization/__init__.py +3 -0
  38. raglens/normalization/section_flattener.py +66 -0
  39. raglens/parsers/__init__.py +11 -0
  40. raglens/parsers/docling_parser.py +42 -0
  41. raglens/parsers/hierarchy_builder.py +45 -0
  42. raglens/parsers/level_inference.py +28 -0
  43. raglens/parsers/markdown_section_parser.py +111 -0
  44. raglens/pipeline.py +237 -0
  45. raglens/preprocessing/__init__.py +3 -0
  46. raglens/preprocessing/formula_cleaner.py +74 -0
  47. raglens/question_generation/__init__.py +25 -0
  48. raglens/question_generation/question_cache.py +154 -0
  49. raglens/question_generation/question_dataset_builder.py +107 -0
  50. raglens/question_generation/question_dataset_loader.py +83 -0
  51. raglens/question_generation/question_generator.py +130 -0
  52. raglens/ragas/__init__.py +32 -0
  53. raglens/ragas/answer_cache.py +115 -0
  54. raglens/ragas/answer_dataset_builder.py +120 -0
  55. raglens/ragas/answer_generator.py +58 -0
  56. raglens/ragas/evaluation_dataset_builder.py +47 -0
  57. raglens/ragas/fast_context_precision.py +31 -0
  58. raglens/ragas/judge.py +75 -0
  59. raglens/ragas/ragas_dataset_loader.py +88 -0
  60. raglens/ragas/ragas_scorer.py +216 -0
  61. raglens/ragas/ragas_visualizer.py +44 -0
  62. raglens/ragas/sampling.py +35 -0
  63. raglens/retrieval/__init__.py +23 -0
  64. raglens/retrieval/base/__init__.py +3 -0
  65. raglens/retrieval/base/base_retriever.py +20 -0
  66. raglens/retrieval/bm25/__init__.py +3 -0
  67. raglens/retrieval/bm25/bm25_retriever.py +42 -0
  68. raglens/retrieval/dense/__init__.py +3 -0
  69. raglens/retrieval/dense/dense_retriever.py +118 -0
  70. raglens/retrieval/hierarchical/__init__.py +3 -0
  71. raglens/retrieval/hierarchical/hierarchical_retriever.py +97 -0
  72. raglens/retrieval/hybrid/__init__.py +3 -0
  73. raglens/retrieval/hybrid/hybrid_retriever.py +67 -0
  74. raglens/retrieval/neighbor/__init__.py +6 -0
  75. raglens/retrieval/neighbor/neighbor_heirarchical_retriever.py +92 -0
  76. raglens/retrieval/neighbor/neighbor_retriever.py +85 -0
  77. raglens/validation/__init__.py +3 -0
  78. raglens/validation/chunk_auditor.py +120 -0
  79. raglens/vectorstore/__init__.py +1 -0
  80. raglens/vectorstore/chroma_store.py +149 -0
  81. raglens_toolkit-0.1.0.dist-info/METADATA +541 -0
  82. raglens_toolkit-0.1.0.dist-info/RECORD +86 -0
  83. raglens_toolkit-0.1.0.dist-info/WHEEL +5 -0
  84. raglens_toolkit-0.1.0.dist-info/entry_points.txt +2 -0
  85. raglens_toolkit-0.1.0.dist-info/licenses/LICENSE +21 -0
  86. raglens_toolkit-0.1.0.dist-info/top_level.txt +1 -0
raglens/__init__.py ADDED
@@ -0,0 +1,13 @@
1
+ __version__ = "0.1.0"
2
+
3
+ __all__ = ["RagLensPipeline"]
4
+
5
+
6
+ def __getattr__(name):
7
+ # Lazy: raglens.pipeline pulls in Docling/transformers, which is heavy
8
+ # and irrelevant to callers that only need e.g. raglens.evaluation or
9
+ # raglens.ragas (CLI report command, the dashboard, unit tests, ...).
10
+ if name == "RagLensPipeline":
11
+ from raglens.pipeline import RagLensPipeline
12
+ return RagLensPipeline
13
+ raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
@@ -0,0 +1,19 @@
1
+ from .parser_cache import (
2
+ save_parsed_documents,
3
+ load_parsed_documents,
4
+ parsed_documents_exist
5
+ )
6
+ from .chunk_cache import (
7
+ save_chunks,
8
+ load_chunks,
9
+ chunks_exist,
10
+ )
11
+
12
+ __all__ = [
13
+ "save_parsed_documents",
14
+ "load_parsed_documents",
15
+ "parsed_documents_exist",
16
+ "save_chunks",
17
+ "load_chunks",
18
+ "chunks_exist",
19
+ ]
@@ -0,0 +1,32 @@
1
+ import os
2
+ import pickle
3
+ from pathlib import Path
4
+
5
+ _DEFAULT_CACHE_DIR = "data/cache"
6
+
7
+
8
+ def _cache_dir() -> Path:
9
+ path = Path(
10
+ os.getenv("RAGLENS_CHUNK_CACHE_DIR", _DEFAULT_CACHE_DIR)
11
+ )
12
+ path.mkdir(parents=True, exist_ok=True)
13
+ return path
14
+
15
+
16
+ def save_chunks(chunks):
17
+ path = _cache_dir() / "all_chunks.pkl"
18
+ with open(path, "wb") as f:
19
+ pickle.dump(chunks, f)
20
+ print(f"Saved -> {path}")
21
+
22
+
23
+ def load_chunks():
24
+ path = _cache_dir() / "all_chunks.pkl"
25
+ with open(path, "rb") as f:
26
+ chunks = pickle.load(f)
27
+ print(f"Loaded -> {path}")
28
+ return chunks
29
+
30
+
31
+ def chunks_exist():
32
+ return (_cache_dir() / "all_chunks.pkl").exists()
@@ -0,0 +1,71 @@
1
+ import os
2
+ import pickle
3
+ from pathlib import Path
4
+
5
+
6
+ _DEFAULT_CACHE_DIR = "data/cache"
7
+
8
+
9
+ def _cache_dir() -> Path:
10
+ path = Path(
11
+ os.getenv("RAGLENS_PARSER_CACHE_DIR", _DEFAULT_CACHE_DIR)
12
+ )
13
+ path.mkdir(parents=True, exist_ok=True)
14
+ return path
15
+
16
+
17
+ def save_parsed_documents(
18
+ parsed_documents
19
+ ):
20
+
21
+ path = (
22
+ _cache_dir()
23
+ / "parsed_documents.pkl"
24
+ )
25
+
26
+ with open(
27
+ path,
28
+ "wb"
29
+ ) as file:
30
+
31
+ pickle.dump(
32
+ parsed_documents,
33
+ file
34
+ )
35
+
36
+ print(
37
+ f"Saved -> {path}"
38
+ )
39
+
40
+
41
+ def load_parsed_documents():
42
+
43
+ path = (
44
+ _cache_dir()
45
+ / "parsed_documents.pkl"
46
+ )
47
+
48
+ with open(
49
+ path,
50
+ "rb"
51
+ ) as file:
52
+
53
+ parsed_documents = (
54
+ pickle.load(
55
+ file
56
+ )
57
+ )
58
+
59
+ print(
60
+ f"Loaded -> {path}"
61
+ )
62
+
63
+ return parsed_documents
64
+
65
+
66
+ def parsed_documents_exist():
67
+
68
+ return (
69
+ _cache_dir()
70
+ / "parsed_documents.pkl"
71
+ ).exists()
@@ -0,0 +1,3 @@
1
+ from raglens.chunking.section_chunker import (
2
+ SectionChunker
3
+ )
@@ -0,0 +1,296 @@
1
+ from langchain_text_splitters import (
2
+ RecursiveCharacterTextSplitter
3
+ )
4
+
5
+ from raglens.chunking.structure_preserver import (
6
+ StructurePreserver
7
+ )
8
+
9
+ from raglens.models import (
10
+ Chunk,
11
+ FlattenedSection,
12
+ Document
13
+ )
14
+
15
+ import re
16
+
17
+
18
+ class SectionChunker:
19
+
20
+ def __init__(
21
+ self,
22
+ chunk_size=1200,
23
+ chunk_overlap=200,
24
+ max_section_length=2500
25
+ ):
26
+
27
+ self.chunk_size = chunk_size
28
+
29
+ self.chunk_overlap = chunk_overlap
30
+
31
+ self.max_section_length = (
32
+ max_section_length
33
+ )
34
+
35
+ self.splitter = (
36
+ RecursiveCharacterTextSplitter(
37
+ chunk_size=chunk_size,
38
+ chunk_overlap=chunk_overlap
39
+ )
40
+ )
41
+
42
+ self.preserver = (
43
+ StructurePreserver()
44
+ )
45
+ self.safety_splitter = (
46
+ RecursiveCharacterTextSplitter(
47
+ chunk_size=2500,
48
+ chunk_overlap=200
49
+ )
50
+ )
51
+
52
+ self.max_embeddable_length = 5000
53
+
54
+ def chunk(
55
+ self,
56
+ document: Document,
57
+ sections: list[FlattenedSection]
58
+ ) -> list[Chunk]:
59
+
60
+ chunks = []
61
+
62
+ chunk_counter = 0
63
+
64
+ for section in sections:
65
+
66
+ # ----------------------------------
67
+ # Skip empty sections
68
+ # ----------------------------------
69
+ if len(
70
+ section.content.strip()
71
+ ) == 0:
72
+ continue
73
+
74
+ # ----------------------------------
75
+ # Small section
76
+ # ----------------------------------
77
+ if (
78
+ len(section.content)
79
+ <= self.max_section_length
80
+ ):
81
+
82
+ chunks.append(
83
+ Chunk(
84
+ chunk_id=f"{document.doc_id}"f"_{section.section_id}"f"_{chunk_counter}",
85
+ parent_doc_id=document.doc_id,
86
+ parent_section_id=section.section_id,
87
+ chunk_order=chunk_counter,
88
+ fragment_index=-1,
89
+ section_title=section.section_title,
90
+ path=section.path,
91
+ level=section.level,
92
+ chunk_type="parent_section",
93
+ content=section.content,
94
+ metadata={}
95
+ )
96
+ )
97
+
98
+ chunk_counter += 1
99
+
100
+ # ----------------------------------
101
+ # Large section
102
+ # ----------------------------------
103
+ else:
104
+
105
+ # Parent section chunk
106
+ chunks.append(
107
+ Chunk(
108
+ chunk_id=f"{document.doc_id}"f"_{section.section_id}"f"_{chunk_counter}",
109
+ parent_doc_id=document.doc_id,
110
+ parent_section_id=section.section_id,
111
+ chunk_order=chunk_counter,
112
+ fragment_index=-1,
113
+ section_title=section.section_title,
114
+ path=section.path,
115
+ level=section.level,
116
+ chunk_type="parent_section",
117
+ content=section.content,
118
+ metadata={}
119
+ )
120
+ )
121
+
122
+ chunk_counter += 1
123
+
124
+ protected_text, mapping = (
125
+ self.preserver.protect(
126
+ section.content
127
+ )
128
+ )
129
+
130
+ # ----------------------------------
131
+ # Create table fragment chunks
132
+ # directly
133
+ # ----------------------------------
134
+
135
+ table_placeholders = re.findall(
136
+ r"__TABLE_\d+_FRAGMENT_\d+__",
137
+ protected_text
138
+ )
139
+
140
+ for placeholder in (
141
+ table_placeholders
142
+ ):
143
+
144
+ table_info = (
145
+ mapping[
146
+ placeholder
147
+ ]
148
+ )
149
+
150
+ chunks.append(
151
+ Chunk(
152
+ chunk_id=f"{document.doc_id}"f"_{section.section_id}"f"_{chunk_counter}",
153
+ parent_doc_id=document.doc_id,
154
+ parent_section_id=section.section_id,
155
+ chunk_order=chunk_counter,
156
+ fragment_index=(
157
+ table_info[
158
+ "table_fragment_index"
159
+ ]
160
+ ),
161
+ section_title=section.section_title,
162
+ path=section.path,
163
+ level=section.level,
164
+ chunk_type="table_fragment",
165
+ content=(
166
+ table_info[
167
+ "content"
168
+ ]
169
+ ),
170
+ metadata={
171
+ "table_id":
172
+ table_info[
173
+ "table_id"
174
+ ],
175
+
176
+ "table_fragment_index":
177
+ table_info[
178
+ "table_fragment_index"
179
+ ],
180
+
181
+ "table_fragment_count":
182
+ table_info[
183
+ "table_fragment_count"
184
+ ]
185
+ }
186
+ )
187
+ )
188
+
189
+ chunk_counter += 1
190
+
191
+ # ----------------------------------
192
+ # Remove table placeholders
193
+ # before recursive splitting
194
+ # ----------------------------------
195
+
196
+ text_without_tables = (
197
+ protected_text
198
+ )
199
+
200
+ for placeholder in (
201
+ table_placeholders
202
+ ):
203
+
204
+ text_without_tables = (
205
+ text_without_tables.replace(
206
+ placeholder,
207
+ ""
208
+ )
209
+ )
210
+
211
+ # ----------------------------------
212
+ # Split remaining text
213
+ # ----------------------------------
214
+
215
+ fragments = (
216
+ self.splitter.split_text(
217
+ text_without_tables
218
+ )
219
+ )
220
+
221
+ for idx, fragment in enumerate(
222
+ fragments
223
+ ):
224
+
225
+ restored_fragment = (
226
+ self.preserver.restore(
227
+ fragment,
228
+ mapping
229
+ )
230
+ )
231
+
232
+ if (
233
+ len(
234
+ restored_fragment.strip()
235
+ ) == 0
236
+ ):
237
+ continue
238
+
239
+ # ----------------------------------
240
+ # Safety split for embedding limits
241
+ # ----------------------------------
242
+
243
+ if (
244
+ len(restored_fragment)
245
+ > self.max_embeddable_length
246
+ ):
247
+
248
+ safety_fragments = (
249
+ self.safety_splitter.split_text(
250
+ restored_fragment
251
+ )
252
+ )
253
+
254
+ for safety_idx, safety_fragment in enumerate(
255
+ safety_fragments
256
+ ):
257
+
258
+ chunks.append(
259
+ Chunk(
260
+ chunk_id=f"{document.doc_id}"f"_{section.section_id}"f"_{chunk_counter}",
261
+ parent_doc_id=document.doc_id,
262
+ parent_section_id=section.section_id,
263
+ chunk_order=chunk_counter,
264
+ fragment_index=safety_idx,
265
+ section_title=section.section_title,
266
+ path=section.path,
267
+ level=section.level,
268
+ chunk_type="section_fragment",
269
+ content=safety_fragment,
270
+ metadata={}
271
+ )
272
+ )
273
+
274
+ chunk_counter += 1
275
+
276
+ else:
277
+
278
+ chunks.append(
279
+ Chunk(
280
+ chunk_id=f"{document.doc_id}"f"_{section.section_id}"f"_{chunk_counter}",
281
+ parent_doc_id=document.doc_id,
282
+ parent_section_id=section.section_id,
283
+ chunk_order=chunk_counter,
284
+ fragment_index=idx,
285
+ section_title=section.section_title,
286
+ path=section.path,
287
+ level=section.level,
288
+ chunk_type="section_fragment",
289
+ content=restored_fragment,
290
+ metadata={}
291
+ )
292
+ )
293
+
294
+ chunk_counter += 1
295
+
296
+ return chunks
@@ -0,0 +1,121 @@
1
+ import re
2
+
3
+
4
+ class StructurePreserver:
5
+ MAX_ATOMIC_TABLE_LENGTH = 3000
6
+ TABLE_FRAGMENT_SIZE = 1500
7
+
8
+ def protect(self, text: str):
9
+ replacements = {}
10
+ counter = 0
11
+
12
+ # --------------------------------------------------
13
+ # Formula blocks
14
+ # --------------------------------------------------
15
+ formula_pattern = r"\$\$(.*?)\$\$"
16
+
17
+ def replace_formula(match):
18
+ nonlocal counter
19
+ key = f"__FORMULA_{counter}__"
20
+ replacements[key] = {
21
+ "content": match.group(0)
22
+ }
23
+ counter += 1
24
+ return key
25
+
26
+ text = re.sub(formula_pattern, replace_formula, text, flags=re.DOTALL)
27
+
28
+ # --------------------------------------------------
29
+ # Tables
30
+ # --------------------------------------------------
31
+ lines = text.splitlines(keepends=True)
32
+ output = []
33
+ i = 0
34
+
35
+ while i < len(lines):
36
+ line = lines[i]
37
+
38
+ # Detect markdown table start
39
+ if "|" in line and i + 1 < len(lines) and "|" in lines[i + 1]:
40
+ table_lines = []
41
+ while i < len(lines) and "|" in lines[i]:
42
+ table_lines.append(lines[i])
43
+ i += 1
44
+
45
+ table_text = "".join(table_lines)
46
+
47
+ # Small table -> atomic
48
+ if len(table_text) <= self.MAX_ATOMIC_TABLE_LENGTH:
49
+ key = f"__TABLE_{counter}__"
50
+ replacements[key] = {
51
+ "content": table_text
52
+ }
53
+ output.append(key + "\n")
54
+ counter += 1
55
+
56
+ # Large table -> split by rows
57
+ else:
58
+ table_chunks = self._split_large_table(
59
+ table_text
60
+ )
61
+ table_id = counter
62
+
63
+ for fragment_idx, chunk in enumerate(table_chunks):
64
+
65
+ key = (
66
+ f"__TABLE_{table_id}_FRAGMENT_{fragment_idx}__"
67
+ )
68
+
69
+ replacements[key] = {
70
+ "content": chunk,
71
+ "table_id": f"table_{table_id}",
72
+ "table_fragment_index": fragment_idx,
73
+ "table_fragment_count": len(table_chunks)
74
+ }
75
+
76
+ output.append(
77
+ key + "\n"
78
+ )
79
+ counter += 1
80
+ else:
81
+ output.append(line)
82
+ i += 1
83
+
84
+ text = "".join(output)
85
+ return text, replacements
86
+
87
+ def restore(self, text: str, replacements: dict):
88
+ # Replace keys with their saved content
89
+ for key, value in replacements.items():
90
+ text = text.replace(key, value["content"])
91
+ return text
92
+
93
+ def _split_large_table(self, table_text: str):
94
+ lines = table_text.strip().splitlines()
95
+ if len(lines) < 3:
96
+ return [table_text]
97
+
98
+ header = lines[0]
99
+ separator = lines[1]
100
+ rows = lines[2:]
101
+
102
+ fragments = []
103
+ current_rows = []
104
+ current_size = 0
105
+
106
+ for row in rows:
107
+ row_size = len(row)
108
+ if current_rows and current_size + row_size > self.TABLE_FRAGMENT_SIZE:
109
+ fragment = header + "\n" + separator + "\n" + "\n".join(current_rows)
110
+ fragments.append(fragment)
111
+ current_rows = []
112
+ current_size = 0
113
+
114
+ current_rows.append(row)
115
+ current_size += row_size
116
+
117
+ if current_rows:
118
+ fragment = header + "\n" + separator + "\n" + "\n".join(current_rows)
119
+ fragments.append(fragment)
120
+
121
+ return fragments