sai-Rohan commited on
Commit
53bdcd2
·
1 Parent(s): d707b13

updated explanation parser for good retrival

Browse files
db/chunker/bns_chunker.py ADDED
@@ -0,0 +1,430 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass, asdict
4
+ from typing import List, Optional
5
+
6
+
7
+ # =========================================================
8
+ # CHUNK MODEL
9
+ # =========================================================
10
+
11
+ @dataclass
12
+ class LegalChunk:
13
+
14
+ chunk_id: str
15
+
16
+ level: str
17
+
18
+ document: str
19
+
20
+ chapter_no: Optional[str]
21
+
22
+ section_no: Optional[str]
23
+
24
+ clause_no: Optional[str]
25
+
26
+ sub_clause_no: Optional[str]
27
+
28
+ roman_no: Optional[str]
29
+
30
+ title: str
31
+
32
+ text: str
33
+
34
+ enriched_text: str
35
+
36
+ parent_id: Optional[str]
37
+
38
+
39
+ # =========================================================
40
+ # CHUNKER
41
+ # =========================================================
42
+
43
+ class LegalChunker:
44
+
45
+ def __init__(
46
+ self,
47
+ document_name: str = "BNS"
48
+ ):
49
+ self.document_name = document_name
50
+
51
+ # -----------------------------------------------------
52
+ # Helpers
53
+ # -----------------------------------------------------
54
+
55
+ def build_context(
56
+ self,
57
+ title: str,
58
+ section_no: str = "",
59
+ clause_no: str = "",
60
+ sub_clause_no: str = "",
61
+ roman_no: str = ""
62
+ ) -> str:
63
+
64
+ parts = [
65
+ self.document_name
66
+ ]
67
+
68
+ if section_no:
69
+ parts.append(
70
+ f"Section {section_no}"
71
+ )
72
+
73
+ if title:
74
+ parts.append(
75
+ title
76
+ )
77
+
78
+ if clause_no:
79
+ parts.append(
80
+ f"Clause ({clause_no})"
81
+ )
82
+
83
+ if sub_clause_no:
84
+ parts.append(
85
+ f"SubClause ({sub_clause_no})"
86
+ )
87
+
88
+ if roman_no:
89
+ parts.append(
90
+ f"Roman Clause ({roman_no})"
91
+ )
92
+
93
+ return "\n".join(parts)
94
+
95
+ # -----------------------------------------------------
96
+ # Main
97
+ # -----------------------------------------------------
98
+
99
+ def chunk_document(
100
+ self,
101
+ document
102
+ ) -> List[LegalChunk]:
103
+
104
+ chunks = []
105
+
106
+ for chapter in document.chapters:
107
+
108
+ for section in chapter.sections:
109
+
110
+ section_id = (
111
+ f"BNS-{section.section_no}"
112
+ )
113
+
114
+ title = (
115
+ getattr(
116
+ section,
117
+ "title",
118
+ ""
119
+ )
120
+ )
121
+
122
+ # =================================
123
+ # SECTION CHUNK
124
+ # =================================
125
+
126
+ section_chunk = LegalChunk(
127
+ chunk_id=section_id,
128
+
129
+ level="section",
130
+
131
+ document="bns",
132
+
133
+ chapter_no=
134
+ chapter.chapter_no,
135
+
136
+ section_no=
137
+ section.section_no,
138
+
139
+ clause_no=None,
140
+
141
+ sub_clause_no=None,
142
+
143
+ roman_no=None,
144
+
145
+ title=title,
146
+
147
+ text=section.text,
148
+
149
+ enriched_text=
150
+ self.build_context(
151
+ title=title,
152
+ section_no=
153
+ section.section_no
154
+ )
155
+ + "\n\n"
156
+ + section.text,
157
+
158
+ parent_id=None
159
+ )
160
+
161
+ chunks.append(
162
+ section_chunk
163
+ )
164
+
165
+ # =================================
166
+ # CLAUSES
167
+ # =================================
168
+
169
+ for clause in section.clauses:
170
+
171
+ clause_id = (
172
+ f"{section_id}"
173
+ f"({clause.clause_no})"
174
+ )
175
+
176
+ clause_chunk = LegalChunk(
177
+
178
+ chunk_id=
179
+ clause_id,
180
+
181
+ level=
182
+ "clause",
183
+
184
+ document=
185
+ "bns",
186
+
187
+ chapter_no=
188
+ chapter.chapter_no,
189
+
190
+ section_no=
191
+ section.section_no,
192
+
193
+ clause_no=
194
+ clause.clause_no,
195
+
196
+ sub_clause_no=
197
+ None,
198
+
199
+ roman_no=
200
+ None,
201
+
202
+ title=
203
+ title,
204
+
205
+ text=
206
+ clause.text,
207
+
208
+ enriched_text=
209
+ self.build_context(
210
+ title=title,
211
+ section_no=
212
+ section.section_no,
213
+ clause_no=
214
+ clause.clause_no
215
+ )
216
+ + "\n\n"
217
+ + clause.text,
218
+
219
+ parent_id=
220
+ section_id
221
+ )
222
+
223
+ chunks.append(
224
+ clause_chunk
225
+ )
226
+
227
+ # =============================
228
+ # SUB CLAUSES
229
+ # =============================
230
+
231
+ for sub in clause.sub_clauses:
232
+
233
+ sub_id = (
234
+ f"{clause_id}"
235
+ f"({sub.sub_clause_no})"
236
+ )
237
+
238
+ sub_chunk = LegalChunk(
239
+
240
+ chunk_id=
241
+ sub_id,
242
+
243
+ level=
244
+ "subclause",
245
+
246
+ document=
247
+ "bns",
248
+
249
+ chapter_no=
250
+ chapter.chapter_no,
251
+
252
+ section_no=
253
+ section.section_no,
254
+
255
+ clause_no=
256
+ clause.clause_no,
257
+
258
+ sub_clause_no=
259
+ sub.sub_clause_no,
260
+
261
+ roman_no=
262
+ None,
263
+
264
+ title=
265
+ title,
266
+
267
+ text=
268
+ sub.text,
269
+
270
+ enriched_text=
271
+ self.build_context(
272
+ title=title,
273
+ section_no=
274
+ section.section_no,
275
+ clause_no=
276
+ clause.clause_no,
277
+ sub_clause_no=
278
+ sub.sub_clause_no
279
+ )
280
+ + "\n\n"
281
+ + sub.text,
282
+
283
+ parent_id=
284
+ clause_id
285
+ )
286
+
287
+ chunks.append(
288
+ sub_chunk
289
+ )
290
+
291
+ # =========================
292
+ # ROMAN CLAUSES
293
+ # =========================
294
+
295
+ for roman in sub.roman_clauses:
296
+
297
+ roman_id = (
298
+ f"{sub_id}"
299
+ f"({roman.roman_no})"
300
+ )
301
+
302
+ roman_chunk = LegalChunk(
303
+
304
+ chunk_id=
305
+ roman_id,
306
+
307
+ level=
308
+ "roman",
309
+
310
+ document=
311
+ "bns",
312
+
313
+ chapter_no=
314
+ chapter.chapter_no,
315
+
316
+ section_no=
317
+ section.section_no,
318
+
319
+ clause_no=
320
+ clause.clause_no,
321
+
322
+ sub_clause_no=
323
+ sub.sub_clause_no,
324
+
325
+ roman_no=
326
+ roman.roman_no,
327
+
328
+ title=
329
+ title,
330
+
331
+ text=
332
+ roman.text,
333
+
334
+ enriched_text=
335
+ self.build_context(
336
+ title=title,
337
+ section_no=
338
+ section.section_no,
339
+ clause_no=
340
+ clause.clause_no,
341
+ sub_clause_no=
342
+ sub.sub_clause_no,
343
+ roman_no=
344
+ roman.roman_no
345
+ )
346
+ + "\n\n"
347
+ + roman.text,
348
+
349
+ parent_id=
350
+ sub_id
351
+ )
352
+
353
+ chunks.append(
354
+ roman_chunk
355
+ )
356
+
357
+ # =================================
358
+ # EXPLANATIONS
359
+ # =================================
360
+
361
+ for explanation in (
362
+ section.explanations
363
+ ):
364
+
365
+ explanation_id = (
366
+ f"{section_id}"
367
+ f"-EXP-"
368
+ f"{explanation.explanation_no}"
369
+ )
370
+
371
+ chunk = LegalChunk(
372
+
373
+ chunk_id=
374
+ explanation_id,
375
+
376
+ level=
377
+ "explanation",
378
+
379
+ document=
380
+ "bns",
381
+
382
+ chapter_no=
383
+ chapter.chapter_no,
384
+
385
+ section_no=
386
+ section.section_no,
387
+
388
+ clause_no=None,
389
+
390
+ sub_clause_no=None,
391
+
392
+ roman_no=None,
393
+
394
+ title=title,
395
+
396
+ text=
397
+ explanation.text,
398
+
399
+ enriched_text=
400
+ self.build_context(
401
+ title=title,
402
+ section_no=
403
+ section.section_no
404
+ )
405
+ + "\n\n"
406
+ + explanation.text,
407
+
408
+ parent_id=
409
+ section_id
410
+ )
411
+
412
+ chunks.append(
413
+ chunk
414
+ )
415
+
416
+ return chunks
417
+
418
+
419
+ # =========================================================
420
+ # EXPORT
421
+ # =========================================================
422
+
423
+ def chunks_to_dicts(
424
+ chunks: List[LegalChunk]
425
+ ):
426
+
427
+ return [
428
+ asdict(chunk)
429
+ for chunk in chunks
430
+ ]
db/embbeder/bns_embedder.py ADDED
@@ -0,0 +1,230 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ from typing import List
4
+
5
+ from tqdm import tqdm
6
+
7
+ from sentence_transformers import (
8
+ SentenceTransformer
9
+ )
10
+
11
+ from qdrant_client import QdrantClient
12
+
13
+ from qdrant_client.models import (
14
+ Distance,
15
+ VectorParams,
16
+ PointStruct
17
+ )
18
+
19
+
20
+ class QdrantEmbeddingGenerator:
21
+
22
+ def __init__(
23
+ self,
24
+ collection_name: str = "bns",
25
+ model_name: str = "BAAI/bge-large-en-v1.5",
26
+ host: str = "localhost",
27
+ port: int = 6333
28
+ ):
29
+
30
+ self.collection_name = (
31
+ collection_name
32
+ )
33
+
34
+ self.model = (
35
+ SentenceTransformer(
36
+ model_name
37
+ )
38
+ )
39
+
40
+ self.client = (
41
+ QdrantClient(
42
+ host=host,
43
+ port=port
44
+ )
45
+ )
46
+
47
+ # =====================================
48
+ # CREATE COLLECTION
49
+ # =====================================
50
+
51
+ def create_collection(self):
52
+
53
+ dimension = (
54
+ self.model
55
+ .get_sentence_embedding_dimension()
56
+ )
57
+
58
+ collections = (
59
+ self.client
60
+ .get_collections()
61
+ )
62
+
63
+ existing = {
64
+ c.name
65
+ for c in collections.collections
66
+ }
67
+
68
+ if (
69
+ self.collection_name
70
+ not in existing
71
+ ):
72
+
73
+ self.client.create_collection(
74
+ collection_name=
75
+ self.collection_name,
76
+
77
+ vectors_config=
78
+ VectorParams(
79
+ size=dimension,
80
+ distance=
81
+ Distance.COSINE
82
+ )
83
+ )
84
+
85
+ print(
86
+ f"Created collection "
87
+ f"{self.collection_name}"
88
+ )
89
+
90
+ # =====================================
91
+ # BUILD TEXT
92
+ # =====================================
93
+
94
+ def build_text(
95
+ self,
96
+ chunk: dict
97
+ ) -> str:
98
+
99
+ return chunk.get(
100
+ "enriched_text",
101
+ chunk["text"]
102
+ )
103
+
104
+ # =====================================
105
+ # INGEST
106
+ # =====================================
107
+
108
+ def ingest_chunks(
109
+ self,
110
+ chunks: List[dict],
111
+ batch_size: int = 64
112
+ ):
113
+
114
+ self.create_collection()
115
+
116
+ point_id = 1
117
+
118
+ for start in tqdm(
119
+ range(
120
+ 0,
121
+ len(chunks),
122
+ batch_size
123
+ )
124
+ ):
125
+
126
+ batch = chunks[
127
+ start:
128
+ start + batch_size
129
+ ]
130
+
131
+ texts = [
132
+ self.build_text(
133
+ chunk
134
+ )
135
+ for chunk in batch
136
+ ]
137
+
138
+ embeddings = (
139
+ self.model.encode(
140
+ texts,
141
+ normalize_embeddings=True,
142
+ convert_to_numpy=True,
143
+ show_progress_bar=False
144
+ )
145
+ )
146
+
147
+ points = []
148
+
149
+ for chunk, embedding in zip(
150
+ batch,
151
+ embeddings
152
+ ):
153
+
154
+ payload = {
155
+
156
+ "chunk_id":
157
+ chunk.get(
158
+ "chunk_id"
159
+ ),
160
+
161
+ "level":
162
+ chunk.get(
163
+ "level"
164
+ ),
165
+
166
+ "chapter_no":
167
+ chunk.get(
168
+ "chapter_no"
169
+ ),
170
+
171
+ "section_no":
172
+ chunk.get(
173
+ "section_no"
174
+ ),
175
+
176
+ "clause_no":
177
+ chunk.get(
178
+ "clause_no"
179
+ ),
180
+
181
+ "sub_clause_no":
182
+ chunk.get(
183
+ "sub_clause_no"
184
+ ),
185
+
186
+ "roman_no":
187
+ chunk.get(
188
+ "roman_no"
189
+ ),
190
+
191
+ "title":
192
+ chunk.get(
193
+ "title"
194
+ ),
195
+
196
+ "text":
197
+ chunk.get(
198
+ "text"
199
+ ),
200
+
201
+ "parent_id":
202
+ chunk.get(
203
+ "parent_id"
204
+ )
205
+ }
206
+
207
+ points.append(
208
+ PointStruct(
209
+ id=point_id,
210
+ vector=
211
+ embedding.tolist(),
212
+ payload=
213
+ payload
214
+ )
215
+ )
216
+
217
+ point_id += 1
218
+
219
+ self.client.upsert(
220
+ collection_name=
221
+ self.collection_name,
222
+
223
+ points=
224
+ points
225
+ )
226
+
227
+ print(
228
+ f"Ingested "
229
+ f"{len(chunks)} chunks"
230
+ )
db/parsers/bns/chunk_temp.py ADDED
@@ -0,0 +1,430 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass, asdict
4
+ from typing import List, Optional
5
+
6
+
7
+ # =========================================================
8
+ # CHUNK MODEL
9
+ # =========================================================
10
+
11
+ @dataclass
12
+ class LegalChunk:
13
+
14
+ chunk_id: str
15
+
16
+ level: str
17
+
18
+ document: str
19
+
20
+ chapter_no: Optional[str]
21
+
22
+ section_no: Optional[str]
23
+
24
+ clause_no: Optional[str]
25
+
26
+ sub_clause_no: Optional[str]
27
+
28
+ roman_no: Optional[str]
29
+
30
+ title: str
31
+
32
+ text: str
33
+
34
+ enriched_text: str
35
+
36
+ parent_id: Optional[str]
37
+
38
+
39
+ # =========================================================
40
+ # CHUNKER
41
+ # =========================================================
42
+
43
+ class LegalChunker:
44
+
45
+ def __init__(
46
+ self,
47
+ document_name: str = "BNS"
48
+ ):
49
+ self.document_name = document_name
50
+
51
+ # -----------------------------------------------------
52
+ # Helpers
53
+ # -----------------------------------------------------
54
+
55
+ def build_context(
56
+ self,
57
+ title: str,
58
+ section_no: str = "",
59
+ clause_no: str = "",
60
+ sub_clause_no: str = "",
61
+ roman_no: str = ""
62
+ ) -> str:
63
+
64
+ parts = [
65
+ self.document_name
66
+ ]
67
+
68
+ if section_no:
69
+ parts.append(
70
+ f"Section {section_no}"
71
+ )
72
+
73
+ if title:
74
+ parts.append(
75
+ title
76
+ )
77
+
78
+ if clause_no:
79
+ parts.append(
80
+ f"Clause ({clause_no})"
81
+ )
82
+
83
+ if sub_clause_no:
84
+ parts.append(
85
+ f"SubClause ({sub_clause_no})"
86
+ )
87
+
88
+ if roman_no:
89
+ parts.append(
90
+ f"Roman Clause ({roman_no})"
91
+ )
92
+
93
+ return "\n".join(parts)
94
+
95
+ # -----------------------------------------------------
96
+ # Main
97
+ # -----------------------------------------------------
98
+
99
+ def chunk_document(
100
+ self,
101
+ document
102
+ ) -> List[LegalChunk]:
103
+
104
+ chunks = []
105
+
106
+ for chapter in document.chapters:
107
+
108
+ for section in chapter.sections:
109
+
110
+ section_id = (
111
+ f"BNS-{section.section_no}"
112
+ )
113
+
114
+ title = (
115
+ getattr(
116
+ section,
117
+ "title",
118
+ ""
119
+ )
120
+ )
121
+
122
+ # =================================
123
+ # SECTION CHUNK
124
+ # =================================
125
+
126
+ section_chunk = LegalChunk(
127
+ chunk_id=section_id,
128
+
129
+ level="section",
130
+
131
+ document="bns",
132
+
133
+ chapter_no=
134
+ chapter.chapter_no,
135
+
136
+ section_no=
137
+ section.section_no,
138
+
139
+ clause_no=None,
140
+
141
+ sub_clause_no=None,
142
+
143
+ roman_no=None,
144
+
145
+ title=title,
146
+
147
+ text=section.text,
148
+
149
+ enriched_text=
150
+ self.build_context(
151
+ title=title,
152
+ section_no=
153
+ section.section_no
154
+ )
155
+ + "\n\n"
156
+ + section.text,
157
+
158
+ parent_id=None
159
+ )
160
+
161
+ chunks.append(
162
+ section_chunk
163
+ )
164
+
165
+ # =================================
166
+ # CLAUSES
167
+ # =================================
168
+
169
+ for clause in section.clauses:
170
+
171
+ clause_id = (
172
+ f"{section_id}"
173
+ f"({clause.clause_no})"
174
+ )
175
+
176
+ clause_chunk = LegalChunk(
177
+
178
+ chunk_id=
179
+ clause_id,
180
+
181
+ level=
182
+ "clause",
183
+
184
+ document=
185
+ "bns",
186
+
187
+ chapter_no=
188
+ chapter.chapter_no,
189
+
190
+ section_no=
191
+ section.section_no,
192
+
193
+ clause_no=
194
+ clause.clause_no,
195
+
196
+ sub_clause_no=
197
+ None,
198
+
199
+ roman_no=
200
+ None,
201
+
202
+ title=
203
+ title,
204
+
205
+ text=
206
+ clause.text,
207
+
208
+ enriched_text=
209
+ self.build_context(
210
+ title=title,
211
+ section_no=
212
+ section.section_no,
213
+ clause_no=
214
+ clause.clause_no
215
+ )
216
+ + "\n\n"
217
+ + clause.text,
218
+
219
+ parent_id=
220
+ section_id
221
+ )
222
+
223
+ chunks.append(
224
+ clause_chunk
225
+ )
226
+
227
+ # =============================
228
+ # SUB CLAUSES
229
+ # =============================
230
+
231
+ for sub in clause.sub_clauses:
232
+
233
+ sub_id = (
234
+ f"{clause_id}"
235
+ f"({sub.sub_clause_no})"
236
+ )
237
+
238
+ sub_chunk = LegalChunk(
239
+
240
+ chunk_id=
241
+ sub_id,
242
+
243
+ level=
244
+ "subclause",
245
+
246
+ document=
247
+ "bns",
248
+
249
+ chapter_no=
250
+ chapter.chapter_no,
251
+
252
+ section_no=
253
+ section.section_no,
254
+
255
+ clause_no=
256
+ clause.clause_no,
257
+
258
+ sub_clause_no=
259
+ sub.sub_clause_no,
260
+
261
+ roman_no=
262
+ None,
263
+
264
+ title=
265
+ title,
266
+
267
+ text=
268
+ sub.text,
269
+
270
+ enriched_text=
271
+ self.build_context(
272
+ title=title,
273
+ section_no=
274
+ section.section_no,
275
+ clause_no=
276
+ clause.clause_no,
277
+ sub_clause_no=
278
+ sub.sub_clause_no
279
+ )
280
+ + "\n\n"
281
+ + sub.text,
282
+
283
+ parent_id=
284
+ clause_id
285
+ )
286
+
287
+ chunks.append(
288
+ sub_chunk
289
+ )
290
+
291
+ # =========================
292
+ # ROMAN CLAUSES
293
+ # =========================
294
+
295
+ for roman in sub.roman_clauses:
296
+
297
+ roman_id = (
298
+ f"{sub_id}"
299
+ f"({roman.roman_no})"
300
+ )
301
+
302
+ roman_chunk = LegalChunk(
303
+
304
+ chunk_id=
305
+ roman_id,
306
+
307
+ level=
308
+ "roman",
309
+
310
+ document=
311
+ "bns",
312
+
313
+ chapter_no=
314
+ chapter.chapter_no,
315
+
316
+ section_no=
317
+ section.section_no,
318
+
319
+ clause_no=
320
+ clause.clause_no,
321
+
322
+ sub_clause_no=
323
+ sub.sub_clause_no,
324
+
325
+ roman_no=
326
+ roman.roman_no,
327
+
328
+ title=
329
+ title,
330
+
331
+ text=
332
+ roman.text,
333
+
334
+ enriched_text=
335
+ self.build_context(
336
+ title=title,
337
+ section_no=
338
+ section.section_no,
339
+ clause_no=
340
+ clause.clause_no,
341
+ sub_clause_no=
342
+ sub.sub_clause_no,
343
+ roman_no=
344
+ roman.roman_no
345
+ )
346
+ + "\n\n"
347
+ + roman.text,
348
+
349
+ parent_id=
350
+ sub_id
351
+ )
352
+
353
+ chunks.append(
354
+ roman_chunk
355
+ )
356
+
357
+ # =================================
358
+ # EXPLANATIONS
359
+ # =================================
360
+
361
+ for explanation in (
362
+ section.explanations
363
+ ):
364
+
365
+ explanation_id = (
366
+ f"{section_id}"
367
+ f"-EXP-"
368
+ f"{explanation.explanation_no}"
369
+ )
370
+
371
+ chunk = LegalChunk(
372
+
373
+ chunk_id=
374
+ explanation_id,
375
+
376
+ level=
377
+ "explanation",
378
+
379
+ document=
380
+ "bns",
381
+
382
+ chapter_no=
383
+ chapter.chapter_no,
384
+
385
+ section_no=
386
+ section.section_no,
387
+
388
+ clause_no=None,
389
+
390
+ sub_clause_no=None,
391
+
392
+ roman_no=None,
393
+
394
+ title=title,
395
+
396
+ text=
397
+ explanation.text,
398
+
399
+ enriched_text=
400
+ self.build_context(
401
+ title=title,
402
+ section_no=
403
+ section.section_no
404
+ )
405
+ + "\n\n"
406
+ + explanation.text,
407
+
408
+ parent_id=
409
+ section_id
410
+ )
411
+
412
+ chunks.append(
413
+ chunk
414
+ )
415
+
416
+ return chunks
417
+
418
+
419
+ # =========================================================
420
+ # EXPORT
421
+ # =========================================================
422
+
423
+ def chunks_to_dicts(
424
+ chunks: List[LegalChunk]
425
+ ):
426
+
427
+ return [
428
+ asdict(chunk)
429
+ for chunk in chunks
430
+ ]
db/parsers/bns/embedder_temp.py ADDED
@@ -0,0 +1,29 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from sentence_transformers import (
2
+ SentenceTransformer
3
+ )
4
+
5
+
6
+ class LegalEmbedder:
7
+
8
+ def __init__(
9
+ self,
10
+ model_name: str =
11
+ "BAAI/bge-large-en-v1.5"
12
+ ):
13
+
14
+ self.model = (
15
+ SentenceTransformer(
16
+ model_name
17
+ )
18
+ )
19
+
20
+ def embed(
21
+ self,
22
+ texts: list[str]
23
+ ):
24
+
25
+ return self.model.encode(
26
+ texts,
27
+ normalize_embeddings=True,
28
+ convert_to_numpy=True
29
+ )
db/parsers/bns/explanation_parser.py CHANGED
@@ -30,6 +30,22 @@ class ExplanationParser:
30
  EXPLANATION_RE = re.compile(
31
  r"(?im)^Explanation\s*([0-9IVXLCDM]*)\s*[\.\-—:]"
32
  )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
33
 
34
  # =====================================================
35
  # ROMAN NUMERAL NORMALIZATION
@@ -95,14 +111,35 @@ class ExplanationParser:
95
 
96
  start = match.start()
97
 
98
- end = (
99
- matches[i + 1].start()
100
- if i + 1 < len(matches)
101
- else len(text)
102
- )
 
 
 
 
 
 
103
 
104
- explanation_no = self.normalize_explanation_no(
105
- match.group(1)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
106
  )
107
 
108
  explanation_text = (
@@ -113,13 +150,15 @@ class ExplanationParser:
113
  explanations.append(
114
  Explanation(
115
  document="bns",
116
- explanation_no=explanation_no,
117
- text=explanation_text
 
 
 
118
  )
119
  )
120
 
121
  return explanations
122
-
123
  # =====================================================
124
  # VALIDATION
125
  # =====================================================
 
30
  EXPLANATION_RE = re.compile(
31
  r"(?im)^Explanation\s*([0-9IVXLCDM]*)\s*[\.\-—:]"
32
  )
33
+ STOP_RE = re.compile(
34
+ r"(?im)^("
35
+ r"Explanation\s*"
36
+ r"|Illustration[s]?\."
37
+ r"|Exception\s*[.—:-]"
38
+ r"|\(\d+[A-Za-z]?\)"
39
+ r")"
40
+ )
41
+ NEXT_BLOCK_RE = re.compile(
42
+ r"(?im)^("
43
+ r"Explanation\s*"
44
+ r"|Illustration[s]?\."
45
+ r"|Exception\s*[.—:-]"
46
+ r"|\(\d+[A-Za-z]?\)"
47
+ r")"
48
+ )
49
 
50
  # =====================================================
51
  # ROMAN NUMERAL NORMALIZATION
 
111
 
112
  start = match.start()
113
 
114
+ if i + 1 < len(matches):
115
+
116
+ end = matches[i + 1].start()
117
+
118
+ else:
119
+
120
+ end = len(text)
121
+
122
+ remaining = text[start:]
123
+
124
+
125
 
126
+ next_block = list(
127
+ self.NEXT_BLOCK_RE.finditer(
128
+ remaining
129
+ )
130
+ )
131
+
132
+ if len(next_block) > 1:
133
+
134
+ end = (
135
+ start +
136
+ next_block[1].start()
137
+ )
138
+
139
+ explanation_no = (
140
+ self.normalize_explanation_no(
141
+ match.group(1)
142
+ )
143
  )
144
 
145
  explanation_text = (
 
150
  explanations.append(
151
  Explanation(
152
  document="bns",
153
+ explanation_no=
154
+ explanation_no,
155
+
156
+ text=
157
+ explanation_text
158
  )
159
  )
160
 
161
  return explanations
 
162
  # =====================================================
163
  # VALIDATION
164
  # =====================================================
db/parsers/bns/ingest.py ADDED
@@ -0,0 +1,97 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from tqdm import tqdm
2
+
3
+ from qdrant_client.models import (
4
+ PointStruct
5
+ )
6
+
7
+ from embedder_temp import (
8
+ LegalEmbedder
9
+ )
10
+
11
+ from qdrant_store import (
12
+ QdrantStore
13
+ )
14
+
15
+
16
+ class LegalIngestionPipeline:
17
+
18
+ def __init__(self):
19
+
20
+ self.embedder = (
21
+ LegalEmbedder()
22
+ )
23
+
24
+ self.store = (
25
+ QdrantStore(
26
+ collection_name="bns"
27
+ )
28
+ )
29
+
30
+ def ingest(
31
+ self,
32
+ chunks,
33
+ batch_size=64
34
+ ):
35
+
36
+ dimension = (
37
+ self.embedder
38
+ .model
39
+ .get_sentence_embedding_dimension()
40
+ )
41
+
42
+ self.store.create_collection(
43
+ dimension
44
+ )
45
+
46
+ point_id = 1
47
+
48
+ for start in tqdm(
49
+ range(
50
+ 0,
51
+ len(chunks),
52
+ batch_size
53
+ )
54
+ ):
55
+
56
+ batch = chunks[
57
+ start:
58
+ start + batch_size
59
+ ]
60
+
61
+ texts = [
62
+ chunk[
63
+ "enriched_text"
64
+ ]
65
+ for chunk in batch
66
+ ]
67
+
68
+ embeddings = (
69
+ self.embedder.embed(
70
+ texts
71
+ )
72
+ )
73
+
74
+ points = []
75
+
76
+ for chunk, embedding in zip(
77
+ batch,
78
+ embeddings
79
+ ):
80
+
81
+ points.append(
82
+ PointStruct(
83
+ id=point_id,
84
+
85
+ vector=
86
+ embedding.tolist(),
87
+
88
+ payload=
89
+ chunk
90
+ )
91
+ )
92
+
93
+ point_id += 1
94
+
95
+ self.store.upsert(
96
+ points
97
+ )
db/parsers/bns/qdrant_store.py ADDED
@@ -0,0 +1,96 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from qdrant_client import (
2
+ QdrantClient
3
+ )
4
+
5
+ from qdrant_client.models import (
6
+ Distance,
7
+ VectorParams,
8
+ PointStruct
9
+ )
10
+
11
+
12
+ class QdrantStore:
13
+
14
+ def __init__(
15
+ self,
16
+ collection_name: str,
17
+ host="localhost",
18
+ port=6333
19
+ ):
20
+
21
+ self.collection_name = (
22
+ collection_name
23
+ )
24
+
25
+ self.client = (
26
+ QdrantClient(
27
+ host=host,
28
+ port=port
29
+ )
30
+ )
31
+
32
+ def create_collection(
33
+ self,
34
+ vector_size: int
35
+ ):
36
+
37
+ collections = (
38
+ self.client
39
+ .get_collections()
40
+ )
41
+
42
+ existing = {
43
+ c.name
44
+ for c in collections.collections
45
+ }
46
+
47
+ if (
48
+ self.collection_name
49
+ not in existing
50
+ ):
51
+
52
+ self.client.create_collection(
53
+ collection_name=
54
+ self.collection_name,
55
+
56
+ vectors_config=
57
+ VectorParams(
58
+ size=
59
+ vector_size,
60
+
61
+ distance=
62
+ Distance.COSINE
63
+ )
64
+ )
65
+
66
+ def upsert(
67
+ self,
68
+ points
69
+ ):
70
+
71
+ self.client.upsert(
72
+ collection_name=
73
+ self.collection_name,
74
+
75
+ points=
76
+ points
77
+ )
78
+
79
+ def search(
80
+ self,
81
+ query_vector,
82
+ limit=10
83
+ ):
84
+
85
+ return (
86
+ self.client.query_points(
87
+ collection_name=
88
+ self.collection_name,
89
+
90
+ query=
91
+ query_vector,
92
+
93
+ limit=
94
+ limit
95
+ )
96
+ )
db/parsers/bns/search.py ADDED
@@ -0,0 +1,68 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from sentence_transformers import (
2
+ SentenceTransformer
3
+ )
4
+
5
+ from qdrant_client import (
6
+ QdrantClient
7
+ )
8
+
9
+
10
+ class BNSRetriever:
11
+
12
+ def __init__(
13
+ self,
14
+ collection_name="bns",
15
+ model_name="BAAI/bge-large-en-v1.5"
16
+ ):
17
+
18
+ self.collection_name = (
19
+ collection_name
20
+ )
21
+
22
+ self.client = (
23
+ QdrantClient(
24
+ host="localhost",
25
+ port=6333
26
+ )
27
+ )
28
+
29
+ self.model = (
30
+ SentenceTransformer(
31
+ model_name
32
+ )
33
+ )
34
+
35
+ def search(
36
+ self,
37
+ query: str,
38
+ limit: int = 10
39
+ ):
40
+
41
+ query_vector = (
42
+ self.model.encode(
43
+ query,
44
+ normalize_embeddings=True
45
+ ).tolist()
46
+ )
47
+
48
+ hits = (
49
+ self.client.query_points(
50
+ collection_name=
51
+ self.collection_name,
52
+
53
+ query=
54
+ query_vector,
55
+
56
+ limit=
57
+ limit
58
+ )
59
+ )
60
+
61
+ return hits
62
+
63
+
64
+ if __name__=="__main__":
65
+ query = "punishment for trafficking of person"
66
+
67
+ bns_search=BNSRetriever()
68
+ print(bns_search.search(query))
db/parsers/bns/usage.py ADDED
@@ -0,0 +1,112 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from collections import Counter
2
+
3
+ from cleaner import BNSTextCleaner
4
+ from bns_parser import BNSParser
5
+
6
+ from chunk_temp import (
7
+ LegalChunker,
8
+ chunks_to_dicts
9
+ )
10
+
11
+ from ingest import (
12
+ LegalIngestionPipeline
13
+ )
14
+
15
+ # =====================================================
16
+ # LOAD BNS
17
+ # =====================================================
18
+
19
+ with open(
20
+ "../../pdfs/bns.txt",
21
+ "r",
22
+ encoding="utf8"
23
+ ) as f:
24
+
25
+ text = f.read()
26
+
27
+ # =====================================================
28
+ # CLEAN
29
+ # =====================================================
30
+
31
+ cleaner = BNSTextCleaner()
32
+
33
+ text = cleaner.clean(
34
+ text
35
+ )
36
+
37
+ # =====================================================
38
+ # PARSE
39
+ # =====================================================
40
+
41
+ parser = BNSParser()
42
+
43
+ document = parser.parse(
44
+ text
45
+ )
46
+
47
+ # =====================================================
48
+ # CHUNK
49
+ # =====================================================
50
+
51
+ chunker = LegalChunker()
52
+
53
+ chunks = chunks_to_dicts(
54
+ chunker.chunk_document(
55
+ document
56
+ )
57
+ )
58
+
59
+ # =====================================================
60
+ # STATS
61
+ # =====================================================
62
+
63
+ print("\n========== CHUNK STATS ==========\n")
64
+
65
+ print(
66
+ f"Total Chunks: {len(chunks)}"
67
+ )
68
+
69
+ levels = Counter(
70
+ chunk["level"]
71
+ for chunk in chunks
72
+ )
73
+
74
+ for level, count in sorted(
75
+ levels.items()
76
+ ):
77
+ print(
78
+ f"{level}: {count}"
79
+ )
80
+
81
+ # =====================================================
82
+ # INGEST
83
+ # =====================================================
84
+
85
+ pipeline = (
86
+ LegalIngestionPipeline()
87
+ )
88
+
89
+ # Optional:
90
+ # wipe collection before indexing
91
+
92
+ try:
93
+
94
+ pipeline.store.client.delete_collection(
95
+ collection_name="bns"
96
+ )
97
+
98
+ print(
99
+ "\nDeleted existing collection."
100
+ )
101
+
102
+ except Exception:
103
+ pass
104
+
105
+ pipeline.ingest(
106
+ chunks=chunks,
107
+ batch_size=64
108
+ )
109
+
110
+ print(
111
+ "\nBNS successfully indexed."
112
+ )