sai-Rohan commited on
Commit
dbabef2
·
1 Parent(s): 660dde6

combined results of all parsers and made a single pipeline

Browse files
This view is limited to 50 files because it contains too many changes.   See raw diff
Files changed (50) hide show
  1. db/all_chunks.py +330 -0
  2. db/embbeder/bns_embedder.py +0 -230
  3. db/embbeder/constitution_embedding.py +0 -244
  4. db/embedder.py +126 -0
  5. db/embeddings_generator.py +0 -47
  6. db/ingest.py +92 -146
  7. db/parsers/base_parser.py +0 -165
  8. db/parsers/bns/bns_parser.py +7 -7
  9. db/parsers/bns/chunk_temp.py +0 -430
  10. db/{chunker/bns_chunker.py → parsers/bns/chunker.py} +0 -0
  11. db/parsers/bns/clause_parser.py +2 -2
  12. db/parsers/bns/explanation_parser.py +1 -1
  13. db/parsers/bns/ingest.py +2 -2
  14. db/parsers/bns/section_parser.py +1 -1
  15. db/parsers/bns/temp.py +0 -599
  16. db/parsers/bns/usage.py +3 -3
  17. db/parsers/bns_parser.py +0 -264
  18. db/parsers/bnss/bnss_parser.py +7 -7
  19. db/parsers/bnss/{chunk_temp.py → chunker.py} +0 -0
  20. db/parsers/bnss/clause_parser.py +2 -2
  21. db/parsers/bnss/cleaner.py +4 -4
  22. db/parsers/bnss/explanation_parser.py +1 -1
  23. db/parsers/bnss/ingest.py +2 -2
  24. db/parsers/bnss/search.py +2 -2
  25. db/parsers/bnss/section_parser.py +1 -1
  26. db/parsers/bnss/temp.py +0 -599
  27. db/parsers/bnss/usage.py +3 -3
  28. db/parsers/bnss_parser.py +0 -221
  29. db/parsers/bsa/bsa_parser.py +9 -9
  30. db/parsers/bsa/chapter_parser.py +1 -1
  31. db/parsers/bsa/clause_parser.py +1 -1
  32. db/parsers/bsa/explaination_parser.py +1 -1
  33. db/parsers/bsa/illustration_parser.py +1 -1
  34. db/parsers/bsa/ingest.py +2 -2
  35. db/parsers/bsa/part_parser.py +1 -1
  36. db/parsers/bsa/section_parser.py +2 -2
  37. db/parsers/bsa/usage.py +3 -3
  38. db/parsers/bsa_parser.py +0 -387
  39. db/{chunker/constitution_chunker.py → parsers/consitution/chunker.py} +125 -18
  40. db/parsers/consitution/chunker_test.py +0 -432
  41. db/parsers/consitution/clause_parser.py +1 -1
  42. db/parsers/consitution/cleaner.py +61 -0
  43. db/parsers/consitution/constitution_parser.py +9 -9
  44. db/parsers/consitution/embeddings_test.py +1 -1
  45. db/parsers/consitution/explanation_parser.py +1 -1
  46. db/parsers/consitution/proviso_parser.py +1 -1
  47. db/parsers/consitution/reference_extractor.py +1 -1
  48. db/parsers/consitution/schedule_parser.py +1 -1
  49. db/parsers/consitution/usage.py +4 -4
  50. db/parsers/constitution_parser.py +0 -1823
db/all_chunks.py ADDED
@@ -0,0 +1,330 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ # =====================================================
4
+ # BNS
5
+ # =====================================================
6
+
7
+ from db.parsers.bns.cleaner import (
8
+ BNSTextCleaner
9
+ )
10
+
11
+ from db.parsers.bns.bns_parser import (
12
+ BNSParser
13
+ )
14
+
15
+ from db.parsers.bns.chunker import (
16
+ LegalChunker as BNSChunker,
17
+ chunks_to_dicts as bns_to_dicts
18
+ )
19
+
20
+ # =====================================================
21
+ # BNSS
22
+ # =====================================================
23
+
24
+ from db.parsers.bnss.cleaner import (
25
+ BNSSTextCleaner
26
+ )
27
+
28
+ from db.parsers.bnss.bnss_parser import (
29
+ BNSSParser
30
+ )
31
+
32
+ from db.parsers.bnss.chunker import (
33
+ LegalChunker as BNSSChunker,
34
+ chunks_to_dicts as bnss_to_dicts
35
+ )
36
+
37
+ # =====================================================
38
+ # BSA
39
+ # =====================================================
40
+
41
+ from db.parsers.bsa.cleaner import (
42
+ BSATextCleaner
43
+ )
44
+
45
+ from db.parsers.bsa.bsa_parser import (
46
+ BSAParser
47
+ )
48
+
49
+ from db.parsers.bsa.chunker import (
50
+ LegalChunker as BSAChunker,
51
+ chunks_to_dicts as bsa_to_dicts
52
+ )
53
+
54
+ # =====================================================
55
+ # CONSTITUTION
56
+ # =====================================================
57
+
58
+ from db.parsers.consitution.cleaner import (
59
+ ConstitutionTextCleaner
60
+ )
61
+
62
+ from db.parsers.consitution.constitution_parser import (
63
+ ConstitutionParser
64
+ )
65
+
66
+ from db.parsers.consitution.chunker import (
67
+ LegalChunker as ConstitutionChunker,
68
+ chunks_to_dicts as constitution_to_dicts
69
+ )
70
+
71
+
72
+ class LegalCorpusBuilder:
73
+
74
+ # =================================================
75
+ # LOAD FILE
76
+ # =================================================
77
+
78
+ @staticmethod
79
+ def load_text(
80
+ filepath
81
+ ):
82
+
83
+ with open(
84
+ filepath,
85
+ "r",
86
+ encoding="utf8"
87
+ ) as f:
88
+
89
+ return f.read()
90
+
91
+ # =================================================
92
+ # BNS
93
+ # =================================================
94
+
95
+ def build_bns_chunks(
96
+ self,
97
+ filepath
98
+ ):
99
+
100
+ text = self.load_text(
101
+ filepath
102
+ )
103
+
104
+ text = (
105
+ BNSTextCleaner()
106
+ .clean(text)
107
+ )
108
+
109
+ document = (
110
+ BNSParser()
111
+ .parse(text)
112
+ )
113
+
114
+ chunks = (
115
+ BNSChunker(
116
+ document_name="BNS"
117
+ )
118
+ .chunk_document(
119
+ document
120
+ )
121
+ )
122
+
123
+ return (
124
+ bns_to_dicts(
125
+ chunks
126
+ )
127
+ )
128
+
129
+ # =================================================
130
+ # BNSS
131
+ # =================================================
132
+
133
+ def build_bnss_chunks(
134
+ self,
135
+ filepath
136
+ ):
137
+
138
+ text = self.load_text(
139
+ filepath
140
+ )
141
+
142
+ text = (
143
+ BNSSTextCleaner()
144
+ .clean(text)
145
+ )
146
+
147
+ document = (
148
+ BNSSParser()
149
+ .parse(text)
150
+ )
151
+
152
+ chunks = (
153
+ BNSSChunker(
154
+ document_name="BNSS"
155
+ )
156
+ .chunk_document(
157
+ document
158
+ )
159
+ )
160
+
161
+ return (
162
+ bnss_to_dicts(
163
+ chunks
164
+ )
165
+ )
166
+
167
+ # =================================================
168
+ # BSA
169
+ # =================================================
170
+
171
+ def build_bsa_chunks(
172
+ self,
173
+ filepath
174
+ ):
175
+
176
+ text = self.load_text(
177
+ filepath
178
+ )
179
+
180
+ text = (
181
+ BSATextCleaner()
182
+ .clean(text)
183
+ )
184
+
185
+ document = (
186
+ BSAParser()
187
+ .parse(text)
188
+ )
189
+
190
+ chunks = (
191
+ BSAChunker(
192
+ document_name="BSA"
193
+ )
194
+ .chunk_document(
195
+ document
196
+ )
197
+ )
198
+
199
+ return (
200
+ bsa_to_dicts(
201
+ chunks
202
+ )
203
+ )
204
+
205
+ # =================================================
206
+ # CONSTITUTION
207
+ # =================================================
208
+
209
+ def build_constitution_chunks(
210
+ self,
211
+ filepath
212
+ ):
213
+
214
+ text = self.load_text(
215
+ filepath
216
+ )
217
+
218
+ text = (
219
+ ConstitutionTextCleaner()
220
+ .clean(text)
221
+ )
222
+
223
+ document = (
224
+ ConstitutionParser()
225
+ .parse(text)
226
+ )
227
+
228
+ chunker = ConstitutionChunker()
229
+
230
+ chunks = (
231
+ chunker.chunk_constitution(
232
+ document
233
+ )
234
+ )
235
+
236
+ return (
237
+ constitution_to_dicts(
238
+ chunks
239
+ )
240
+ )
241
+
242
+ # =================================================
243
+ # ALL CHUNKS
244
+ # =================================================
245
+
246
+ def build_all_chunks(
247
+ self,
248
+ bns_path,
249
+ bnss_path,
250
+ bsa_path,
251
+ constitution_path
252
+ ):
253
+
254
+ all_chunks = []
255
+
256
+ print(
257
+ "\nBuilding BNS..."
258
+ )
259
+
260
+ all_chunks.extend(
261
+ self.build_bns_chunks(
262
+ bns_path
263
+ )
264
+ )
265
+
266
+ print(
267
+ "Building BNSS..."
268
+ )
269
+
270
+ all_chunks.extend(
271
+ self.build_bnss_chunks(
272
+ bnss_path
273
+ )
274
+ )
275
+
276
+ print(
277
+ "Building BSA..."
278
+ )
279
+
280
+ all_chunks.extend(
281
+ self.build_bsa_chunks(
282
+ bsa_path
283
+ )
284
+ )
285
+
286
+ print(
287
+ "Building Constitution..."
288
+ )
289
+
290
+ all_chunks.extend(
291
+ self.build_constitution_chunks(
292
+ constitution_path
293
+ )
294
+ )
295
+
296
+ return all_chunks
297
+
298
+
299
+ # =====================================================
300
+ # TEST
301
+ # =====================================================
302
+
303
+ if __name__ == "__main__":
304
+
305
+ builder = (
306
+ LegalCorpusBuilder()
307
+ )
308
+
309
+ all_chunks = (
310
+ builder.build_all_chunks(
311
+ bns_path=
312
+ "db/pdfs/bns.txt",
313
+
314
+ bnss_path=
315
+ "db/pdfs/bnss.txt",
316
+
317
+ bsa_path=
318
+ "db/pdfs/bsa.txt",
319
+
320
+ constitution_path=
321
+ "db/pdfs/constitution.txt"
322
+ )
323
+ )
324
+
325
+ print()
326
+
327
+ print(
328
+ f"Total Chunks: "
329
+ f"{len(all_chunks)}"
330
+ )
db/embbeder/bns_embedder.py DELETED
@@ -1,230 +0,0 @@
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/embbeder/constitution_embedding.py DELETED
@@ -1,244 +0,0 @@
1
-
2
- from __future__ import annotations
3
-
4
- import json
5
- import pickle
6
- from pathlib import Path
7
-
8
- import numpy as np
9
- from sentence_transformers import SentenceTransformer
10
-
11
-
12
- # =========================================================
13
- # EMBEDDING GENERATOR
14
- # =========================================================
15
-
16
- class EmbeddingGenerator:
17
-
18
- def __init__(
19
- self,
20
- model_name: str = "BAAI/bge-large-en-v1.5"
21
- ):
22
-
23
- self.model = SentenceTransformer(
24
- model_name
25
- )
26
-
27
- # =====================================================
28
- # GENERATE
29
- # =====================================================
30
-
31
- def generate(
32
- self,
33
- chunks: list,
34
- batch_size: int = 32
35
- ):
36
-
37
- texts = [
38
- chunk.text
39
- for chunk in chunks
40
- ]
41
-
42
- embeddings = self.model.encode(
43
- texts,
44
- batch_size=batch_size,
45
- show_progress_bar=True,
46
- convert_to_numpy=True,
47
- normalize_embeddings=True
48
- )
49
-
50
- return embeddings
51
-
52
- # =====================================================
53
- # SAVE NUMPY
54
- # =====================================================
55
-
56
- def save_embeddings(
57
- self,
58
- embeddings,
59
- output_path: str
60
- ):
61
-
62
- np.save(
63
- output_path,
64
- embeddings
65
- )
66
-
67
- # =====================================================
68
- # SAVE COMPLETE DATASET
69
- # =====================================================
70
-
71
- def save_dataset(
72
- self,
73
- chunks,
74
- embeddings,
75
- output_file: str
76
- ):
77
-
78
- records = []
79
-
80
- for chunk, embedding in zip(
81
- chunks,
82
- embeddings
83
- ):
84
-
85
- records.append(
86
- {
87
- "chunk_id":
88
- chunk.chunk_id,
89
-
90
- "chunk_type":
91
- chunk.chunk_type,
92
-
93
- "text":
94
- chunk.text,
95
-
96
- "metadata":
97
- chunk.metadata,
98
-
99
- "references":
100
- chunk.references,
101
-
102
- "embedding":
103
- embedding.tolist()
104
- }
105
- )
106
-
107
- with open(
108
- output_file,
109
- "w",
110
- encoding="utf8"
111
- ) as f:
112
-
113
- json.dump(
114
- records,
115
- f,
116
- ensure_ascii=False
117
- )
118
-
119
- # =====================================================
120
- # PICKLE
121
- # =====================================================
122
-
123
- def save_pickle(
124
- self,
125
- chunks,
126
- embeddings,
127
- output_file
128
- ):
129
-
130
- data = []
131
-
132
- for chunk, embedding in zip(
133
- chunks,
134
- embeddings
135
- ):
136
-
137
- data.append(
138
- {
139
- "chunk_id":
140
- chunk.chunk_id,
141
-
142
- "chunk_type":
143
- chunk.chunk_type,
144
-
145
- "text":
146
- chunk.text,
147
-
148
- "metadata":
149
- chunk.metadata,
150
-
151
- "references":
152
- chunk.references,
153
-
154
- "embedding":
155
- embedding
156
- }
157
- )
158
-
159
- with open(
160
- output_file,
161
- "wb"
162
- ) as f:
163
-
164
- pickle.dump(
165
- data,
166
- f
167
- )
168
-
169
-
170
- # =========================================================
171
- # EXAMPLE
172
- # =========================================================
173
-
174
- if __name__ == "__main__":
175
-
176
- from constitution_parser import ConstitutionParser
177
- from legal_chunker import LegalChunker
178
-
179
- with open(
180
- "../../pdfs/constitution.txt",
181
- "r",
182
- encoding="utf8"
183
- ) as f:
184
-
185
- text = f.read()
186
-
187
- # -----------------------------------------
188
- # Parse
189
- # -----------------------------------------
190
-
191
- parser = ConstitutionParser()
192
-
193
- constitution = parser.parse(
194
- text
195
- )
196
-
197
- # -----------------------------------------
198
- # Chunk
199
- # -----------------------------------------
200
-
201
- chunker = LegalChunker()
202
-
203
- chunks = chunker.chunk_constitution(
204
- constitution
205
- )
206
-
207
- print(
208
- "Chunks:",
209
- len(chunks)
210
- )
211
-
212
- # -----------------------------------------
213
- # Embeddings
214
- # -----------------------------------------
215
-
216
- generator = EmbeddingGenerator()
217
-
218
- embeddings = generator.generate(
219
- chunks
220
- )
221
-
222
- print(
223
- "Embeddings Shape:",
224
- embeddings.shape
225
- )
226
-
227
- # -----------------------------------------
228
- # Save
229
- # -----------------------------------------
230
-
231
- generator.save_embeddings(
232
- embeddings,
233
- "constitution_embeddings.npy"
234
- )
235
-
236
- generator.save_pickle(
237
- chunks,
238
- embeddings,
239
- "constitution_dataset.pkl"
240
- )
241
-
242
- print(
243
- "Saved successfully"
244
- )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
db/embedder.py ADDED
@@ -0,0 +1,126 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ from sentence_transformers import (
4
+ SentenceTransformer
5
+ )
6
+
7
+
8
+ class LegalEmbedder:
9
+
10
+ def __init__(
11
+ self,
12
+ model_name: str =
13
+ "BAAI/bge-large-en-v1.5"
14
+ ):
15
+
16
+ print(
17
+ f"Loading embedding model: "
18
+ f"{model_name}"
19
+ )
20
+
21
+ self.model = (
22
+ SentenceTransformer(
23
+ model_name
24
+ )
25
+ )
26
+
27
+ # =====================================================
28
+ # DOCUMENT EMBEDDINGS
29
+ # =====================================================
30
+
31
+ def embed(
32
+ self,
33
+ texts: list[str]
34
+ ):
35
+
36
+ return self.model.encode(
37
+ texts,
38
+ normalize_embeddings=True,
39
+ convert_to_numpy=True,
40
+ batch_size=16,
41
+ show_progress_bar=True
42
+ )
43
+
44
+ # =====================================================
45
+ # QUERY EMBEDDING
46
+ # =====================================================
47
+
48
+ def embed_query(
49
+ self,
50
+ query: str
51
+ ):
52
+
53
+ query = (
54
+ "Represent this sentence "
55
+ "for searching relevant "
56
+ f"passages: {query}"
57
+ )
58
+
59
+ return self.model.encode(
60
+ query,
61
+ normalize_embeddings=True,
62
+ convert_to_numpy=True
63
+ )
64
+
65
+ # =====================================================
66
+ # VECTOR SIZE
67
+ # =====================================================
68
+
69
+ def vector_size(
70
+ self
71
+ ) -> int:
72
+
73
+ return (
74
+ self.model
75
+ .get_sentence_embedding_dimension()
76
+ )
77
+
78
+
79
+ # =========================================================
80
+ # TEST
81
+ # =========================================================
82
+
83
+ if __name__ == "__main__":
84
+
85
+ embedder = (
86
+ LegalEmbedder()
87
+ )
88
+
89
+ texts = [
90
+ "Section 52. Facts of which Court shall take judicial notice.",
91
+ "Article 21. Protection of life and personal liberty."
92
+ ]
93
+
94
+ vectors = (
95
+ embedder.embed(
96
+ texts
97
+ )
98
+ )
99
+
100
+ print()
101
+
102
+ print(
103
+ "Vectors:",
104
+ len(vectors)
105
+ )
106
+
107
+ print(
108
+ "Dimension:",
109
+ len(vectors[0])
110
+ )
111
+
112
+ query_vector = (
113
+ embedder.embed_query(
114
+ "facts judicially noticed by court"
115
+ )
116
+ )
117
+
118
+ print(
119
+ "Query Dimension:",
120
+ len(query_vector)
121
+ )
122
+
123
+ print(
124
+ "Model Dimension:",
125
+ embedder.vector_size()
126
+ )
db/embeddings_generator.py DELETED
@@ -1,47 +0,0 @@
1
- from sentence_transformers import SentenceTransformer
2
- from typing import List, Dict
3
-
4
-
5
- class LegalEmbedder:
6
- """
7
- Converts legal records into embeddings.
8
- """
9
-
10
- def __init__(
11
- self,
12
- model_name: str = "BAAI/bge-large-en-v1.5"
13
- ):
14
- self.model = SentenceTransformer(
15
- model_name
16
- )
17
-
18
- def embed_records(
19
- self,
20
- records: List[Dict]
21
- ) -> List[Dict]:
22
-
23
- if not records:
24
- return []
25
-
26
- texts = [
27
- record["embedding_text"]
28
- for record in records
29
- ]
30
-
31
- embeddings = self.model.encode(
32
- texts,
33
- batch_size=32,
34
- show_progress_bar=True,
35
- convert_to_numpy=True,
36
- normalize_embeddings=True
37
- )
38
-
39
- for record, embedding in zip(
40
- records,
41
- embeddings
42
- ):
43
- record["embedding"] = (
44
- embedding.tolist()
45
- )
46
-
47
- return records
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
db/ingest.py CHANGED
@@ -1,172 +1,118 @@
1
- from collections import defaultdict
2
- import json
3
- import uuid
4
- import hashlib
5
 
6
- from embeddings_generator import LegalEmbedder
7
- from graph_db import LegalGraphDB
8
- from vector_db import QdrantIngestor
9
- from parsers.constitution_parser import ConstitutionParser
10
- from parsers.bns_parser import BNSParser
11
- from parsers.bnss_parser import BNSSParser
12
- from parsers.bsa_parser import BSAParser
13
- from parsers.legal_normaliser import LegalNormalizer
14
- from generator.legal_record_generator import LegalRecordGenerator
15
- from generator.get_all_chunks import normalise_document
16
 
17
 
18
- # --------------------------------------------------
19
- # STABLE ID GENERATOR (CORE FIX)
20
- # --------------------------------------------------
21
 
22
- def stable_id(chunk: dict, document_uuid: str) -> str:
23
- meta = chunk.get("meta", {})
 
 
24
 
25
- base = (
26
- document_uuid + "|" +
27
- chunk.get("type", "") + "|" +
28
- str(meta.get("division_no", "")) + "|" +
29
- str(meta.get("provision_no", "")) + "|" +
30
- str(meta.get("clause_no", "")) + "|" +
31
- str(meta.get("sub_clause_no", "")) + "|" +
32
- str(meta.get("roman_no", "")) + "|" +
33
- chunk.get("text", "")
34
- )
35
 
36
- return hashlib.sha256(base.encode("utf-8")).hexdigest()[:32]
 
 
 
37
 
 
 
 
38
 
39
- # --------------------------------------------------
40
- # LOAD RAW TEXT
41
- # --------------------------------------------------
 
 
42
 
43
- with open("./pdfs/constitution.txt", "r", encoding="utf-8") as f:
44
- constitution_text = f.read()
45
 
46
- with open("./pdfs/bns.txt", "r", encoding="utf-8") as f:
47
- bns_text = f.read()
 
48
 
49
- with open("./pdfs/bnss.txt", "r", encoding="utf-8") as f:
50
- bnss_text = f.read()
51
 
52
- with open("./pdfs/bsa.txt", "r", encoding="utf-8") as f:
53
- bsa_text = f.read()
 
54
 
 
 
 
 
 
 
55
 
56
- # --------------------------------------------------
57
- # PARSERS
58
- # --------------------------------------------------
59
 
60
- constitution_parser = ConstitutionParser()
61
- bns_parser = BNSParser()
62
- bnss_parser = BNSSParser()
63
- bsa_parser = BSAParser()
 
64
 
65
- constitution_parsed = constitution_parser.parse(constitution_text)
66
- bns_parsed = bns_parser.parse(bns_text)
67
- bnss_parsed = bnss_parser.parse(bnss_text)
68
- bsa_parsed = bsa_parser.parse(bsa_text)
69
 
 
 
 
 
70
 
71
- # --------------------------------------------------
72
- # NORMALISE
73
- # --------------------------------------------------
74
 
75
- normaliser = LegalNormalizer()
 
 
76
 
77
- constitution_normalised = normaliser.normalize(
78
- constitution_parsed,
79
- document_type="constitution"
80
- )
81
 
82
- bns_normalised = normaliser.normalize(
83
- bns_parsed.divisions,
84
- document_type="bns"
85
- )
86
 
87
- bnss_normalised = normaliser.normalize(
88
- bnss_parsed,
89
- document_type="bnss"
90
- )
91
 
 
 
 
 
 
 
 
 
 
 
92
 
93
- # --------------------------------------------------
94
- # FLATTEN ALL CHUNKS
95
- # --------------------------------------------------
 
96
 
97
- all_chunks = []
98
- for doc in [
99
- constitution_normalised,
100
- bns_normalised,
101
- bnss_normalised,
102
- bsa_parsed
103
- ]:
104
- all_chunks.extend(normalise_document(doc))
105
 
106
-
107
- # --------------------------------------------------
108
- # ⭐ DOCUMENT UUID (SAME FOR ENTIRE RUN)
109
- # --------------------------------------------------
110
-
111
- document_uuid = str(uuid.uuid4())
112
-
113
-
114
- # --------------------------------------------------
115
- # ��� ASSIGN STABLE IDS (IMPORTANT FIX)
116
- # --------------------------------------------------
117
-
118
- for chunk in all_chunks:
119
- chunk["document_uuid"] = document_uuid
120
- chunk["id"] = stable_id(chunk, document_uuid)
121
-
122
-
123
- # --------------------------------------------------
124
- # GRAPH DB (NEO4J)
125
- # --------------------------------------------------
126
-
127
- graph = LegalGraphDB(
128
- uri="bolt://localhost:7687",
129
- user="neo4j",
130
- password="test12345"
131
- )
132
-
133
- graph.insert_chunks(all_chunks)
134
- print("✅ Data inserted into Neo4j")
135
-
136
- for chunk in all_chunks:
137
- chunk["id"] = chunk.get("id") or stable_id(chunk)
138
- # --------------------------------------------------
139
- # RECORD GENERATION (FOR VECTOR DB)
140
- # --------------------------------------------------
141
-
142
- record_generator = LegalRecordGenerator()
143
-
144
- records = record_generator.generate(
145
- normalized_chunks=all_chunks,
146
- document_type="legal"
147
- )
148
-
149
-
150
- # --------------------------------------------------
151
- # EMBEDDINGS
152
- # --------------------------------------------------
153
-
154
- embedder = LegalEmbedder(model_name="BAAI/bge-large-en-v1.5")
155
- records = embedder.embed_records(records)
156
-
157
-
158
- # --------------------------------------------------
159
- # VECTOR DB (QDRANT)
160
- # --------------------------------------------------
161
-
162
- qdrant = QdrantIngestor()
163
- qdrant.upsert_chunks(records)
164
-
165
- print("✅ Data inserted into Qdrant")
166
-
167
-
168
- # --------------------------------------------------
169
- # CLEANUP
170
- # --------------------------------------------------
171
-
172
- graph.close()
 
1
+ from __future__ import annotations
 
 
 
2
 
3
+ from db.embedder import LegalEmbedder
4
+ from db.vector_store import QdrantStore
 
 
 
 
 
 
 
 
5
 
6
 
7
+ class LegalIngestionPipeline:
 
 
8
 
9
+ def __init__(
10
+ self,
11
+ collection_name: str = "legal_rag"
12
+ ):
13
 
14
+ self.embedder = LegalEmbedder()
 
 
 
 
 
 
 
 
 
15
 
16
+ self.store = QdrantStore(
17
+ collection_name=
18
+ collection_name
19
+ )
20
 
21
+ # =====================================================
22
+ # INGEST
23
+ # =====================================================
24
 
25
+ def ingest(
26
+ self,
27
+ chunks: list[dict],
28
+ recreate_collection: bool = False
29
+ ):
30
 
31
+ if not chunks:
 
32
 
33
+ print(
34
+ "No chunks found."
35
+ )
36
 
37
+ return
 
38
 
39
+ print(
40
+ f"\nChunks: {len(chunks)}"
41
+ )
42
 
43
+ texts = [
44
+ chunk[
45
+ "enriched_text"
46
+ ]
47
+ for chunk in chunks
48
+ ]
49
 
50
+ print(
51
+ "Generating embeddings..."
52
+ )
53
 
54
+ embeddings = (
55
+ self.embedder.embed(
56
+ texts
57
+ )
58
+ )
59
 
60
+ vector_size = len(
61
+ embeddings[0]
62
+ )
 
63
 
64
+ print(
65
+ f"Vector Size: "
66
+ f"{vector_size}"
67
+ )
68
 
69
+ if recreate_collection:
 
 
70
 
71
+ self.store.recreate_collection(
72
+ vector_size
73
+ )
74
 
75
+ else:
 
 
 
76
 
77
+ self.store.create_collection(
78
+ vector_size
79
+ )
 
80
 
81
+ points = []
 
 
 
82
 
83
+ for idx, (
84
+ chunk,
85
+ vector
86
+ ) in enumerate(
87
+ zip(
88
+ chunks,
89
+ embeddings
90
+ ),
91
+ start=1
92
+ ):
93
 
94
+ points.append(
95
+ {
96
+ "id":
97
+ idx,
98
 
99
+ "vector":
100
+ vector.tolist(),
 
 
 
 
 
 
101
 
102
+ "payload":
103
+ chunk
104
+ }
105
+ )
106
+
107
+ print(
108
+ f"Uploading "
109
+ f"{len(points)} points..."
110
+ )
111
+
112
+ self.store.upsert_points(
113
+ points
114
+ )
115
+
116
+ print(
117
+ "\nIngestion Complete."
118
+ )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
db/parsers/base_parser.py DELETED
@@ -1,165 +0,0 @@
1
- from abc import ABC, abstractmethod
2
- import re
3
-
4
-
5
- class BaseLegalParser(ABC):
6
-
7
- # ----------------------------------
8
- # REFERENCES
9
- # ----------------------------------
10
-
11
- REFERENCE_RE = re.compile(
12
- r"(?:article|section)[s]?\s+(\d+[A-Z]?)",
13
- re.IGNORECASE
14
- )
15
-
16
- # ----------------------------------
17
- # LEGAL HIERARCHY
18
- # ----------------------------------
19
-
20
- CLAUSE_RE = re.compile(
21
- r"(?m)^\((\d+[A-Za-z]?)\)\s*(.*)"
22
- )
23
-
24
- SUBCLAUSE_RE = re.compile(
25
- r"(?m)^\(([a-z])\)\s*(.*)"
26
- )
27
-
28
- ROMAN_RE = re.compile(
29
- r"(?m)^\((i|ii|iii|iv|v|vi|vii|viii|ix|x)\)\s*(.*)"
30
- )
31
-
32
- # ----------------------------------
33
- # CLEANER
34
- # ----------------------------------
35
-
36
- def clean(self, text: str):
37
-
38
- text = text.replace("\r\n", "\n")
39
-
40
- text = re.sub(
41
- r"(?m)^\s*\d+\s*$",
42
- "",
43
- text
44
- )
45
-
46
- text = re.sub(
47
- r"\n{3,}",
48
- "\n\n",
49
- text
50
- )
51
-
52
- return text.strip()
53
-
54
- # ----------------------------------
55
- # REFERENCES
56
- # ----------------------------------
57
-
58
- def extract_references(self, text):
59
-
60
- return sorted(
61
- set(
62
- self.REFERENCE_RE.findall(text)
63
- )
64
- )
65
-
66
- # ----------------------------------
67
- # CLAUSES (1) (2) (3)
68
- # ----------------------------------
69
-
70
- def extract_clauses(self, text):
71
-
72
- matches = list(
73
- self.CLAUSE_RE.finditer(text)
74
- )
75
-
76
- results = []
77
-
78
- for i, m in enumerate(matches):
79
-
80
- start = m.start()
81
-
82
- end = (
83
- matches[i + 1].start()
84
- if i + 1 < len(matches)
85
- else len(text)
86
- )
87
-
88
- results.append(
89
- {
90
- "clause_no": m.group(1),
91
- "text": text[start:end].strip()
92
- }
93
- )
94
-
95
- return results
96
-
97
- # ----------------------------------
98
- # SUBCLAUSES (a)(b)(c)
99
- # ----------------------------------
100
-
101
- def extract_subclauses(self, text):
102
-
103
- matches = list(
104
- self.SUBCLAUSE_RE.finditer(text)
105
- )
106
-
107
- results = []
108
-
109
- for i, m in enumerate(matches):
110
-
111
- start = m.start()
112
-
113
- end = (
114
- matches[i + 1].start()
115
- if i + 1 < len(matches)
116
- else len(text)
117
- )
118
-
119
- results.append(
120
- {
121
- "sub_clause_no": m.group(1),
122
- "text": text[start:end].strip()
123
- }
124
- )
125
-
126
- return results
127
-
128
- # ----------------------------------
129
- # ROMAN CLAUSES (i)(ii)(iii)
130
- # ----------------------------------
131
-
132
- def extract_roman_clauses(self, text):
133
-
134
- matches = list(
135
- self.ROMAN_RE.finditer(text)
136
- )
137
-
138
- results = []
139
-
140
- for i, m in enumerate(matches):
141
-
142
- start = m.start()
143
-
144
- end = (
145
- matches[i + 1].start()
146
- if i + 1 < len(matches)
147
- else len(text)
148
- )
149
-
150
- results.append(
151
- {
152
- "roman_no": m.group(1),
153
- "text": text[start:end].strip()
154
- }
155
- )
156
-
157
- return results
158
-
159
- # ----------------------------------
160
- # ABSTRACT
161
- # ----------------------------------
162
-
163
- @abstractmethod
164
- def parse(self, text):
165
- pass
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
db/parsers/bns/bns_parser.py CHANGED
@@ -1,13 +1,13 @@
1
  from __future__ import annotations
2
 
3
- from chapter_parser import ChapterParser
4
- from section_parser import SectionParser
5
- from clause_parser import ClauseParser
6
- from explanation_parser import ExplanationParser
7
- from reference_extractor import ReferenceExtractor
8
- from cleaner import BNSTextCleaner
9
  import re
10
- from legal_models import (
11
  BNSDocument,
12
  Chapter,
13
  Section
 
1
  from __future__ import annotations
2
 
3
+ from db.parsers.bns.chapter_parser import ChapterParser
4
+ from db.parsers.bns.section_parser import SectionParser
5
+ from db.parsers.bns.clause_parser import ClauseParser
6
+ from db.parsers.bns.explanation_parser import ExplanationParser
7
+ from db.parsers.bns.reference_extractor import ReferenceExtractor
8
+ from db.parsers.bns.cleaner import BNSTextCleaner
9
  import re
10
+ from db.parsers.bns.legal_models import (
11
  BNSDocument,
12
  Chapter,
13
  Section
db/parsers/bns/chunk_temp.py DELETED
@@ -1,430 +0,0 @@
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/{chunker/bns_chunker.py → parsers/bns/chunker.py} RENAMED
File without changes
db/parsers/bns/clause_parser.py CHANGED
@@ -3,8 +3,8 @@ from __future__ import annotations
3
  import re
4
  from typing import List
5
 
6
- from cleaner import BNSTextCleaner
7
- from legal_models import (
8
  Clause,
9
  SubClause,
10
  RomanClause
 
3
  import re
4
  from typing import List
5
 
6
+ from db.parsers.bns.cleaner import BNSTextCleaner
7
+ from db.parsers.bns.legal_models import (
8
  Clause,
9
  SubClause,
10
  RomanClause
db/parsers/bns/explanation_parser.py CHANGED
@@ -3,7 +3,7 @@ from __future__ import annotations
3
  import re
4
  from typing import List, Optional
5
 
6
- from legal_models import Explanation
7
 
8
 
9
  class ExplanationParser:
 
3
  import re
4
  from typing import List, Optional
5
 
6
+ from db.parsers.bns.legal_models import Explanation
7
 
8
 
9
  class ExplanationParser:
db/parsers/bns/ingest.py CHANGED
@@ -4,11 +4,11 @@ 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
 
 
4
  PointStruct
5
  )
6
 
7
+ from db.parsers.bns.embedder_temp import (
8
  LegalEmbedder
9
  )
10
 
11
+ from db.parsers.bns.qdrant_store import (
12
  QdrantStore
13
  )
14
 
db/parsers/bns/section_parser.py CHANGED
@@ -3,7 +3,7 @@ from __future__ import annotations
3
 
4
  import re
5
  from typing import List, Dict
6
- from cleaner import BNSTextCleaner
7
 
8
 
9
  class SectionParser:
 
3
 
4
  import re
5
  from typing import List, Dict
6
+ from db.parsers.bns.cleaner import BNSTextCleaner
7
 
8
 
9
  class SectionParser:
db/parsers/bns/temp.py DELETED
@@ -1,599 +0,0 @@
1
- from __future__ import annotations
2
-
3
- import re
4
- from typing import List
5
-
6
- from cleaner import BNSTextCleaner
7
- from legal_models import (
8
- Clause,
9
- SubClause,
10
- RomanClause
11
- )
12
-
13
-
14
- class ClauseParser:
15
-
16
- """
17
- BNS Clause Parser
18
-
19
- Supports:
20
-
21
- (1)
22
- (2)
23
- (2A)
24
-
25
- Sub Clauses:
26
-
27
- (a)
28
- (b)
29
-
30
- Roman Clauses:
31
-
32
- (i)
33
- (ii)
34
- (iii)
35
-
36
- """
37
-
38
- # =====================================================
39
- # REGEX
40
- # =====================================================
41
- NUMERIC_RE = re.compile(
42
- r'(?m)^\((\d+[A-Z]?)\)'
43
- )
44
-
45
- ALPHA_RE = re.compile(
46
- r'(?m)^\(([a-z])\)'
47
- )
48
-
49
- CAPITAL_RE = re.compile(
50
- r'(?m)^\(([A-Z])\)'
51
- )
52
-
53
- ROMAN_RE = re.compile(
54
- r'(?:(?<=\n)|(?<=;)|(?<=:)|(?<=—)|(?<=\.)|^)\s*\((i|ii|iii|iv|v|vi|vii|viii|ix|x|xi|xii|xiii|xiv|xv|xvi|xvii|xviii|xix|xx)\)',
55
- re.I
56
- )
57
- CLAUSE_RE = re.compile(
58
- r'(?m)^\s*\((\d+[A-Z]?)\)'
59
- )
60
-
61
- SUBCLAUSE_RE = re.compile(
62
- r'(?m)^\s*\(([a-z])\)'
63
- )
64
-
65
- # =====================================================
66
- # SPLIT SECTIONS
67
- # =====================================================
68
-
69
- def split_sections(
70
- self,
71
- text: str,
72
- pattern: re.Pattern
73
- ):
74
-
75
- matches = list(
76
- pattern.finditer(text)
77
- )
78
-
79
- if not matches:
80
- return []
81
-
82
- sections = []
83
-
84
- for i, match in enumerate(matches):
85
-
86
- start = match.start()
87
-
88
- end = (
89
- matches[i + 1].start()
90
- if i + 1 < len(matches)
91
- else len(text)
92
- )
93
-
94
- sections.append(
95
- (
96
- match.group(1),
97
- text[start:end].strip()
98
- )
99
- )
100
-
101
- return sections
102
-
103
- # =====================================================
104
- # ROMAN CLAUSES
105
- # =====================================================
106
-
107
- def parse_roman_clauses(
108
- self,
109
- text: str
110
- ) -> List[RomanClause]:
111
-
112
- romans = []
113
-
114
- roman_sections = (
115
- self.split_sections(
116
- text,
117
- self.ROMAN_RE
118
- )
119
- )
120
-
121
- for roman_no, roman_text in roman_sections:
122
-
123
- romans.append(
124
- RomanClause(
125
- document="bns",
126
- roman_no=roman_no,
127
- text=roman_text
128
- )
129
- )
130
-
131
- return romans
132
-
133
- # =====================================================
134
- # SUB CLAUSES
135
- # =====================================================
136
-
137
- ROMAN_VALUES = {
138
- "i", "ii", "iii", "iv", "v",
139
- "vi", "vii", "viii", "ix", "x",
140
- "xi", "xii", "xiii", "xiv", "xv",
141
- "xvi", "xvii", "xviii", "xix", "xx"
142
- }
143
- @staticmethod
144
- def remove_illustrations(
145
- self,
146
- text: str
147
- ):
148
-
149
- return re.split(
150
- r'Illustrations?\.',
151
- text,
152
- maxsplit=1,
153
- flags=re.I
154
- )[0]
155
-
156
- def parse_subclauses(
157
- self,
158
- text: str
159
- ) -> List[SubClause]:
160
- text=self.remove_illustrations(text)
161
- subclauses = []
162
-
163
- sub_sections = (
164
- self.split_sections(
165
- text,
166
- self.SUBCLAUSE_RE
167
- )
168
- )
169
-
170
- for sub_no, sub_text in sub_sections:
171
-
172
- # Skip roman numerals
173
- if (
174
- sub_no.lower()
175
- in self.ROMAN_VALUES
176
- ):
177
- continue
178
-
179
- sub = SubClause(
180
- document="bns",
181
- sub_clause_no=sub_no,
182
- text=sub_text
183
- )
184
-
185
- sub.roman_clauses.extend(
186
- self.parse_roman_clauses(
187
- sub_text
188
- )
189
- )
190
-
191
- subclauses.append(
192
- sub
193
- )
194
-
195
- return subclauses
196
-
197
-
198
- def build_alpha_structure(
199
- self,
200
- text: str,
201
- pattern: re.Pattern
202
- ) -> List[Clause]:
203
-
204
- clause = Clause(
205
- document="bns",
206
- clause_no="0",
207
- text=text
208
- )
209
-
210
- sections = self.split_sections(
211
- text,
212
- pattern
213
- )
214
-
215
- for sub_no, sub_text in sections:
216
-
217
- sub = SubClause(
218
- document="bns",
219
- sub_clause_no=sub_no,
220
- text=sub_text
221
- )
222
-
223
- sub.roman_clauses.extend(
224
- self.parse_roman_clauses(
225
- sub_text
226
- )
227
- )
228
-
229
- clause.sub_clauses.append(
230
- sub
231
- )
232
-
233
- return [clause]
234
-
235
- def build_numeric_structure(
236
- self,
237
- text: str
238
- ) -> List[Clause]:
239
-
240
- clauses = []
241
-
242
- clause_sections = self.split_sections(
243
- text,
244
- self.NUMERIC_RE
245
- )
246
-
247
- for clause_no, clause_text in clause_sections:
248
-
249
- clause = Clause(
250
- document="bns",
251
- clause_no=clause_no,
252
- text=clause_text
253
- )
254
-
255
- alpha_sections = self.split_sections(
256
- clause_text,
257
- self.ALPHA_RE
258
- )
259
-
260
- if alpha_sections:
261
-
262
- for sub_no, sub_text in alpha_sections:
263
-
264
- sub = SubClause(
265
- document="bns",
266
- sub_clause_no=sub_no,
267
- text=sub_text
268
- )
269
-
270
- sub.roman_clauses.extend(
271
- self.parse_roman_clauses(
272
- sub_text
273
- )
274
- )
275
-
276
- clause.sub_clauses.append(
277
- sub
278
- )
279
-
280
- clauses.append(
281
- clause
282
- )
283
-
284
- return clauses
285
- def parse_alpha_clauses(
286
- self,
287
- text: str
288
- ):
289
-
290
- return self.build_alpha_structure(
291
- text,
292
- self.ALPHA_RE
293
- )
294
-
295
-
296
- def parse_alpha_roman(
297
- self,
298
- text: str
299
- ):
300
-
301
- return self.build_alpha_structure(
302
- text,
303
- self.ALPHA_RE
304
- )
305
-
306
-
307
- def parse_capital_roman(
308
- self,
309
- text: str
310
- ):
311
-
312
- return self.build_alpha_structure(
313
- text,
314
- self.CAPITAL_RE
315
- )
316
-
317
-
318
- def parse_numeric_clauses(
319
- self,
320
- text: str
321
- ):
322
-
323
- clauses = []
324
-
325
- sections = self.split_sections(
326
- text,
327
- self.NUMERIC_RE
328
- )
329
-
330
- for clause_no, clause_text in sections:
331
-
332
- clauses.append(
333
- Clause(
334
- document="bns",
335
- clause_no=clause_no,
336
- text=clause_text
337
- )
338
- )
339
-
340
- return clauses
341
-
342
- def parse_numeric_alpha(
343
- self,
344
- text: str
345
- ):
346
-
347
- return self.build_numeric_structure(
348
- text
349
- )
350
-
351
-
352
- def parse_numeric_alpha_roman(
353
- self,
354
- text: str
355
- ):
356
-
357
- return self.build_numeric_structure(
358
- text
359
- )
360
-
361
-
362
-
363
-
364
- # =====================================================
365
- # CLAUSES
366
- # =====================================================
367
-
368
- def parse_clauses(
369
- self,
370
- section_text: str
371
- ) -> List[Clause]:
372
-
373
- clauses = []
374
-
375
- clause_sections = self.split_sections(
376
- section_text,
377
- self.CLAUSE_RE
378
- )
379
-
380
- # No numbered clauses present
381
- if not clause_sections:
382
-
383
- subclauses = self.parse_subclauses(
384
- section_text
385
- )
386
-
387
- if subclauses:
388
-
389
- synthetic_clause = Clause(
390
- document="bns",
391
- clause_no="0",
392
- text=section_text
393
- )
394
-
395
- synthetic_clause.sub_clauses.extend(
396
- subclauses
397
- )
398
-
399
- clauses.append(
400
- synthetic_clause
401
- )
402
-
403
- return clauses
404
-
405
- for clause_no, clause_text in clause_sections:
406
-
407
- clause = Clause(
408
- document="bns",
409
- clause_no=clause_no,
410
- text=clause_text
411
- )
412
-
413
- clause.sub_clauses.extend(
414
- self.parse_subclauses(
415
- clause_text
416
- )
417
- )
418
- clause.roman_clauses.extend(
419
- self.parse_roman_clauses(
420
- clause_text
421
- )
422
- )
423
-
424
- clauses.append(
425
- clause
426
- )
427
-
428
- return clauses
429
-
430
-
431
-
432
-
433
- def parse_section_structure(
434
- self,
435
- section_text: str
436
- ) -> List[Clause]:
437
-
438
- has_numeric = bool(
439
- self.NUMERIC_RE.search(
440
- section_text
441
- )
442
- )
443
-
444
- has_alpha = bool(
445
- self.ALPHA_RE.search(
446
- section_text
447
- )
448
- )
449
-
450
- has_capital = bool(
451
- self.CAPITAL_RE.search(
452
- section_text
453
- )
454
- )
455
-
456
- has_roman = bool(
457
- self.ROMAN_RE.search(
458
- section_text
459
- )
460
- )
461
-
462
- if has_numeric:
463
- return self.build_numeric_structure(
464
- section_text
465
- )
466
-
467
- if has_capital:
468
- return self.build_alpha_structure(
469
- section_text,
470
- self.CAPITAL_RE
471
- )
472
-
473
- if has_alpha:
474
- return self.build_alpha_structure(
475
- section_text,
476
- self.ALPHA_RE
477
- )
478
-
479
- return []
480
-
481
-
482
-
483
-
484
-
485
-
486
-
487
- # =====================================================
488
- # VALIDATION
489
- # =====================================================
490
-
491
- def validate_clauses(
492
- self,
493
- section_no,
494
- clauses: List[Clause]
495
- ) -> List[str]:
496
-
497
- errors = []
498
-
499
- seen = set()
500
-
501
- for clause in clauses:
502
-
503
- if clause.clause_no in seen:
504
-
505
- print("\n" + "="*80)
506
- print("section: ",section_no)
507
- print("DUPLICATE SUBCLAUSE")
508
- print("Clause:", clause.clause_no)
509
- print("SubClause:", sub.sub_clause_no)
510
- print(sub.text[:1000])
511
-
512
- errors.append(
513
- f"Duplicate SubClause "
514
- f"{sub.sub_clause_no}"
515
- )
516
-
517
- seen.add(
518
- clause.clause_no
519
- )
520
-
521
- sub_seen = set()
522
-
523
- for sub in clause.sub_clauses:
524
-
525
- if sub.sub_clause_no in sub_seen:
526
-
527
- errors.append(
528
- f"Duplicate SubClause "
529
- f"{sub.sub_clause_no}"
530
- )
531
-
532
- sub_seen.add(
533
- sub.sub_clause_no
534
- )
535
-
536
- roman_seen = set()
537
-
538
- for roman in sub.roman_clauses:
539
-
540
- if roman.roman_no in roman_seen:
541
-
542
- errors.append(
543
- f"Duplicate Roman "
544
- f"{roman.roman_no}"
545
- )
546
-
547
- roman_seen.add(
548
- roman.roman_no
549
- )
550
-
551
- return errors
552
-
553
-
554
- # =========================================================
555
- # TEST
556
- # =========================================================
557
-
558
- if __name__ == "__main__":
559
-
560
- with open(
561
- "../../pdfs/bns.txt",
562
- "r",
563
- encoding="utf8"
564
- ) as f:
565
-
566
- text = f.read()
567
-
568
- cleaner=BNSTextCleaner()
569
- text=cleaner.clean(text)
570
-
571
-
572
-
573
- parser = ClauseParser()
574
-
575
- print(len(parser.CLAUSE_RE.findall(text)))
576
- print(len(parser.SUBCLAUSE_RE.findall(text)))
577
- print(len(parser.ROMAN_RE.findall(text)))
578
-
579
- clauses = parser.parse_section_structure(
580
- text
581
- )
582
-
583
- print(
584
- "Clauses:",
585
- len(clauses)
586
- )
587
-
588
- # for clause in clauses:
589
-
590
- # print(
591
- # clause.clause_no,
592
- # len(clause.sub_clauses)
593
- # )
594
-
595
- # print(
596
- # parser.validate_clauses(
597
- # clauses
598
- # )
599
- # )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
db/parsers/bns/usage.py CHANGED
@@ -1,9 +1,9 @@
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
  )
 
1
  from collections import Counter
2
 
3
+ from db.parsers.bns.cleaner import BNSTextCleaner
4
+ from db.parsers.bns.bns_parser import BNSParser
5
 
6
+ from db.parsers.bns.chunker import (
7
  LegalChunker,
8
  chunks_to_dicts
9
  )
db/parsers/bns_parser.py DELETED
@@ -1,264 +0,0 @@
1
- # parsers/bns_parser.py
2
-
3
- from __future__ import annotations
4
-
5
- import re
6
-
7
- from parsers.base_parser import BaseLegalParser
8
-
9
- from models.legal_models import (
10
- Chapter,
11
- Section,
12
- Clause,
13
- SubClause,
14
- RomanClause,
15
- LegalDocument
16
- )
17
-
18
-
19
- class BNSParser(BaseLegalParser):
20
-
21
- CHAPTER_RE = re.compile(
22
- r"CHAPTER\s+([IVXLC]+)\s*\n+([^\n]+)",
23
- re.IGNORECASE
24
- )
25
-
26
- SECTION_RE = re.compile(
27
- r"(?m)^(\d+[A-Z]?)\.\s+(.+)$"
28
- )
29
-
30
- # -------------------------------------------------
31
- # CHAPTERS
32
- # -------------------------------------------------
33
-
34
- def extract_chapters(self, text):
35
-
36
- matches = list(
37
- self.CHAPTER_RE.finditer(text)
38
- )
39
-
40
- chapters = []
41
-
42
- for i, match in enumerate(matches):
43
-
44
- start = match.start()
45
-
46
- end = (
47
- matches[i + 1].start()
48
- if i + 1 < len(matches)
49
- else len(text)
50
- )
51
-
52
- chapters.append(
53
- {
54
- "chapter_no":
55
- match.group(1).strip(),
56
-
57
- "chapter_title":
58
- match.group(2).strip(),
59
-
60
- "text":
61
- text[start:end]
62
- }
63
- )
64
-
65
- return chapters
66
-
67
- # -------------------------------------------------
68
- # SECTIONS
69
- # -------------------------------------------------
70
-
71
- def extract_sections(self, chapter_text):
72
-
73
- matches = list(
74
- self.SECTION_RE.finditer(
75
- chapter_text
76
- )
77
- )
78
-
79
- sections = []
80
-
81
- for i, match in enumerate(matches):
82
-
83
- start = match.start()
84
-
85
- end = (
86
- matches[i + 1].start()
87
- if i + 1 < len(matches)
88
- else len(chapter_text)
89
- )
90
-
91
- sections.append(
92
- {
93
- "section_no":
94
- match.group(1),
95
-
96
- "section_title":
97
- match.group(2).strip(),
98
-
99
- "text":
100
- chapter_text[
101
- start:end
102
- ].strip()
103
- }
104
- )
105
-
106
- return sections
107
-
108
- # -------------------------------------------------
109
- # MAIN PARSER
110
- # -------------------------------------------------
111
-
112
- def parse(self, raw_text):
113
-
114
- raw_text = self.clean(raw_text)
115
-
116
- parsed_chapters = []
117
-
118
- chapter_blocks = (
119
- self.extract_chapters(
120
- raw_text
121
- )
122
- )
123
-
124
- for chapter_data in chapter_blocks:
125
-
126
- chapter = Chapter(
127
- document="bns",
128
- chapter_no=
129
- chapter_data[
130
- "chapter_no"
131
- ],
132
-
133
- chapter_title=
134
- chapter_data[
135
- "chapter_title"
136
- ]
137
- )
138
-
139
- section_blocks = (
140
- self.extract_sections(
141
- chapter_data["text"]
142
- )
143
- )
144
-
145
- for section_data in section_blocks:
146
-
147
- section = Section(
148
- document="bns",
149
-
150
- section_no=
151
- section_data[
152
- "section_no"
153
- ],
154
-
155
- section_title=
156
- section_data[
157
- "section_title"
158
- ],
159
-
160
- text=
161
- section_data[
162
- "text"
163
- ]
164
- )
165
-
166
- # -------------------------
167
- # SUBSECTIONS (1),(2),(3)
168
- # -------------------------
169
-
170
- clause_blocks = (
171
- self.extract_clauses(
172
- section.text
173
- )
174
- )
175
-
176
- for clause_data in clause_blocks:
177
-
178
- clause = Clause(
179
- document="bns",
180
- clause_no=
181
- clause_data[
182
- "clause_no"
183
- ],
184
-
185
- text=
186
- clause_data[
187
- "text"
188
- ]
189
- )
190
-
191
- # ---------------------
192
- # (a)(b)(c)
193
- # ---------------------
194
-
195
- sub_blocks = (
196
- self.extract_subclauses(
197
- clause.text
198
- )
199
- )
200
-
201
- for sub_data in sub_blocks:
202
-
203
- sub_clause = (
204
- SubClause(
205
- sub_clause_no=
206
- sub_data[
207
- "sub_clause_no"
208
- ],
209
-
210
- text=
211
- sub_data[
212
- "text"
213
- ]
214
- )
215
- )
216
-
217
- # -----------------
218
- # (i)(ii)(iii)
219
- # -----------------
220
-
221
- roman_blocks = (
222
- self.extract_roman_clauses(
223
- sub_clause.text
224
- )
225
- )
226
-
227
- for roman_data in roman_blocks:
228
-
229
- sub_clause.roman_clauses.append(
230
- RomanClause(
231
- document="bns",
232
- roman_no=
233
- roman_data[
234
- "roman_no"
235
- ],
236
-
237
- text=
238
- roman_data[
239
- "text"
240
- ]
241
- )
242
- )
243
-
244
- clause.sub_clauses.append(
245
- sub_clause
246
- )
247
-
248
- section.clauses.append(
249
- clause
250
- )
251
-
252
- chapter.sections.append(
253
- section
254
- )
255
-
256
- parsed_chapters.append(
257
- chapter
258
- )
259
-
260
- return LegalDocument(
261
- document="bns",
262
- document_type="bns",
263
- divisions=parsed_chapters
264
- )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
db/parsers/bnss/bnss_parser.py CHANGED
@@ -1,13 +1,13 @@
1
  from __future__ import annotations
2
 
3
- from chapter_parser import ChapterParser
4
- from section_parser import SectionParser
5
- from clause_parser import ClauseParser
6
- from explanation_parser import ExplanationParser
7
- from reference_extractor import ReferenceExtractor
8
- from cleaner import BNSSTextCleaner
9
  import re
10
- from legal_models import (
11
  BNSSDocument,
12
  Chapter,
13
  Section
 
1
  from __future__ import annotations
2
 
3
+ from db.parsers.bnss.chapter_parser import ChapterParser
4
+ from db.parsers.bnss.section_parser import SectionParser
5
+ from db.parsers.bnss.clause_parser import ClauseParser
6
+ from db.parsers.bnss.explanation_parser import ExplanationParser
7
+ from db.parsers.bnss.reference_extractor import ReferenceExtractor
8
+ from db.parsers.bnss.cleaner import BNSSTextCleaner
9
  import re
10
+ from db.parsers.bnss.legal_models import (
11
  BNSSDocument,
12
  Chapter,
13
  Section
db/parsers/bnss/{chunk_temp.py → chunker.py} RENAMED
File without changes
db/parsers/bnss/clause_parser.py CHANGED
@@ -3,8 +3,8 @@ from __future__ import annotations
3
  import re
4
  from typing import List
5
 
6
- from cleaner import BNSSTextCleaner
7
- from legal_models import (
8
  Clause,
9
  SubClause,
10
  RomanClause
 
3
  import re
4
  from typing import List
5
 
6
+ from db.parsers.bnss.cleaner import BNSSTextCleaner
7
+ from db.parsers.bnss.legal_models import (
8
  Clause,
9
  SubClause,
10
  RomanClause
db/parsers/bnss/cleaner.py CHANGED
@@ -132,7 +132,6 @@ class BNSSTextCleaner:
132
 
133
  text = re.sub(
134
  r"""
135
- (?mx)
136
  ^
137
  (?:
138
  [A-Z][A-Za-z' -]{0,40}
@@ -142,9 +141,10 @@ class BNSSTextCleaner:
142
  $
143
  """,
144
  "",
145
- text
146
- )
147
-
 
148
  # =====================================
149
  # Collapse empty lines
150
  # =====================================
 
132
 
133
  text = re.sub(
134
  r"""
 
135
  ^
136
  (?:
137
  [A-Z][A-Za-z' -]{0,40}
 
141
  $
142
  """,
143
  "",
144
+ text,
145
+ flags=re.MULTILINE | re.VERBOSE
146
+ )
147
+
148
  # =====================================
149
  # Collapse empty lines
150
  # =====================================
db/parsers/bnss/explanation_parser.py CHANGED
@@ -3,7 +3,7 @@ from __future__ import annotations
3
  import re
4
  from typing import List, Optional
5
 
6
- from legal_models import Explanation
7
 
8
 
9
  class ExplanationParser:
 
3
  import re
4
  from typing import List, Optional
5
 
6
+ from db.parsers.bnss.legal_models import Explanation
7
 
8
 
9
  class ExplanationParser:
db/parsers/bnss/ingest.py CHANGED
@@ -6,11 +6,11 @@ from qdrant_client.models import (
6
  PointStruct
7
  )
8
 
9
- from embedder_temp import (
10
  LegalEmbedder
11
  )
12
 
13
- from qdrant_store import (
14
  QdrantStore
15
  )
16
 
 
6
  PointStruct
7
  )
8
 
9
+ from db.parsers.bnss.embedder_temp import (
10
  LegalEmbedder
11
  )
12
 
13
+ from db.parsers.bnss.qdrant_store import (
14
  QdrantStore
15
  )
16
 
db/parsers/bnss/search.py CHANGED
@@ -1,5 +1,5 @@
1
- from embedder_temp import LegalEmbedder
2
- from qdrant_store import QdrantStore
3
 
4
 
5
  class BNSSRetriever:
 
1
+ from db.parsers.bnss.embedder_temp import LegalEmbedder
2
+ from db.parsers.bnss.qdrant_store import QdrantStore
3
 
4
 
5
  class BNSSRetriever:
db/parsers/bnss/section_parser.py CHANGED
@@ -3,7 +3,7 @@ from __future__ import annotations
3
 
4
  import re
5
  from typing import List, Dict
6
- from cleaner import BNSSTextCleaner
7
 
8
 
9
  class SectionParser:
 
3
 
4
  import re
5
  from typing import List, Dict
6
+ from db.parsers.bnss.cleaner import BNSSTextCleaner
7
 
8
 
9
  class SectionParser:
db/parsers/bnss/temp.py DELETED
@@ -1,599 +0,0 @@
1
- from __future__ import annotations
2
-
3
- import re
4
- from typing import List
5
-
6
- from cleaner import BNSSTextCleaner
7
- from legal_models import (
8
- Clause,
9
- SubClause,
10
- RomanClause
11
- )
12
-
13
-
14
- class ClauseParser:
15
-
16
- """
17
- BNS Clause Parser
18
-
19
- Supports:
20
-
21
- (1)
22
- (2)
23
- (2A)
24
-
25
- Sub Clauses:
26
-
27
- (a)
28
- (b)
29
-
30
- Roman Clauses:
31
-
32
- (i)
33
- (ii)
34
- (iii)
35
-
36
- """
37
-
38
- # =====================================================
39
- # REGEX
40
- # =====================================================
41
- NUMERIC_RE = re.compile(
42
- r'(?m)^\((\d+[A-Z]?)\)'
43
- )
44
-
45
- ALPHA_RE = re.compile(
46
- r'(?m)^\(([a-z])\)'
47
- )
48
-
49
- CAPITAL_RE = re.compile(
50
- r'(?m)^\(([A-Z])\)'
51
- )
52
-
53
- ROMAN_RE = re.compile(
54
- r'(?:(?<=\n)|(?<=;)|(?<=:)|(?<=—)|(?<=\.)|^)\s*\((i|ii|iii|iv|v|vi|vii|viii|ix|x|xi|xii|xiii|xiv|xv|xvi|xvii|xviii|xix|xx)\)',
55
- re.I
56
- )
57
- CLAUSE_RE = re.compile(
58
- r'(?m)^\s*\((\d+[A-Z]?)\)'
59
- )
60
-
61
- SUBCLAUSE_RE = re.compile(
62
- r'(?m)^\s*\(([a-z])\)'
63
- )
64
-
65
- # =====================================================
66
- # SPLIT SECTIONS
67
- # =====================================================
68
-
69
- def split_sections(
70
- self,
71
- text: str,
72
- pattern: re.Pattern
73
- ):
74
-
75
- matches = list(
76
- pattern.finditer(text)
77
- )
78
-
79
- if not matches:
80
- return []
81
-
82
- sections = []
83
-
84
- for i, match in enumerate(matches):
85
-
86
- start = match.start()
87
-
88
- end = (
89
- matches[i + 1].start()
90
- if i + 1 < len(matches)
91
- else len(text)
92
- )
93
-
94
- sections.append(
95
- (
96
- match.group(1),
97
- text[start:end].strip()
98
- )
99
- )
100
-
101
- return sections
102
-
103
- # =====================================================
104
- # ROMAN CLAUSES
105
- # =====================================================
106
-
107
- def parse_roman_clauses(
108
- self,
109
- text: str
110
- ) -> List[RomanClause]:
111
-
112
- romans = []
113
-
114
- roman_sections = (
115
- self.split_sections(
116
- text,
117
- self.ROMAN_RE
118
- )
119
- )
120
-
121
- for roman_no, roman_text in roman_sections:
122
-
123
- romans.append(
124
- RomanClause(
125
- document="bns",
126
- roman_no=roman_no,
127
- text=roman_text
128
- )
129
- )
130
-
131
- return romans
132
-
133
- # =====================================================
134
- # SUB CLAUSES
135
- # =====================================================
136
-
137
- ROMAN_VALUES = {
138
- "i", "ii", "iii", "iv", "v",
139
- "vi", "vii", "viii", "ix", "x",
140
- "xi", "xii", "xiii", "xiv", "xv",
141
- "xvi", "xvii", "xviii", "xix", "xx"
142
- }
143
- @staticmethod
144
- def remove_illustrations(
145
- self,
146
- text: str
147
- ):
148
-
149
- return re.split(
150
- r'Illustrations?\.',
151
- text,
152
- maxsplit=1,
153
- flags=re.I
154
- )[0]
155
-
156
- def parse_subclauses(
157
- self,
158
- text: str
159
- ) -> List[SubClause]:
160
- text=self.remove_illustrations(text)
161
- subclauses = []
162
-
163
- sub_sections = (
164
- self.split_sections(
165
- text,
166
- self.SUBCLAUSE_RE
167
- )
168
- )
169
-
170
- for sub_no, sub_text in sub_sections:
171
-
172
- # Skip roman numerals
173
- if (
174
- sub_no.lower()
175
- in self.ROMAN_VALUES
176
- ):
177
- continue
178
-
179
- sub = SubClause(
180
- document="bns",
181
- sub_clause_no=sub_no,
182
- text=sub_text
183
- )
184
-
185
- sub.roman_clauses.extend(
186
- self.parse_roman_clauses(
187
- sub_text
188
- )
189
- )
190
-
191
- subclauses.append(
192
- sub
193
- )
194
-
195
- return subclauses
196
-
197
-
198
- def build_alpha_structure(
199
- self,
200
- text: str,
201
- pattern: re.Pattern
202
- ) -> List[Clause]:
203
-
204
- clause = Clause(
205
- document="bns",
206
- clause_no="0",
207
- text=text
208
- )
209
-
210
- sections = self.split_sections(
211
- text,
212
- pattern
213
- )
214
-
215
- for sub_no, sub_text in sections:
216
-
217
- sub = SubClause(
218
- document="bns",
219
- sub_clause_no=sub_no,
220
- text=sub_text
221
- )
222
-
223
- sub.roman_clauses.extend(
224
- self.parse_roman_clauses(
225
- sub_text
226
- )
227
- )
228
-
229
- clause.sub_clauses.append(
230
- sub
231
- )
232
-
233
- return [clause]
234
-
235
- def build_numeric_structure(
236
- self,
237
- text: str
238
- ) -> List[Clause]:
239
-
240
- clauses = []
241
-
242
- clause_sections = self.split_sections(
243
- text,
244
- self.NUMERIC_RE
245
- )
246
-
247
- for clause_no, clause_text in clause_sections:
248
-
249
- clause = Clause(
250
- document="bns",
251
- clause_no=clause_no,
252
- text=clause_text
253
- )
254
-
255
- alpha_sections = self.split_sections(
256
- clause_text,
257
- self.ALPHA_RE
258
- )
259
-
260
- if alpha_sections:
261
-
262
- for sub_no, sub_text in alpha_sections:
263
-
264
- sub = SubClause(
265
- document="bns",
266
- sub_clause_no=sub_no,
267
- text=sub_text
268
- )
269
-
270
- sub.roman_clauses.extend(
271
- self.parse_roman_clauses(
272
- sub_text
273
- )
274
- )
275
-
276
- clause.sub_clauses.append(
277
- sub
278
- )
279
-
280
- clauses.append(
281
- clause
282
- )
283
-
284
- return clauses
285
- def parse_alpha_clauses(
286
- self,
287
- text: str
288
- ):
289
-
290
- return self.build_alpha_structure(
291
- text,
292
- self.ALPHA_RE
293
- )
294
-
295
-
296
- def parse_alpha_roman(
297
- self,
298
- text: str
299
- ):
300
-
301
- return self.build_alpha_structure(
302
- text,
303
- self.ALPHA_RE
304
- )
305
-
306
-
307
- def parse_capital_roman(
308
- self,
309
- text: str
310
- ):
311
-
312
- return self.build_alpha_structure(
313
- text,
314
- self.CAPITAL_RE
315
- )
316
-
317
-
318
- def parse_numeric_clauses(
319
- self,
320
- text: str
321
- ):
322
-
323
- clauses = []
324
-
325
- sections = self.split_sections(
326
- text,
327
- self.NUMERIC_RE
328
- )
329
-
330
- for clause_no, clause_text in sections:
331
-
332
- clauses.append(
333
- Clause(
334
- document="bns",
335
- clause_no=clause_no,
336
- text=clause_text
337
- )
338
- )
339
-
340
- return clauses
341
-
342
- def parse_numeric_alpha(
343
- self,
344
- text: str
345
- ):
346
-
347
- return self.build_numeric_structure(
348
- text
349
- )
350
-
351
-
352
- def parse_numeric_alpha_roman(
353
- self,
354
- text: str
355
- ):
356
-
357
- return self.build_numeric_structure(
358
- text
359
- )
360
-
361
-
362
-
363
-
364
- # =====================================================
365
- # CLAUSES
366
- # =====================================================
367
-
368
- def parse_clauses(
369
- self,
370
- section_text: str
371
- ) -> List[Clause]:
372
-
373
- clauses = []
374
-
375
- clause_sections = self.split_sections(
376
- section_text,
377
- self.CLAUSE_RE
378
- )
379
-
380
- # No numbered clauses present
381
- if not clause_sections:
382
-
383
- subclauses = self.parse_subclauses(
384
- section_text
385
- )
386
-
387
- if subclauses:
388
-
389
- synthetic_clause = Clause(
390
- document="bns",
391
- clause_no="0",
392
- text=section_text
393
- )
394
-
395
- synthetic_clause.sub_clauses.extend(
396
- subclauses
397
- )
398
-
399
- clauses.append(
400
- synthetic_clause
401
- )
402
-
403
- return clauses
404
-
405
- for clause_no, clause_text in clause_sections:
406
-
407
- clause = Clause(
408
- document="bns",
409
- clause_no=clause_no,
410
- text=clause_text
411
- )
412
-
413
- clause.sub_clauses.extend(
414
- self.parse_subclauses(
415
- clause_text
416
- )
417
- )
418
- clause.roman_clauses.extend(
419
- self.parse_roman_clauses(
420
- clause_text
421
- )
422
- )
423
-
424
- clauses.append(
425
- clause
426
- )
427
-
428
- return clauses
429
-
430
-
431
-
432
-
433
- def parse_section_structure(
434
- self,
435
- section_text: str
436
- ) -> List[Clause]:
437
-
438
- has_numeric = bool(
439
- self.NUMERIC_RE.search(
440
- section_text
441
- )
442
- )
443
-
444
- has_alpha = bool(
445
- self.ALPHA_RE.search(
446
- section_text
447
- )
448
- )
449
-
450
- has_capital = bool(
451
- self.CAPITAL_RE.search(
452
- section_text
453
- )
454
- )
455
-
456
- has_roman = bool(
457
- self.ROMAN_RE.search(
458
- section_text
459
- )
460
- )
461
-
462
- if has_numeric:
463
- return self.build_numeric_structure(
464
- section_text
465
- )
466
-
467
- if has_capital:
468
- return self.build_alpha_structure(
469
- section_text,
470
- self.CAPITAL_RE
471
- )
472
-
473
- if has_alpha:
474
- return self.build_alpha_structure(
475
- section_text,
476
- self.ALPHA_RE
477
- )
478
-
479
- return []
480
-
481
-
482
-
483
-
484
-
485
-
486
-
487
- # =====================================================
488
- # VALIDATION
489
- # =====================================================
490
-
491
- def validate_clauses(
492
- self,
493
- section_no,
494
- clauses: List[Clause]
495
- ) -> List[str]:
496
-
497
- errors = []
498
-
499
- seen = set()
500
-
501
- for clause in clauses:
502
-
503
- if clause.clause_no in seen:
504
-
505
- print("\n" + "="*80)
506
- print("section: ",section_no)
507
- print("DUPLICATE SUBCLAUSE")
508
- print("Clause:", clause.clause_no)
509
- print("SubClause:", sub.sub_clause_no)
510
- print(sub.text[:1000])
511
-
512
- errors.append(
513
- f"Duplicate SubClause "
514
- f"{sub.sub_clause_no}"
515
- )
516
-
517
- seen.add(
518
- clause.clause_no
519
- )
520
-
521
- sub_seen = set()
522
-
523
- for sub in clause.sub_clauses:
524
-
525
- if sub.sub_clause_no in sub_seen:
526
-
527
- errors.append(
528
- f"Duplicate SubClause "
529
- f"{sub.sub_clause_no}"
530
- )
531
-
532
- sub_seen.add(
533
- sub.sub_clause_no
534
- )
535
-
536
- roman_seen = set()
537
-
538
- for roman in sub.roman_clauses:
539
-
540
- if roman.roman_no in roman_seen:
541
-
542
- errors.append(
543
- f"Duplicate Roman "
544
- f"{roman.roman_no}"
545
- )
546
-
547
- roman_seen.add(
548
- roman.roman_no
549
- )
550
-
551
- return errors
552
-
553
-
554
- # =========================================================
555
- # TEST
556
- # =========================================================
557
-
558
- if __name__ == "__main__":
559
-
560
- with open(
561
- "../../pdfs/bns.txt",
562
- "r",
563
- encoding="utf8"
564
- ) as f:
565
-
566
- text = f.read()
567
-
568
- cleaner=BNSSTextCleaner()
569
- text=cleaner.clean(text)
570
-
571
-
572
-
573
- parser = ClauseParser()
574
-
575
- print(len(parser.CLAUSE_RE.findall(text)))
576
- print(len(parser.SUBCLAUSE_RE.findall(text)))
577
- print(len(parser.ROMAN_RE.findall(text)))
578
-
579
- clauses = parser.parse_section_structure(
580
- text
581
- )
582
-
583
- print(
584
- "Clauses:",
585
- len(clauses)
586
- )
587
-
588
- # for clause in clauses:
589
-
590
- # print(
591
- # clause.clause_no,
592
- # len(clause.sub_clauses)
593
- # )
594
-
595
- # print(
596
- # parser.validate_clauses(
597
- # clauses
598
- # )
599
- # )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
db/parsers/bnss/usage.py CHANGED
@@ -2,16 +2,16 @@ from collections import Counter
2
 
3
  from cleaner import BNSSTextCleaner
4
 
5
- from bnss_parser import (
6
  BNSSParser
7
  )
8
 
9
- from chunk_temp import (
10
  LegalChunker,
11
  chunks_to_dicts
12
  )
13
 
14
- from ingest import (
15
  LegalIngestionPipeline
16
  )
17
 
 
2
 
3
  from cleaner import BNSSTextCleaner
4
 
5
+ from db.parsers.bnss.bnss_parser import (
6
  BNSSParser
7
  )
8
 
9
+ from db.parsers.bnss.chunker import (
10
  LegalChunker,
11
  chunks_to_dicts
12
  )
13
 
14
+ from db.parsers.bnss.ingest import (
15
  LegalIngestionPipeline
16
  )
17
 
db/parsers/bnss_parser.py DELETED
@@ -1,221 +0,0 @@
1
- # parsers/bnss_parser.py
2
-
3
- from __future__ import annotations
4
-
5
- import re
6
-
7
- from dataclasses import dataclass, field
8
- from typing import List
9
-
10
- from parsers.base_parser import (
11
- BaseLegalParser,
12
-
13
- )
14
- from models.legal_models import (
15
- BaseNode,
16
- Chapter,
17
- Section,
18
- Clause,
19
- SubClause,
20
- RomanClause,
21
- )
22
-
23
-
24
-
25
- # ---------------------------------------------------
26
- # DATA MODELS
27
- # ---------------------------------------------------
28
-
29
- @dataclass
30
- class Section(BaseNode):
31
- section_no: str
32
- section_title: str
33
- text: str
34
-
35
- clauses: List[Clause] = field(default_factory=list)
36
-
37
-
38
- @dataclass
39
- class Chapter(BaseNode):
40
- chapter_no: str
41
- chapter_title: str
42
-
43
- sections: List[Section] = field(default_factory=list)
44
-
45
-
46
- # ---------------------------------------------------
47
- # PARSER
48
- # ---------------------------------------------------
49
-
50
- class BNSSParser(BaseLegalParser):
51
-
52
- CHAPTER_RE = re.compile(
53
- r"CHAPTER\s+([IVXLC]+)\s*\n+([^\n]+)",
54
- re.IGNORECASE
55
- )
56
-
57
- SECTION_RE = re.compile(
58
- r"(?m)^(\d{1,3}[A-Z]?)\.\s*(.+)$"
59
- )
60
-
61
- # -------------------------------------------
62
- # CHAPTER EXTRACTION
63
- # -------------------------------------------
64
-
65
- def extract_chapters(self, text):
66
-
67
- matches = list(
68
- self.CHAPTER_RE.finditer(text)
69
- )
70
-
71
- chapters = []
72
-
73
- for i, match in enumerate(matches):
74
-
75
- start = match.start()
76
-
77
- end = (
78
- matches[i + 1].start()
79
- if i + 1 < len(matches)
80
- else len(text)
81
- )
82
-
83
- chapters.append(
84
- {
85
- "chapter_no": match.group(1).strip(),
86
- "chapter_title": match.group(2).strip(),
87
- "text": text[start:end]
88
- }
89
- )
90
-
91
- return chapters
92
-
93
- # -------------------------------------------
94
- # SECTION EXTRACTION
95
- # -------------------------------------------
96
-
97
- def extract_sections(self, chapter_text):
98
-
99
- matches = list(
100
- self.SECTION_RE.finditer(chapter_text)
101
- )
102
-
103
- sections = []
104
-
105
- for i, match in enumerate(matches):
106
-
107
- start = match.start()
108
-
109
- end = (
110
- matches[i + 1].start()
111
- if i + 1 < len(matches)
112
- else len(chapter_text)
113
- )
114
-
115
- sections.append(
116
- {
117
- "section_no": match.group(1).strip(),
118
- "section_title": match.group(2).strip(),
119
- "text": chapter_text[start:end].strip()
120
- }
121
- )
122
-
123
- return sections
124
-
125
- # -------------------------------------------
126
- # MAIN PARSE
127
- # -------------------------------------------
128
-
129
- def parse(self, raw_text):
130
-
131
- raw_text = self.clean(raw_text)
132
-
133
- chapters = []
134
-
135
- for chapter_data in self.extract_chapters(raw_text):
136
-
137
- chapter = Chapter(
138
- document="bnss",
139
- chapter_no=chapter_data["chapter_no"],
140
- chapter_title=chapter_data["chapter_title"]
141
- )
142
-
143
- for section_data in self.extract_sections(
144
- chapter_data["text"]
145
- ):
146
-
147
- section = Section(
148
- document="bnss",
149
- section_no=section_data["section_no"],
150
- section_title=section_data["section_title"],
151
- text=section_data["text"]
152
- )
153
-
154
- # -----------------------------
155
- # Clauses
156
- # -----------------------------
157
-
158
- clause_data = self.extract_chapters(
159
- section.text
160
- )
161
-
162
- for c in clause_data:
163
-
164
- clause = Clause(
165
- clause_no=c["clause_no"],
166
- text=c["text"]
167
- )
168
-
169
- # -------------------------
170
- # Sub Clauses
171
- # -------------------------
172
-
173
- sub_clause_data = (
174
- self.extract_subclauses(
175
- clause.text
176
- )
177
- )
178
-
179
- for s in sub_clause_data:
180
-
181
- sub_clause = SubClause(
182
- document="bnss",
183
- sub_clause_no=s["sub_clause_no"],
184
- text=s["text"]
185
- )
186
-
187
- # ---------------------
188
- # Roman Clauses
189
- # ---------------------
190
-
191
- roman_data = (
192
- self.extract_roman_clauses(
193
- sub_clause.text
194
- )
195
- )
196
-
197
- for r in roman_data:
198
-
199
- sub_clause.roman_clauses.append(
200
- RomanClause(
201
- document="bnss",
202
- roman_no=r["roman_no"],
203
- text=r["text"]
204
- )
205
- )
206
-
207
- clause.sub_clauses.append(
208
- sub_clause
209
- )
210
-
211
- section.clauses.append(
212
- clause
213
- )
214
-
215
- chapter.sections.append(
216
- section
217
- )
218
-
219
- chapters.append(chapter)
220
-
221
- return chapters
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
db/parsers/bsa/bsa_parser.py CHANGED
@@ -1,35 +1,35 @@
1
  from __future__ import annotations
2
 
3
- from cleaner import BSATextCleaner
4
- from legal_models import (
5
  LegalDocument
6
  )
7
 
8
- from part_parser import (
9
  PartParser
10
  )
11
 
12
- from chapter_parser import (
13
  ChapterParser
14
  )
15
 
16
- from section_parser import (
17
  SectionParser
18
  )
19
 
20
- from clause_parser import (
21
  ClauseParser
22
  )
23
 
24
- from explaination_parser import (
25
  ExplanationParser
26
  )
27
 
28
- from illustration_parser import (
29
  IllustrationParser
30
  )
31
 
32
- from reference_parser import (
33
  ReferenceExtractor
34
  )
35
 
 
1
  from __future__ import annotations
2
 
3
+ from db.parsers.bsa.cleaner import BSATextCleaner
4
+ from db.parsers.bsa.legal_models import (
5
  LegalDocument
6
  )
7
 
8
+ from db.parsers.bsa.part_parser import (
9
  PartParser
10
  )
11
 
12
+ from db.parsers.bsa.chapter_parser import (
13
  ChapterParser
14
  )
15
 
16
+ from db.parsers.bsa.section_parser import (
17
  SectionParser
18
  )
19
 
20
+ from db.parsers.bsa.clause_parser import (
21
  ClauseParser
22
  )
23
 
24
+ from db.parsers.bsa.explaination_parser import (
25
  ExplanationParser
26
  )
27
 
28
+ from db.parsers.bsa.illustration_parser import (
29
  IllustrationParser
30
  )
31
 
32
+ from db.parsers.bsa.reference_parser import (
33
  ReferenceExtractor
34
  )
35
 
db/parsers/bsa/chapter_parser.py CHANGED
@@ -4,7 +4,7 @@ from __future__ import annotations
4
  import re
5
  from typing import List, Dict
6
 
7
- from legal_models import Chapter
8
 
9
 
10
  class ChapterParser:
 
4
  import re
5
  from typing import List, Dict
6
 
7
+ from db.parsers.bsa.legal_models import Chapter
8
 
9
 
10
  class ChapterParser:
db/parsers/bsa/clause_parser.py CHANGED
@@ -3,7 +3,7 @@ from __future__ import annotations
3
  import re
4
  from typing import List
5
 
6
- from legal_models import (
7
  Clause,
8
  SubClause,
9
  RomanClause
 
3
  import re
4
  from typing import List
5
 
6
+ from db.parsers.bsa.legal_models import (
7
  Clause,
8
  SubClause,
9
  RomanClause
db/parsers/bsa/explaination_parser.py CHANGED
@@ -3,7 +3,7 @@ from __future__ import annotations
3
  import re
4
  from typing import List, Optional
5
 
6
- from legal_models import Explanation
7
 
8
 
9
  class ExplanationParser:
 
3
  import re
4
  from typing import List, Optional
5
 
6
+ from db.parsers.bsa.legal_models import Explanation
7
 
8
 
9
  class ExplanationParser:
db/parsers/bsa/illustration_parser.py CHANGED
@@ -3,7 +3,7 @@ from __future__ import annotations
3
  import re
4
  from typing import List
5
 
6
- from legal_models import Illustration
7
 
8
 
9
  class IllustrationParser:
 
3
  import re
4
  from typing import List
5
 
6
+ from db.parsers.bsa.legal_models import Illustration
7
 
8
 
9
  class IllustrationParser:
db/parsers/bsa/ingest.py CHANGED
@@ -6,11 +6,11 @@ from qdrant_client.models import (
6
  PointStruct
7
  )
8
 
9
- from embedder import (
10
  LegalEmbedder
11
  )
12
 
13
- from qdrant_store import (
14
  QdrantStore
15
  )
16
 
 
6
  PointStruct
7
  )
8
 
9
+ from db.parsers.bsa.embedder import (
10
  LegalEmbedder
11
  )
12
 
13
+ from db.parsers.bsa.qdrant_store import (
14
  QdrantStore
15
  )
16
 
db/parsers/bsa/part_parser.py CHANGED
@@ -3,7 +3,7 @@ from __future__ import annotations
3
  import re
4
  from typing import List
5
 
6
- from legal_models import Part
7
 
8
 
9
  class PartParser:
 
3
  import re
4
  from typing import List
5
 
6
+ from db.parsers.bsa.legal_models import Part
7
 
8
 
9
  class PartParser:
db/parsers/bsa/section_parser.py CHANGED
@@ -3,8 +3,8 @@ from __future__ import annotations
3
  import re
4
  from typing import List
5
 
6
- from cleaner import BSATextCleaner
7
- from legal_models import Section
8
 
9
 
10
  class SectionParser:
 
3
  import re
4
  from typing import List
5
 
6
+ from db.parsers.bsa.cleaner import BSATextCleaner
7
+ from db.parsers.bsa.legal_models import Section
8
 
9
 
10
  class SectionParser:
db/parsers/bsa/usage.py CHANGED
@@ -2,16 +2,16 @@ from collections import Counter
2
 
3
  from cleaner import BSATextCleaner
4
 
5
- from bsa_parser import (
6
  BSAParser
7
  )
8
 
9
- from chunker import (
10
  LegalChunker,
11
  chunks_to_dicts
12
  )
13
 
14
- from ingest import (
15
  LegalIngestionPipeline
16
  )
17
 
 
2
 
3
  from cleaner import BSATextCleaner
4
 
5
+ from db.parsers.bsa.bsa_parser import (
6
  BSAParser
7
  )
8
 
9
+ from db.parsers.bsa.chunker import (
10
  LegalChunker,
11
  chunks_to_dicts
12
  )
13
 
14
+ from db.parsers.bsa.ingest import (
15
  LegalIngestionPipeline
16
  )
17
 
db/parsers/bsa_parser.py DELETED
@@ -1,387 +0,0 @@
1
- from __future__ import annotations
2
-
3
- import re
4
-
5
- from parsers.base_parser import BaseLegalParser
6
-
7
- from models.legal_models import (
8
- LegalDocument,
9
- Division,
10
- Provision,
11
- Clause,
12
- SubClause,
13
- RomanClause
14
- )
15
-
16
-
17
- class BSAParser(BaseLegalParser):
18
-
19
- # =====================================================
20
- # REGEX
21
- # =====================================================
22
-
23
- PART_RE = re.compile(
24
- r"(?m)^PART\s+([IVXLC]+)"
25
- )
26
-
27
- CHAPTER_RE = re.compile(
28
- r"(?m)^CHAPTER\s+([IVXLC]+)\s*\n+([^\n]+)"
29
- )
30
-
31
- SECTION_RE = re.compile(
32
- r"(?m)^(\d+[A-Z]?)\.\s*(.+)"
33
- )
34
-
35
- CLAUSE_RE = re.compile(
36
- r"(?m)^\((\d+)\)\s*(.*)"
37
- )
38
-
39
- SUBCLAUSE_RE = re.compile(
40
- r"(?m)^\(([a-z])\)\s*(.*)"
41
- )
42
-
43
- ROMAN_RE = re.compile(
44
- r"(?m)^\((i|ii|iii|iv|v|vi|vii|viii|ix|x)\)\s*(.*)",
45
- re.IGNORECASE
46
- )
47
-
48
- # =====================================================
49
- # CLEAN
50
- # =====================================================
51
-
52
- def clean(self, text):
53
-
54
- text = super().clean(text)
55
-
56
- text = re.sub(
57
- r"THE GAZETTE OF INDIA EXTRAORDINARY.*",
58
- "",
59
- text
60
- )
61
-
62
- text = re.sub(
63
- r"_{5,}",
64
- "",
65
- text
66
- )
67
-
68
- return text
69
-
70
- # =====================================================
71
- # PARTS
72
- # =====================================================
73
-
74
- def extract_parts(self, text):
75
-
76
- matches = list(
77
- self.PART_RE.finditer(text)
78
- )
79
-
80
- results = []
81
-
82
- for i, match in enumerate(matches):
83
-
84
- start = match.start()
85
-
86
- end = (
87
- matches[i + 1].start()
88
- if i + 1 < len(matches)
89
- else len(text)
90
- )
91
-
92
- results.append(
93
- {
94
- "part_no":
95
- match.group(1),
96
-
97
- "text":
98
- text[start:end]
99
- }
100
- )
101
-
102
- return results
103
-
104
- # =====================================================
105
- # CHAPTERS
106
- # =====================================================
107
-
108
- def extract_chapters(self, text):
109
-
110
- matches = list(
111
- self.CHAPTER_RE.finditer(text)
112
- )
113
-
114
- results = []
115
-
116
- for i, match in enumerate(matches):
117
-
118
- start = match.start()
119
-
120
- end = (
121
- matches[i + 1].start()
122
- if i + 1 < len(matches)
123
- else len(text)
124
- )
125
-
126
- results.append(
127
- {
128
- "chapter_no":
129
- match.group(1),
130
-
131
- "chapter_title":
132
- match.group(2).strip(),
133
-
134
- "text":
135
- text[start:end]
136
- }
137
- )
138
-
139
- return results
140
-
141
- # =====================================================
142
- # SECTIONS
143
- # =====================================================
144
-
145
- def extract_sections(self, text):
146
-
147
- matches = list(
148
- self.SECTION_RE.finditer(text)
149
- )
150
-
151
- results = []
152
-
153
- for i, match in enumerate(matches):
154
-
155
- start = match.start()
156
-
157
- end = (
158
- matches[i + 1].start()
159
- if i + 1 < len(matches)
160
- else len(text)
161
- )
162
-
163
- section_no = match.group(1)
164
-
165
- title = match.group(2).strip()
166
-
167
- results.append(
168
- {
169
- "section_no":
170
- section_no,
171
-
172
- "section_title":
173
- title,
174
-
175
- "text":
176
- text[start:end]
177
- }
178
- )
179
-
180
- return results
181
-
182
- # =====================================================
183
- # CLAUSES
184
- # =====================================================
185
-
186
- def extract_clauses(self, text):
187
-
188
- matches = list(
189
- self.CLAUSE_RE.finditer(text)
190
- )
191
-
192
- results = []
193
-
194
- for i, match in enumerate(matches):
195
-
196
- start = match.start()
197
-
198
- end = (
199
- matches[i + 1].start()
200
- if i + 1 < len(matches)
201
- else len(text)
202
- )
203
-
204
- results.append(
205
- {
206
- "clause_no":
207
- match.group(1),
208
-
209
- "text":
210
- text[start:end].strip()
211
- }
212
- )
213
-
214
- return results
215
-
216
- # =====================================================
217
- # SUB CLAUSES
218
- # =====================================================
219
-
220
- def extract_subclauses(self, text):
221
-
222
- matches = list(
223
- self.SUBCLAUSE_RE.finditer(text)
224
- )
225
-
226
- results = []
227
-
228
- for i, match in enumerate(matches):
229
-
230
- start = match.start()
231
-
232
- end = (
233
- matches[i + 1].start()
234
- if i + 1 < len(matches)
235
- else len(text)
236
- )
237
-
238
- results.append(
239
- {
240
- "sub_clause_no":
241
- match.group(1),
242
-
243
- "text":
244
- text[start:end].strip()
245
- }
246
- )
247
-
248
- return results
249
-
250
- # =====================================================
251
- # ROMAN CLAUSES
252
- # =====================================================
253
-
254
- def extract_roman_clauses(self, text):
255
-
256
- matches = list(
257
- self.ROMAN_RE.finditer(text)
258
- )
259
-
260
- results = []
261
-
262
- for i, match in enumerate(matches):
263
-
264
- start = match.start()
265
-
266
- end = (
267
- matches[i + 1].start()
268
- if i + 1 < len(matches)
269
- else len(text)
270
- )
271
-
272
- results.append(
273
- {
274
- "roman_no":
275
- match.group(1),
276
-
277
- "text":
278
- text[start:end].strip()
279
- }
280
- )
281
-
282
- return results
283
-
284
- # =====================================================
285
- # MAIN PARSER
286
- # =====================================================
287
-
288
- def parse(self, raw_text):
289
-
290
- raw_text = self.clean(raw_text)
291
-
292
- divisions = []
293
-
294
- for part_data in self.extract_parts(raw_text):
295
-
296
- part_no = part_data["part_no"]
297
-
298
- for chapter_data in self.extract_chapters(
299
- part_data["text"]
300
- ):
301
-
302
- division = Division(
303
- document="bsa",
304
- division_no=chapter_data["chapter_no"],
305
- title=chapter_data["chapter_title"]
306
- )
307
-
308
- for section_data in self.extract_sections(
309
- chapter_data["text"]
310
- ):
311
-
312
- provision = Provision(
313
- document="bsa",
314
- provision_no=section_data["section_no"],
315
- title=section_data["section_title"],
316
- text=section_data["text"]
317
- )
318
-
319
- # ---------------------
320
- # Clauses
321
- # ---------------------
322
-
323
- for clause_data in self.extract_clauses(
324
- provision.text
325
- ):
326
-
327
- clause = Clause(
328
- document="bsa",
329
- clause_no=clause_data["clause_no"],
330
- text=clause_data["text"]
331
- )
332
-
333
- # -----------------
334
- # Sub Clauses
335
- # -----------------
336
-
337
- for sub_data in self.extract_subclauses(
338
- clause.text
339
- ):
340
-
341
- sub = SubClause(
342
- document="bsa",
343
- sub_clause_no=sub_data["sub_clause_no"],
344
- text=sub_data["text"]
345
- )
346
-
347
- # -------------
348
- # Roman
349
- # -------------
350
-
351
- for roman_data in self.extract_roman_clauses(
352
- sub.text
353
- ):
354
-
355
- sub.roman_clauses.append(
356
- RomanClause(
357
- document="bsa",
358
- roman_no=roman_data["roman_no"],
359
- text=roman_data["text"]
360
- )
361
- )
362
-
363
- clause.sub_clauses.append(sub)
364
-
365
- provision.clauses.append(clause)
366
-
367
- provision.references = (
368
- self.extract_references(
369
- provision.text
370
- )
371
- )
372
-
373
- division.provisions.append(
374
- provision
375
- )
376
-
377
- division.part_no = part_no
378
-
379
- divisions.append(
380
- division
381
- )
382
-
383
- return LegalDocument(
384
- document="bsa",
385
- document_type="bsa",
386
- divisions=divisions
387
- )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
db/{chunker/constitution_chunker.py → parsers/consitution/chunker.py} RENAMED
@@ -3,7 +3,7 @@ from __future__ import annotations
3
 
4
  from dataclasses import dataclass, field
5
  from typing import List, Dict, Any
6
- from parsers.consitution.constitution_parser import ConstitutionParser
7
 
8
  # =========================================================
9
  # LEGAL CHUNK
@@ -407,26 +407,133 @@ class LegalChunker:
407
 
408
 
409
 
410
- if __name__ == "__main__":
 
 
411
 
412
- with open(
413
- "../../pdfs/constitution.txt",
414
- "r",
415
- encoding="utf8"
416
- ) as f:
417
 
418
- text = f.read()
 
 
419
 
420
- parser = ConstitutionParser()
 
 
 
421
 
422
- constitution = parser.parse(
423
- text
424
- )
425
- chunker=LegalChunker()
426
- chunks=chunker.chunk_constitution(constitution)
427
- print(len(chunks))
428
- print(
429
- chunker.stats(chunks)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
430
  )
431
 
432
-
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3
 
4
  from dataclasses import dataclass, field
5
  from typing import List, Dict, Any
6
+ from db.parsers.consitution.constitution_parser import ConstitutionParser
7
 
8
  # =========================================================
9
  # LEGAL CHUNK
 
407
 
408
 
409
 
410
+ def build_enriched_text(
411
+ chunk: LegalChunk
412
+ ) -> str:
413
 
414
+ m = chunk.metadata
 
 
 
 
415
 
416
+ parts = [
417
+ "Constitution of India"
418
+ ]
419
 
420
+ if m.get("part_no"):
421
+ parts.append(
422
+ f"Part {m['part_no']}"
423
+ )
424
 
425
+ if m.get("chapter_no"):
426
+ parts.append(
427
+ f"Chapter {m['chapter_no']}"
428
+ )
429
+
430
+ if m.get("article_no"):
431
+ parts.append(
432
+ f"Article {m['article_no']}"
433
+ )
434
+
435
+ if m.get("clause_no"):
436
+ parts.append(
437
+ f"Clause ({m['clause_no']})"
438
+ )
439
+
440
+ if m.get("sub_clause_no"):
441
+ parts.append(
442
+ f"SubClause ({m['sub_clause_no']})"
443
+ )
444
+
445
+ if m.get("roman_no"):
446
+ parts.append(
447
+ f"Roman ({m['roman_no']})"
448
+ )
449
+
450
+ parts.append("")
451
+ parts.append(
452
+ chunk.text
453
  )
454
 
455
+ return "\n".join(parts)
456
+
457
+
458
+
459
+
460
+ def chunks_to_dicts(
461
+ chunks: List[LegalChunk]
462
+ ) -> List[dict]:
463
+
464
+ result = []
465
+
466
+ for chunk in chunks:
467
+
468
+ metadata = chunk.metadata
469
+
470
+ result.append(
471
+ {
472
+ "chunk_id":
473
+ chunk.chunk_id,
474
+
475
+ "level":
476
+ chunk.chunk_type,
477
+
478
+ "document":
479
+ "constitution",
480
+
481
+ "part_no":
482
+ metadata.get(
483
+ "part_no"
484
+ ),
485
+
486
+ "part_title":
487
+ metadata.get(
488
+ "part_title"
489
+ ),
490
+
491
+ "chapter_no":
492
+ metadata.get(
493
+ "chapter_no"
494
+ ),
495
+
496
+ "chapter_title":
497
+ metadata.get(
498
+ "chapter_title"
499
+ ),
500
+
501
+ "article_no":
502
+ metadata.get(
503
+ "article_no"
504
+ ),
505
+
506
+ "article_title":
507
+ metadata.get(
508
+ "article_title"
509
+ ),
510
+
511
+ "clause_no":
512
+ metadata.get(
513
+ "clause_no"
514
+ ),
515
+
516
+ "sub_clause_no":
517
+ metadata.get(
518
+ "sub_clause_no"
519
+ ),
520
+
521
+ "roman_no":
522
+ metadata.get(
523
+ "roman_no"
524
+ ),
525
+
526
+ "text":
527
+ chunk.text,
528
+
529
+ "references":
530
+ chunk.references,
531
+
532
+ "enriched_text":
533
+ build_enriched_text(
534
+ chunk
535
+ )
536
+ }
537
+ )
538
+
539
+ return result
db/parsers/consitution/chunker_test.py DELETED
@@ -1,432 +0,0 @@
1
-
2
- from __future__ import annotations
3
-
4
- from dataclasses import dataclass, field
5
- from typing import List, Dict, Any
6
- from constitution_parser import ConstitutionParser
7
-
8
- # =========================================================
9
- # LEGAL CHUNK
10
- # =========================================================
11
-
12
- @dataclass
13
- class LegalChunk:
14
-
15
- chunk_id: str
16
-
17
- chunk_type: str
18
-
19
- text: str
20
-
21
- metadata: Dict[str, Any] = field(
22
- default_factory=dict
23
- )
24
-
25
- references: List[str] = field(
26
- default_factory=list
27
- )
28
-
29
-
30
- # =========================================================
31
- # LEGAL CHUNKER
32
- # =========================================================
33
-
34
- class LegalChunker:
35
-
36
- """
37
- Creates chunks at:
38
-
39
- Article
40
- Clause
41
- Proviso
42
- Explanation
43
- Schedule
44
-
45
- for RAG retrieval.
46
- """
47
-
48
- # =====================================================
49
- # MAIN
50
- # =====================================================
51
-
52
- def chunk_constitution(
53
- self,
54
- constitution
55
- ) -> List[LegalChunk]:
56
-
57
- chunks = []
58
-
59
- # -------------------------------------
60
- # PARTS
61
- # -------------------------------------
62
-
63
- for part in constitution.parts:
64
-
65
- part_meta = {
66
- "part_no":
67
- part.part_no,
68
-
69
- "part_title":
70
- part.part_title
71
- }
72
-
73
- # -------------------------
74
- # Articles directly under part
75
- # -------------------------
76
-
77
- for article in getattr(
78
- part,
79
- "articles",
80
- []
81
- ):
82
-
83
- chunks.extend(
84
- self.chunk_article(
85
- article,
86
- part_meta,
87
- {}
88
- )
89
- )
90
-
91
- # -------------------------
92
- # Chapters
93
- # -------------------------
94
-
95
- for chapter in getattr(
96
- part,
97
- "chapters",
98
- []
99
- ):
100
-
101
- chapter_meta = {
102
- "chapter_no":
103
- chapter.chapter_no,
104
-
105
- "chapter_title":
106
- chapter.chapter_title
107
- }
108
-
109
- for article in getattr(
110
- chapter,
111
- "articles",
112
- []
113
- ):
114
-
115
- chunks.extend(
116
- self.chunk_article(
117
- article,
118
- part_meta,
119
- chapter_meta
120
- )
121
- )
122
-
123
- # -------------------------------------
124
- # SCHEDULES
125
- # -------------------------------------
126
-
127
- for schedule in getattr(
128
- constitution,
129
- "schedules",
130
- []
131
- ):
132
-
133
- chunks.append(
134
- LegalChunk(
135
- chunk_id=
136
- f"schedule_"
137
- f"{schedule.schedule_no}",
138
-
139
- chunk_type=
140
- "schedule",
141
-
142
- text=
143
- schedule.text,
144
-
145
- metadata={
146
- "schedule_no":
147
- schedule.schedule_no,
148
-
149
- "schedule_title":
150
- schedule.schedule_title
151
- }
152
- )
153
- )
154
-
155
- return chunks
156
-
157
- # =====================================================
158
- # ARTICLE
159
- # =====================================================
160
-
161
- def chunk_article(
162
- self,
163
- article,
164
- part_meta,
165
- chapter_meta
166
- ) -> List[LegalChunk]:
167
-
168
- chunks = []
169
-
170
- base_meta = {
171
- **part_meta,
172
- **chapter_meta,
173
-
174
- "article_no":
175
- article.article_no,
176
-
177
- "article_title":
178
- article.article_title
179
- }
180
-
181
- # -------------------------------------
182
- # FULL ARTICLE
183
- # -------------------------------------
184
-
185
- chunks.append(
186
- LegalChunk(
187
- chunk_id=
188
- f"article_"
189
- f"{article.article_no}",
190
-
191
- chunk_type=
192
- "article",
193
-
194
- text=
195
- article.text,
196
-
197
- metadata=
198
- base_meta,
199
-
200
- references=[
201
- str(r)
202
- for r in getattr(
203
- article,
204
- "references",
205
- []
206
- )
207
- ]
208
- )
209
- )
210
-
211
- # -------------------------------------
212
- # CLAUSES
213
- # -------------------------------------
214
-
215
- for clause in getattr(
216
- article,
217
- "clauses",
218
- []
219
- ):
220
-
221
- clause_meta = {
222
- **base_meta,
223
-
224
- "clause_no":
225
- clause.clause_no
226
- }
227
-
228
- chunks.append(
229
- LegalChunk(
230
- chunk_id=
231
- f"article_"
232
- f"{article.article_no}"
233
- f"_clause_"
234
- f"{clause.clause_no}",
235
-
236
- chunk_type=
237
- "clause",
238
-
239
- text=
240
- clause.text,
241
-
242
- metadata=
243
- clause_meta
244
- )
245
- )
246
-
247
- # -----------------------------
248
- # SUB CLAUSES
249
- # -----------------------------
250
-
251
- for sub in getattr(
252
- clause,
253
- "sub_clauses",
254
- []
255
- ):
256
-
257
- sub_meta = {
258
- **clause_meta,
259
-
260
- "sub_clause_no":
261
- sub.sub_clause_no
262
- }
263
-
264
- chunks.append(
265
- LegalChunk(
266
- chunk_id=
267
- f"article_"
268
- f"{article.article_no}"
269
- f"_clause_"
270
- f"{clause.clause_no}"
271
- f"_sub_"
272
- f"{sub.sub_clause_no}",
273
-
274
- chunk_type=
275
- "sub_clause",
276
-
277
- text=
278
- sub.text,
279
-
280
- metadata=
281
- sub_meta
282
- )
283
- )
284
-
285
- # -------------------------
286
- # Roman Clauses
287
- # -------------------------
288
-
289
- for roman in getattr(
290
- sub,
291
- "roman_clauses",
292
- []
293
- ):
294
-
295
- chunks.append(
296
- LegalChunk(
297
- chunk_id=
298
- f"article_"
299
- f"{article.article_no}"
300
- f"_roman_"
301
- f"{roman.roman_no}",
302
-
303
- chunk_type=
304
- "roman_clause",
305
-
306
- text=
307
- roman.text,
308
-
309
- metadata={
310
- **sub_meta,
311
-
312
- "roman_no":
313
- roman.roman_no
314
- }
315
- )
316
- )
317
-
318
- # -------------------------------------
319
- # PROVISOS
320
- # -------------------------------------
321
-
322
- for idx, proviso in enumerate(
323
- getattr(
324
- article,
325
- "provisos",
326
- []
327
- ),
328
- start=1
329
- ):
330
-
331
- chunks.append(
332
- LegalChunk(
333
- chunk_id=
334
- f"article_"
335
- f"{article.article_no}"
336
- f"_proviso_"
337
- f"{idx}",
338
-
339
- chunk_type=
340
- "proviso",
341
-
342
- text=
343
- proviso.text,
344
-
345
- metadata=
346
- base_meta
347
- )
348
- )
349
-
350
- # -------------------------------------
351
- # EXPLANATIONS
352
- # -------------------------------------
353
-
354
- for idx, explanation in enumerate(
355
- getattr(
356
- article,
357
- "explanations",
358
- []
359
- ),
360
- start=1
361
- ):
362
-
363
- chunks.append(
364
- LegalChunk(
365
- chunk_id=
366
- f"article_"
367
- f"{article.article_no}"
368
- f"_explanation_"
369
- f"{idx}",
370
-
371
- chunk_type=
372
- "explanation",
373
-
374
- text=
375
- explanation.text,
376
-
377
- metadata=
378
- base_meta
379
- )
380
- )
381
-
382
- return chunks
383
-
384
- # =====================================================
385
- # STATS
386
- # =====================================================
387
-
388
- def stats(
389
- self,
390
- chunks: List[LegalChunk]
391
- ):
392
-
393
- result = {}
394
-
395
- for chunk in chunks:
396
-
397
- result.setdefault(
398
- chunk.chunk_type,
399
- 0
400
- )
401
-
402
- result[
403
- chunk.chunk_type
404
- ] += 1
405
-
406
- return result
407
-
408
-
409
-
410
- if __name__ == "__main__":
411
-
412
- with open(
413
- "../../pdfs/constitution.txt",
414
- "r",
415
- encoding="utf8"
416
- ) as f:
417
-
418
- text = f.read()
419
-
420
- parser = ConstitutionParser()
421
-
422
- constitution = parser.parse(
423
- text
424
- )
425
- chunker=LegalChunker()
426
- chunks=chunker.chunk_constitution(constitution)
427
- print(len(chunks))
428
- print(
429
- chunker.stats(chunks)
430
- )
431
-
432
-
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
db/parsers/consitution/clause_parser.py CHANGED
@@ -4,7 +4,7 @@ from __future__ import annotations
4
  import re
5
  from typing import List
6
 
7
- from legal_models import (
8
  Clause,
9
  SubClause,
10
  RomanClause
 
4
  import re
5
  from typing import List
6
 
7
+ from db.parsers.consitution.legal_models import (
8
  Clause,
9
  SubClause,
10
  RomanClause
db/parsers/consitution/cleaner.py ADDED
@@ -0,0 +1,61 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import re
2
+
3
+ class ConstitutionTextCleaner:
4
+ def clean(
5
+ self,
6
+ text: str
7
+ ) -> str:
8
+
9
+ text = text.replace(
10
+ "\r\n",
11
+ "\n"
12
+ )
13
+
14
+ text = re.sub(
15
+ r"\r",
16
+ "\n",
17
+ text
18
+ )
19
+
20
+ # remove page numbers
21
+
22
+ text = re.sub(
23
+ r"(?m)^\s*\d+\s*$",
24
+ "",
25
+ text
26
+ )
27
+
28
+ # remove repeated spaces
29
+
30
+ text = re.sub(
31
+ r"[ \t]+",
32
+ " ",
33
+ text
34
+ )
35
+
36
+ # remove constitution headers
37
+
38
+ text = re.sub(
39
+ r"THE CONSTITUTION OF INDIA",
40
+ "",
41
+ text,
42
+ flags=re.I
43
+ )
44
+
45
+ # remove separators
46
+
47
+ text = re.sub(
48
+ r"_{3,}",
49
+ "",
50
+ text
51
+ )
52
+
53
+ # collapse blank lines
54
+
55
+ text = re.sub(
56
+ r"\n{3,}",
57
+ "\n\n",
58
+ text
59
+ )
60
+
61
+ return text.strip()
db/parsers/consitution/constitution_parser.py CHANGED
@@ -3,7 +3,7 @@ from __future__ import annotations
3
 
4
  import re
5
 
6
- from legal_models import (
7
  ConstitutionDocument,
8
  Preamble,
9
  Part,
@@ -11,14 +11,14 @@ from legal_models import (
11
  Article
12
  )
13
 
14
- from part_parser import PartParser
15
- from chapter_parser import ChapterParser
16
- from article_parser import ArticleParser
17
- from clause_parser import ClauseParser
18
- from proviso_parser import ProvisoParser
19
- from explanation_parser import ExplanationParser
20
- from schedule_parser import ScheduleParser
21
- from reference_extractor import ReferenceExtractor
22
 
23
 
24
  class ConstitutionParser:
 
3
 
4
  import re
5
 
6
+ from db.parsers.consitution.legal_models import (
7
  ConstitutionDocument,
8
  Preamble,
9
  Part,
 
11
  Article
12
  )
13
 
14
+ from db.parsers.consitution.part_parser import PartParser
15
+ from db.parsers.consitution.chapter_parser import ChapterParser
16
+ from db.parsers.consitution.article_parser import ArticleParser
17
+ from db.parsers.consitution.clause_parser import ClauseParser
18
+ from db.parsers.consitution.proviso_parser import ProvisoParser
19
+ from db.parsers.consitution.explanation_parser import ExplanationParser
20
+ from db.parsers.consitution.schedule_parser import ScheduleParser
21
+ from db.parsers.consitution.reference_extractor import ReferenceExtractor
22
 
23
 
24
  class ConstitutionParser:
db/parsers/consitution/embeddings_test.py CHANGED
@@ -174,7 +174,7 @@ class EmbeddingGenerator:
174
  if __name__ == "__main__":
175
 
176
  from constitution_parser import ConstitutionParser
177
- from chunker_test import LegalChunker
178
 
179
  with open(
180
  "../../pdfs/constitution.txt",
 
174
  if __name__ == "__main__":
175
 
176
  from constitution_parser import ConstitutionParser
177
+ from db.parsers.consitution.chunker import LegalChunker
178
 
179
  with open(
180
  "../../pdfs/constitution.txt",
db/parsers/consitution/explanation_parser.py CHANGED
@@ -4,7 +4,7 @@ from __future__ import annotations
4
  import re
5
  from typing import List
6
 
7
- from legal_models import Explanation
8
 
9
 
10
  class ExplanationParser:
 
4
  import re
5
  from typing import List
6
 
7
+ from db.parsers.consitution.legal_models import Explanation
8
 
9
 
10
  class ExplanationParser:
db/parsers/consitution/proviso_parser.py CHANGED
@@ -4,7 +4,7 @@ from __future__ import annotations
4
  import re
5
  from typing import List
6
 
7
- from legal_models import Proviso
8
 
9
 
10
  class ProvisoParser:
 
4
  import re
5
  from typing import List
6
 
7
+ from db.parsers.consitution.legal_models import Proviso
8
 
9
 
10
  class ProvisoParser:
db/parsers/consitution/reference_extractor.py CHANGED
@@ -4,7 +4,7 @@ from __future__ import annotations
4
  import re
5
  from typing import List
6
 
7
- from legal_models import Reference
8
 
9
 
10
  class ReferenceExtractor:
 
4
  import re
5
  from typing import List
6
 
7
+ from db.parsers.consitution.legal_models import Reference
8
 
9
 
10
  class ReferenceExtractor:
db/parsers/consitution/schedule_parser.py CHANGED
@@ -4,7 +4,7 @@ from __future__ import annotations
4
  import re
5
  from typing import List, Dict
6
 
7
- from legal_models import (
8
  Schedule,
9
  ScheduleParagraph
10
  )
 
4
  import re
5
  from typing import List, Dict
6
 
7
+ from db.parsers.consitution.legal_models import (
8
  Schedule,
9
  ScheduleParagraph
10
  )
db/parsers/consitution/usage.py CHANGED
@@ -1,12 +1,12 @@
1
- from embeddings_test import (
2
  EmbeddingGenerator
3
  )
4
 
5
- from qdrant_store import (
6
  QdrantStore
7
  )
8
- from constitution_parser import ConstitutionParser
9
- from chunker_test import LegalChunker
10
 
11
 
12
  with open(
 
1
+ from db.parsers.consitution.embeddings_test import (
2
  EmbeddingGenerator
3
  )
4
 
5
+ from db.parsers.consitution.qdrant_store import (
6
  QdrantStore
7
  )
8
+ from db.parsers.consitution.constitution_parser import ConstitutionParser
9
+ from db.parsers.consitution.chunker import LegalChunker
10
 
11
 
12
  with open(
db/parsers/constitution_parser.py DELETED
@@ -1,1823 +0,0 @@
1
- # from __future__ import annotations
2
-
3
- # import re
4
- # from dataclasses import dataclass, field, asdict
5
- # from typing import List
6
- # from models.legal_models import Article, Part, Explanation, Proviso, RomanClause, SubClause, Clause
7
-
8
- # # ==========================================================
9
- # # DATA MODELS
10
- # # ==========================================================
11
-
12
- # @dataclass
13
- # class Reference:
14
- # article_no: str
15
-
16
-
17
- # # @dataclass
18
- # # class Explanation:
19
- # # text: str
20
-
21
-
22
- # # @dataclass
23
- # # class Proviso:
24
- # # text: str
25
-
26
-
27
- # # @dataclass
28
- # # class RomanClause:
29
- # # roman_no: str
30
- # # text: str
31
-
32
-
33
- # # @dataclass
34
- # # class SubClause:
35
- # # sub_clause_no: str
36
- # # text: str
37
- # # roman_clauses: List[RomanClause] = field(default_factory=list)
38
-
39
-
40
- # # @dataclass
41
- # # class Clause:
42
- # # clause_no: str
43
- # # text: str
44
- # # sub_clauses: List[SubClause] = field(default_factory=list)
45
-
46
-
47
- # # @dataclass
48
- # # class Article:
49
- # # article_no: str
50
- # # article_title: str
51
- # # text: str
52
-
53
- # # clauses: List[Clause] = field(default_factory=list)
54
- # # provisos: List[Proviso] = field(default_factory=list)
55
- # # explanations: List[Explanation] = field(default_factory=list)
56
- # # references: List[Reference] = field(default_factory=list)
57
-
58
-
59
- # # @dataclass
60
- # # class Part:
61
- # # part_no: str
62
- # # part_title: str
63
- # # articles: List[Article] = field(default_factory=list)
64
-
65
-
66
- # # ==========================================================
67
- # # PARSER
68
- # # ==========================================================
69
-
70
- # class ConstitutionParser:
71
-
72
- # PART_RE = re.compile(
73
- # r"PART\s+([IVXLC]+)\s*\n+([^\n]+)",
74
- # re.IGNORECASE
75
- # )
76
-
77
- # ARTICLE_RE = re.compile(
78
- # r"(?m)^(\d{1,3}[A-Z]?)\.\s*(.+)$"
79
- # )
80
-
81
- # CLAUSE_RE = re.compile(
82
- # r"(?m)^\((\d+[A-Za-z]?)\)\s*(.*)"
83
- # )
84
-
85
- # SUBCLAUSE_RE = re.compile(
86
- # r"(?m)^\(([a-hj-z])\)\s*(.*)"
87
- # )
88
-
89
- # ROMAN_RE = re.compile(
90
- # r"(?m)^\((i|ii|iii|iv|v|vi|vii|viii|ix|x)\)\s*(.*)"
91
- # )
92
-
93
- # PROVISO_RE = re.compile(
94
- # r"(?is)"
95
- # r"(Provided(?:\s+further)?(?:\s+also)?\s+that.*?)(?="
96
- # r"Provided(?:\s+further)?(?:\s+also)?\s+that"
97
- # r"|Explanation"
98
- # r"|$)"
99
- # )
100
-
101
- # EXPLANATION_RE = re.compile(
102
- # r"(?is)"
103
- # r"(Explanation.*?)(?="
104
- # r"Provided"
105
- # r"|$)"
106
- # )
107
-
108
- # REFERENCE_RE = re.compile(
109
- # r"article[s]?\s+(\d+[A-Z]?)",
110
- # re.IGNORECASE
111
- # )
112
-
113
- # # ======================================================
114
- # # OCR CLEANING
115
- # # ======================================================
116
-
117
- # def clean(self, text: str):
118
-
119
- # text = text.replace("\r\n", "\n")
120
-
121
- # # remove page numbers
122
- # text = re.sub(
123
- # r"(?m)^\s*\d+\s*$",
124
- # "",
125
- # text
126
- # )
127
-
128
- # # remove constitution headers
129
- # text = re.sub(
130
- # r"THE CONSTITUTION OF INDIA",
131
- # "",
132
- # text,
133
- # flags=re.IGNORECASE
134
- # )
135
-
136
- # # remove separators
137
- # text = re.sub(
138
- # r"_{5,}",
139
- # "",
140
- # text
141
- # )
142
-
143
- # # remove amendment notes
144
- # text = re.sub(
145
- # r"(?m)^\d+\.\s+(Subs\.|Ins\.|Added|Omitted).*?$",
146
- # "",
147
- # text
148
- # )
149
-
150
- # text = re.sub(
151
- # r"\n{3,}",
152
- # "\n\n",
153
- # text
154
- # )
155
-
156
- # return text.strip()
157
-
158
- # # ======================================================
159
- # # PARTS
160
- # # ======================================================
161
-
162
- # def extract_parts(self, text):
163
-
164
- # matches = list(self.PART_RE.finditer(text))
165
-
166
- # results = []
167
-
168
- # for i, m in enumerate(matches):
169
-
170
- # start = m.start()
171
-
172
- # end = (
173
- # matches[i + 1].start()
174
- # if i + 1 < len(matches)
175
- # else len(text)
176
- # )
177
-
178
- # results.append({
179
- # "part_no": m.group(1).strip(),
180
- # "part_title": m.group(2).strip(),
181
- # "text": text[start:end]
182
- # })
183
-
184
- # return results
185
-
186
- # # ======================================================
187
- # # ARTICLES
188
- # # ======================================================
189
-
190
- # def extract_articles(self, text):
191
-
192
- # matches = list(self.ARTICLE_RE.finditer(text))
193
-
194
- # results = []
195
-
196
- # for i, m in enumerate(matches):
197
-
198
- # start = m.start()
199
-
200
- # end = (
201
- # matches[i + 1].start()
202
- # if i + 1 < len(matches)
203
- # else len(text)
204
- # )
205
-
206
- # results.append({
207
- # "article_no": m.group(1),
208
- # "article_title": m.group(2).strip(),
209
- # "text": text[start:end].strip()
210
- # })
211
-
212
- # return results
213
-
214
- # # ======================================================
215
- # # CLAUSES
216
- # # ======================================================
217
-
218
- # def extract_clauses(self, text):
219
-
220
- # matches = list(self.CLAUSE_RE.finditer(text))
221
-
222
- # results = []
223
-
224
- # for i, m in enumerate(matches):
225
-
226
- # start = m.start()
227
-
228
- # end = (
229
- # matches[i + 1].start()
230
- # if i + 1 < len(matches)
231
- # else len(text)
232
- # )
233
-
234
- # results.append({
235
- # "clause_no": m.group(1),
236
- # "text": text[start:end].strip()
237
- # })
238
-
239
- # return results
240
-
241
- # # ======================================================
242
- # # SUB CLAUSES
243
- # # ======================================================
244
-
245
- # def extract_subclauses(self, text):
246
-
247
- # matches = list(self.SUBCLAUSE_RE.finditer(text))
248
-
249
- # results = []
250
-
251
- # for i, m in enumerate(matches):
252
-
253
- # start = m.start()
254
-
255
- # end = (
256
- # matches[i + 1].start()
257
- # if i + 1 < len(matches)
258
- # else len(text)
259
- # )
260
-
261
- # results.append({
262
- # "sub_clause_no": m.group(1),
263
- # "text": text[start:end].strip()
264
- # })
265
-
266
- # return results
267
-
268
- # # ======================================================
269
- # # ROMAN CLAUSES
270
- # # ======================================================
271
-
272
- # def extract_roman_clauses(self, text):
273
-
274
- # matches = list(self.ROMAN_RE.finditer(text))
275
-
276
- # results = []
277
-
278
- # for i, m in enumerate(matches):
279
-
280
- # start = m.start()
281
-
282
- # end = (
283
- # matches[i + 1].start()
284
- # if i + 1 < len(matches)
285
- # else len(text)
286
- # )
287
-
288
- # results.append({
289
- # "roman_no": m.group(1),
290
- # "text": text[start:end].strip()
291
- # })
292
-
293
- # return results
294
-
295
- # # ======================================================
296
- # # PROVISOS
297
- # # ======================================================
298
-
299
- # def extract_provisos(self, text):
300
-
301
- # return [
302
- # Proviso(text=m.strip())
303
- # for m in self.PROVISO_RE.findall(text)
304
- # ]
305
-
306
- # # ======================================================
307
- # # EXPLANATIONS
308
- # # ======================================================
309
-
310
- # def extract_explanations(self, text):
311
-
312
- # return [
313
- # Explanation(text=m.strip())
314
- # for m in self.EXPLANATION_RE.findall(text)
315
- # ]
316
-
317
- # # ======================================================
318
- # # REFERENCES
319
- # # ======================================================
320
-
321
- # def extract_references(self, text):
322
-
323
- # refs = sorted(
324
- # set(
325
- # self.REFERENCE_RE.findall(text)
326
- # )
327
- # )
328
-
329
- # return [
330
- # Reference(article_no=r)
331
- # for r in refs
332
- # ]
333
-
334
- # # ======================================================
335
- # # MAIN PARSER
336
- # # ======================================================
337
-
338
- # def parse(self, raw_text):
339
-
340
- # raw_text = self.clean(raw_text)
341
-
342
- # parsed_parts = []
343
-
344
- # for p in self.extract_parts(raw_text):
345
-
346
- # part = Part(
347
- # document="constitution",
348
- # part_no=p["part_no"],
349
- # part_title=p["part_title"]
350
- # )
351
-
352
- # for a in self.extract_articles(p["text"]):
353
-
354
- # article = Article(
355
- # document="constitution",
356
- # article_no=a["article_no"],
357
- # article_title=a["article_title"],
358
- # text=a["text"]
359
- # )
360
-
361
- # article.provisos = self.extract_provisos(
362
- # article.text
363
- # )
364
-
365
- # article.explanations = self.extract_explanations(
366
- # article.text
367
- # )
368
-
369
- # article.references = self.extract_references(
370
- # article.text
371
- # )
372
-
373
- # for c in self.extract_clauses(article.text):
374
-
375
- # clause = Clause(
376
- # document="constitution",
377
- # clause_no=c["clause_no"],
378
- # text=c["text"]
379
- # )
380
-
381
- # for s in self.extract_subclauses(
382
- # clause.text
383
- # ):
384
-
385
- # sub = SubClause(
386
- # document="constitution",
387
- # sub_clause_no=s["sub_clause_no"],
388
- # text=s["text"]
389
- # )
390
-
391
- # for r in self.extract_roman_clauses(
392
- # sub.text
393
- # ):
394
-
395
- # sub.roman_clauses.append(
396
- # RomanClause(
397
- # document="constitution",
398
- # roman_no=r["roman_no"],
399
- # text=r["text"]
400
- # )
401
- # )
402
-
403
- # clause.sub_clauses.append(sub)
404
-
405
- # article.clauses.append(clause)
406
-
407
- # part.articles.append(article)
408
-
409
- # parsed_parts.append(part)
410
-
411
- # return parsed_parts
412
-
413
-
414
- # # ==========================================================
415
- # # EXAMPLE
416
- # # ==========================================================
417
-
418
- # if __name__ == "__main__":
419
-
420
- # with open("text_extracted_ocr_output.txt", "r", encoding="utf8") as f:
421
- # text = f.read()
422
-
423
- # parser = ConstitutionParser()
424
-
425
- # result = parser.parse(text)
426
-
427
- # print(asdict(result[0]))
428
-
429
-
430
-
431
-
432
-
433
-
434
-
435
-
436
-
437
-
438
-
439
-
440
-
441
- from __future__ import annotations
442
-
443
- from dataclasses import dataclass, field
444
- from typing import List, Optional
445
-
446
-
447
- # ==========================================================
448
- # BASE NODE
449
- # ==========================================================
450
-
451
- @dataclass
452
- class LegalNode:
453
- """
454
- Base node used by all legal entities.
455
- """
456
-
457
- document: str
458
-
459
- id: Optional[str] = None
460
-
461
- parent_id: Optional[str] = None
462
-
463
- source_text: Optional[str] = None
464
-
465
- start_offset: Optional[int] = None
466
-
467
- end_offset: Optional[int] = None
468
-
469
-
470
- # ==========================================================
471
- # REFERENCES
472
- # ==========================================================
473
-
474
- @dataclass
475
- class Reference:
476
-
477
- reference_type: str
478
-
479
- reference_value: str
480
-
481
- text: str = ""
482
-
483
-
484
- # ==========================================================
485
- # PROVISO
486
- # ==========================================================
487
-
488
- @dataclass
489
- class Proviso(LegalNode):
490
-
491
- text: str = ""
492
-
493
-
494
- # ==========================================================
495
- # EXPLANATION
496
- # ==========================================================
497
-
498
- @dataclass
499
- class Explanation(LegalNode):
500
-
501
- explanation_no: str = ""
502
-
503
- text: str = ""
504
-
505
-
506
- # ==========================================================
507
- # ILLUSTRATION
508
- # ==========================================================
509
-
510
- @dataclass
511
- class Illustration(LegalNode):
512
-
513
- illustration_no: str = ""
514
-
515
- text: str = ""
516
-
517
-
518
- # ==========================================================
519
- # ROMAN CLAUSE
520
- # ==========================================================
521
-
522
- @dataclass
523
- class RomanClause(LegalNode):
524
-
525
- roman_no: str = ""
526
-
527
- text: str = ""
528
-
529
-
530
- # ==========================================================
531
- # SUB CLAUSE
532
- # ==========================================================
533
-
534
- @dataclass
535
- class SubClause(LegalNode):
536
-
537
- sub_clause_no: str = ""
538
-
539
- text: str = ""
540
-
541
- roman_clauses: List[RomanClause] = field(
542
- default_factory=list
543
- )
544
-
545
-
546
- # ==========================================================
547
- # CLAUSE
548
- # ==========================================================
549
-
550
- @dataclass
551
- class Clause(LegalNode):
552
-
553
- clause_no: str = ""
554
-
555
- text: str = ""
556
-
557
- sub_clauses: List[SubClause] = field(
558
- default_factory=list
559
- )
560
-
561
-
562
- # ==========================================================
563
- # ARTICLE
564
- # ==========================================================
565
-
566
- @dataclass
567
- class Article(LegalNode):
568
-
569
- article_no: str = ""
570
-
571
- article_title: str = ""
572
-
573
- text: str = ""
574
-
575
- clauses: List[Clause] = field(
576
- default_factory=list
577
- )
578
-
579
- provisos: List[Proviso] = field(
580
- default_factory=list
581
- )
582
-
583
- explanations: List[Explanation] = field(
584
- default_factory=list
585
- )
586
-
587
- illustrations: List[Illustration] = field(
588
- default_factory=list
589
- )
590
-
591
- references: List[Reference] = field(
592
- default_factory=list
593
- )
594
-
595
-
596
- # ==========================================================
597
- # CHAPTER
598
- # ==========================================================
599
-
600
- @dataclass
601
- class Chapter(LegalNode):
602
-
603
- chapter_no: str = ""
604
-
605
- chapter_title: str = ""
606
-
607
- articles: List[Article] = field(
608
- default_factory=list
609
- )
610
-
611
-
612
- # ==========================================================
613
- # PART
614
- # ==========================================================
615
-
616
- @dataclass
617
- class Part(LegalNode):
618
-
619
- part_no: str = ""
620
-
621
- part_title: str = ""
622
-
623
- chapters: List[Chapter] = field(
624
- default_factory=list
625
- )
626
-
627
- articles: List[Article] = field(
628
- default_factory=list
629
- )
630
-
631
-
632
- # ==========================================================
633
- # SCHEDULE PARAGRAPH
634
- # ==========================================================
635
-
636
- @dataclass
637
- class ScheduleParagraph(LegalNode):
638
-
639
- paragraph_no: str = ""
640
-
641
- text: str = ""
642
-
643
-
644
- # ==========================================================
645
- # SCHEDULE
646
- # ==========================================================
647
-
648
- @dataclass
649
- class Schedule(LegalNode):
650
-
651
- schedule_no: str = ""
652
-
653
- schedule_title: str = ""
654
-
655
- text: str = ""
656
-
657
- paragraphs: List[ScheduleParagraph] = field(
658
- default_factory=list
659
- )
660
-
661
- references: List[Reference] = field(
662
- default_factory=list
663
- )
664
-
665
-
666
- # ==========================================================
667
- # PREAMBLE
668
- # ==========================================================
669
-
670
- @dataclass
671
- class Preamble(LegalNode):
672
-
673
- text: str = ""
674
-
675
-
676
- # ==========================================================
677
- # CONSTITUTION DOCUMENT
678
- # ==========================================================
679
-
680
- @dataclass
681
- class ConstitutionDocument(LegalNode):
682
-
683
- title: str = "Constitution of India"
684
-
685
- preamble: Optional[Preamble] = None
686
-
687
- parts: List[Part] = field(
688
- default_factory=list
689
- )
690
-
691
- schedules: List[Schedule] = field(
692
- default_factory=list
693
- )
694
-
695
-
696
- # ==========================================================
697
- # UTILITIES
698
- # ==========================================================
699
-
700
- def build_id(
701
- prefix: str,
702
- value: str
703
- ) -> str:
704
-
705
- value = value.replace(" ", "_")
706
-
707
- return f"{prefix}:{value}"
708
-
709
-
710
- def assign_ids(
711
- constitution: ConstitutionDocument
712
- ):
713
-
714
- constitution.id = "constitution"
715
-
716
- for part in constitution.parts:
717
-
718
- part.id = build_id(
719
- "part",
720
- part.part_no
721
- )
722
-
723
- part.parent_id = constitution.id
724
-
725
- for chapter in part.chapters:
726
-
727
- chapter.id = build_id(
728
- "chapter",
729
- chapter.chapter_no
730
- )
731
-
732
- chapter.parent_id = part.id
733
-
734
- for article in chapter.articles:
735
-
736
- article.id = build_id(
737
- "article",
738
- article.article_no
739
- )
740
-
741
- article.parent_id = chapter.id
742
-
743
- for article in part.articles:
744
-
745
- article.id = build_id(
746
- "article",
747
- article.article_no
748
- )
749
-
750
- article.parent_id = part.id
751
-
752
- for schedule in constitution.schedules:
753
-
754
- schedule.id = build_id(
755
- "schedule",
756
- schedule.schedule_no
757
- )
758
-
759
- schedule.parent_id = constitution.id
760
-
761
-
762
-
763
- from __future__ import annotations
764
-
765
- import re
766
- from typing import List, Dict
767
-
768
-
769
- class PartParser:
770
- """
771
- Constitution Part Parser
772
-
773
- Extracts:
774
-
775
- PART I
776
- THE UNION AND ITS TERRITORY
777
-
778
- PART II
779
- CITIZENSHIP
780
-
781
- and returns the entire text
782
- belonging to each Part.
783
- """
784
-
785
- # =====================================================
786
- # PART PATTERN
787
- # =====================================================
788
-
789
- PART_RE = re.compile(
790
- r"(?im)^PART\s+([IVXLCDM]+)\s*$"
791
- )
792
-
793
- # =====================================================
794
- # CLEAN TITLE
795
- # =====================================================
796
-
797
- @staticmethod
798
- def clean_title(title: str) -> str:
799
-
800
- title = title.strip()
801
-
802
- title = re.sub(
803
- r"\s+",
804
- " ",
805
- title
806
- )
807
-
808
- return title
809
-
810
- # =====================================================
811
- # EXTRACT TITLE
812
- # =====================================================
813
-
814
- def extract_part_title(
815
- self,
816
- text: str,
817
- part_start: int
818
- ) -> str:
819
-
820
- """
821
- Usually title is within next 5 lines
822
- after PART I
823
- """
824
-
825
- chunk = text[part_start:part_start + 1000]
826
-
827
- lines = [
828
- x.strip()
829
- for x in chunk.split("\n")
830
- if x.strip()
831
- ]
832
-
833
- if len(lines) < 2:
834
- return ""
835
-
836
- title_lines = []
837
-
838
- for line in lines[1:6]:
839
-
840
- # stop if article starts
841
-
842
- if re.match(
843
- r"^\d+[A-Z]{0,3}\.",
844
- line
845
- ):
846
- break
847
-
848
- # stop if chapter starts
849
-
850
- if re.match(
851
- r"^CHAPTER\s+",
852
- line,
853
- re.I
854
- ):
855
- break
856
-
857
- title_lines.append(line)
858
-
859
- return self.clean_title(
860
- " ".join(title_lines)
861
- )
862
-
863
- # =====================================================
864
- # EXTRACT PARTS
865
- # =====================================================
866
-
867
- def extract_parts(
868
- self,
869
- text: str
870
- ) -> List[Dict]:
871
-
872
- matches = list(
873
- self.PART_RE.finditer(text)
874
- )
875
-
876
- if not matches:
877
- return []
878
-
879
- parts = []
880
-
881
- for i, match in enumerate(matches):
882
-
883
- start = match.start()
884
-
885
- end = (
886
- matches[i + 1].start()
887
- if i + 1 < len(matches)
888
- else len(text)
889
- )
890
-
891
- part_no = match.group(1)
892
-
893
- part_title = self.extract_part_title(
894
- text,
895
- start
896
- )
897
-
898
- part_text = text[start:end].strip()
899
-
900
- parts.append(
901
- {
902
- "part_no": part_no,
903
- "part_title": part_title,
904
- "text": part_text,
905
- "start": start,
906
- "end": end
907
- }
908
- )
909
-
910
- return parts
911
-
912
- # =====================================================
913
- # VALIDATION
914
- # =====================================================
915
-
916
- def validate_parts(
917
- self,
918
- parts: List[Dict]
919
- ) -> List[str]:
920
-
921
- errors = []
922
-
923
- if not parts:
924
-
925
- errors.append(
926
- "No Parts Found"
927
- )
928
-
929
- return errors
930
-
931
- seen = set()
932
-
933
- for part in parts:
934
-
935
- part_no = part["part_no"]
936
-
937
- if part_no in seen:
938
-
939
- errors.append(
940
- f"Duplicate Part {part_no}"
941
- )
942
-
943
- seen.add(part_no)
944
-
945
- if not part["part_title"]:
946
-
947
- errors.append(
948
- f"Missing title for Part {part_no}"
949
- )
950
-
951
- return errors
952
-
953
-
954
- # =========================================================
955
- # EXAMPLE
956
- # =========================================================
957
-
958
- if __name__ == "__main__":
959
-
960
- with open(
961
- "constitution.txt",
962
- "r",
963
- encoding="utf8"
964
- ) as f:
965
-
966
- text = f.read()
967
-
968
- parser = PartParser()
969
-
970
- parts = parser.extract_parts(text)
971
-
972
- print(
973
- f"Parts Found: {len(parts)}"
974
- )
975
-
976
- for part in parts[:5]:
977
-
978
- print(
979
- part["part_no"],
980
- part["part_title"]
981
- )
982
-
983
- print(
984
- parser.validate_parts(parts)
985
- )
986
-
987
-
988
- from __future__ import annotations
989
-
990
- import re
991
- from typing import List, Dict
992
-
993
-
994
- class ChapterParser:
995
- """
996
- Constitution Chapter Parser
997
-
998
- Extracts:
999
-
1000
- CHAPTER I
1001
- THE EXECUTIVE
1002
-
1003
- CHAPTER II
1004
- PARLIAMENT
1005
-
1006
- and returns text belonging
1007
- to each chapter.
1008
- """
1009
-
1010
- # =====================================================
1011
- # CHAPTER REGEX
1012
- # =====================================================
1013
-
1014
- CHAPTER_RE = re.compile(
1015
- r"(?im)^CHAPTER\s+([IVXLCDM]+[A-Z]?)\s*$"
1016
- )
1017
-
1018
- # =====================================================
1019
- # TITLE CLEANER
1020
- # =====================================================
1021
-
1022
- @staticmethod
1023
- def clean_title(title: str) -> str:
1024
-
1025
- title = title.strip()
1026
-
1027
- title = re.sub(
1028
- r"\s+",
1029
- " ",
1030
- title
1031
- )
1032
-
1033
- return title
1034
-
1035
- # =====================================================
1036
- # EXTRACT CHAPTER TITLE
1037
- # =====================================================
1038
-
1039
- def extract_chapter_title(
1040
- self,
1041
- text: str,
1042
- chapter_start: int
1043
- ) -> str:
1044
-
1045
- chunk = text[
1046
- chapter_start:
1047
- chapter_start + 1000
1048
- ]
1049
-
1050
- lines = [
1051
- line.strip()
1052
- for line in chunk.split("\n")
1053
- if line.strip()
1054
- ]
1055
-
1056
- if len(lines) < 2:
1057
- return ""
1058
-
1059
- title_lines = []
1060
-
1061
- for line in lines[1:6]:
1062
-
1063
- # stop at article
1064
-
1065
- if re.match(
1066
- r"^\d+[A-Z]{0,3}\.",
1067
- line
1068
- ):
1069
- break
1070
-
1071
- # stop at next chapter
1072
-
1073
- if re.match(
1074
- r"^CHAPTER\s+",
1075
- line,
1076
- re.I
1077
- ):
1078
- break
1079
-
1080
- title_lines.append(line)
1081
-
1082
- return self.clean_title(
1083
- " ".join(title_lines)
1084
- )
1085
-
1086
- # =====================================================
1087
- # EXTRACT CHAPTERS
1088
- # =====================================================
1089
-
1090
- def extract_chapters(
1091
- self,
1092
- text: str
1093
- ) -> List[Dict]:
1094
-
1095
- matches = list(
1096
- self.CHAPTER_RE.finditer(text)
1097
- )
1098
-
1099
- if not matches:
1100
- return []
1101
-
1102
- chapters = []
1103
-
1104
- for i, match in enumerate(matches):
1105
-
1106
- start = match.start()
1107
-
1108
- end = (
1109
- matches[i + 1].start()
1110
- if i + 1 < len(matches)
1111
- else len(text)
1112
- )
1113
-
1114
- chapter_no = match.group(1)
1115
-
1116
- chapter_title = (
1117
- self.extract_chapter_title(
1118
- text,
1119
- start
1120
- )
1121
- )
1122
-
1123
- chapter_text = (
1124
- text[start:end]
1125
- .strip()
1126
- )
1127
-
1128
- chapters.append(
1129
- {
1130
- "chapter_no":
1131
- chapter_no,
1132
-
1133
- "chapter_title":
1134
- chapter_title,
1135
-
1136
- "text":
1137
- chapter_text,
1138
-
1139
- "start":
1140
- start,
1141
-
1142
- "end":
1143
- end
1144
- }
1145
- )
1146
-
1147
- return chapters
1148
-
1149
- # =====================================================
1150
- # VALIDATION
1151
- # =====================================================
1152
-
1153
- def validate_chapters(
1154
- self,
1155
- chapters: List[Dict]
1156
- ) -> List[str]:
1157
-
1158
- errors = []
1159
-
1160
- seen = set()
1161
-
1162
- for chapter in chapters:
1163
-
1164
- chapter_no = (
1165
- chapter["chapter_no"]
1166
- )
1167
-
1168
- if chapter_no in seen:
1169
-
1170
- errors.append(
1171
- f"Duplicate Chapter "
1172
- f"{chapter_no}"
1173
- )
1174
-
1175
- seen.add(chapter_no)
1176
-
1177
- if not chapter[
1178
- "chapter_title"
1179
- ]:
1180
-
1181
- errors.append(
1182
- f"Missing title for "
1183
- f"Chapter {chapter_no}"
1184
- )
1185
-
1186
- return errors
1187
-
1188
-
1189
- # =========================================================
1190
- # EXAMPLE
1191
- # =========================================================
1192
-
1193
- if __name__ == "__main__":
1194
-
1195
- with open(
1196
- "constitution.txt",
1197
- "r",
1198
- encoding="utf8"
1199
- ) as f:
1200
-
1201
- text = f.read()
1202
-
1203
- parser = ChapterParser()
1204
-
1205
- chapters = (
1206
- parser.extract_chapters(
1207
- text
1208
- )
1209
- )
1210
-
1211
- print(
1212
- f"Found "
1213
- f"{len(chapters)} "
1214
- f"chapters"
1215
- )
1216
-
1217
- for chapter in chapters[:10]:
1218
-
1219
- print(
1220
- chapter["chapter_no"],
1221
- chapter["chapter_title"]
1222
- )
1223
-
1224
- print(
1225
- parser.validate_chapters(
1226
- chapters
1227
- )
1228
- )
1229
-
1230
-
1231
- from __future__ import annotations
1232
-
1233
- import re
1234
- from typing import List, Dict
1235
-
1236
-
1237
- class ArticleParser:
1238
- """
1239
- Constitution Article Parser
1240
-
1241
- Supports:
1242
-
1243
- 1.
1244
- 21A.
1245
- 239AA.
1246
- 243ZG.
1247
-
1248
- Returns:
1249
-
1250
- article_no
1251
- article_title
1252
- article_text
1253
- start
1254
- end
1255
- """
1256
-
1257
- # =====================================================
1258
- # ARTICLE HEADER
1259
- # =====================================================
1260
-
1261
- ARTICLE_RE = re.compile(
1262
- r"(?m)^(\d{1,3}[A-Z]{0,3})\.\s*(.*?)\s*$"
1263
- )
1264
-
1265
- # =====================================================
1266
- # ARTICLE NUMBER VALIDATOR
1267
- # =====================================================
1268
-
1269
- ARTICLE_NUMBER_RE = re.compile(
1270
- r"^\d{1,3}[A-Z]{0,3}$"
1271
- )
1272
-
1273
- # =====================================================
1274
- # CLEAN TITLE
1275
- # =====================================================
1276
-
1277
- @staticmethod
1278
- def clean_title(title: str) -> str:
1279
-
1280
- title = title.strip()
1281
-
1282
- title = re.sub(
1283
- r"\s+",
1284
- " ",
1285
- title
1286
- )
1287
-
1288
- title = title.rstrip(
1289
- "."
1290
- )
1291
-
1292
- title = title.rstrip(
1293
- "—"
1294
- )
1295
-
1296
- title = title.strip()
1297
-
1298
- return title
1299
-
1300
- # =====================================================
1301
- # RECOVER MULTI LINE TITLE
1302
- # =====================================================
1303
-
1304
- def recover_title(
1305
- self,
1306
- article_text: str,
1307
- current_title: str
1308
- ) -> str:
1309
-
1310
- if current_title:
1311
- return self.clean_title(
1312
- current_title
1313
- )
1314
-
1315
- lines = [
1316
- line.strip()
1317
- for line in article_text.split(
1318
- "\n"
1319
- )
1320
- if line.strip()
1321
- ]
1322
-
1323
- if len(lines) < 2:
1324
- return ""
1325
-
1326
- candidate = lines[1]
1327
-
1328
- if len(candidate) > 150:
1329
- return ""
1330
-
1331
- return self.clean_title(
1332
- candidate
1333
- )
1334
-
1335
- # =====================================================
1336
- # EXTRACT ARTICLES
1337
- # =====================================================
1338
-
1339
- def extract_articles(
1340
- self,
1341
- text: str
1342
- ) -> List[Dict]:
1343
-
1344
- matches = list(
1345
- self.ARTICLE_RE.finditer(
1346
- text
1347
- )
1348
- )
1349
-
1350
- if not matches:
1351
- return []
1352
-
1353
- articles = []
1354
-
1355
- for i, match in enumerate(
1356
- matches
1357
- ):
1358
-
1359
- start = match.start()
1360
-
1361
- end = (
1362
- matches[i + 1].start()
1363
- if i + 1 < len(matches)
1364
- else len(text)
1365
- )
1366
-
1367
- article_no = (
1368
- match.group(1)
1369
- .strip()
1370
- )
1371
-
1372
- article_title = (
1373
- match.group(2)
1374
- .strip()
1375
- )
1376
-
1377
- article_text = (
1378
- text[start:end]
1379
- .strip()
1380
- )
1381
-
1382
- article_title = (
1383
- self.recover_title(
1384
- article_text,
1385
- article_title
1386
- )
1387
- )
1388
-
1389
- articles.append(
1390
- {
1391
- "article_no":
1392
- article_no,
1393
-
1394
- "article_title":
1395
- article_title,
1396
-
1397
- "text":
1398
- article_text,
1399
-
1400
- "start":
1401
- start,
1402
-
1403
- "end":
1404
- end
1405
- }
1406
- )
1407
-
1408
- return articles
1409
-
1410
- # =====================================================
1411
- # VALIDATION
1412
- # =====================================================
1413
-
1414
- def validate_articles(
1415
- self,
1416
- articles: List[Dict]
1417
- ) -> List[str]:
1418
-
1419
- errors = []
1420
-
1421
- seen = set()
1422
-
1423
- for article in articles:
1424
-
1425
- article_no = (
1426
- article["article_no"]
1427
- )
1428
-
1429
- if article_no in seen:
1430
-
1431
- errors.append(
1432
- f"Duplicate "
1433
- f"Article "
1434
- f"{article_no}"
1435
- )
1436
-
1437
- seen.add(
1438
- article_no
1439
- )
1440
-
1441
- if not self.ARTICLE_NUMBER_RE.match(
1442
- article_no
1443
- ):
1444
-
1445
- errors.append(
1446
- f"Invalid Article "
1447
- f"Number "
1448
- f"{article_no}"
1449
- )
1450
-
1451
- if not article[
1452
- "article_title"
1453
- ]:
1454
-
1455
- errors.append(
1456
- f"Missing title "
1457
- f"for Article "
1458
- f"{article_no}"
1459
- )
1460
-
1461
- return errors
1462
-
1463
- # =====================================================
1464
- # FIND ARTICLE
1465
- # =====================================================
1466
-
1467
- def get_article(
1468
- self,
1469
- articles: List[Dict],
1470
- article_no: str
1471
- ):
1472
-
1473
- for article in articles:
1474
-
1475
- if (
1476
- article["article_no"]
1477
- == article_no
1478
- ):
1479
- return article
1480
-
1481
- return None
1482
-
1483
-
1484
- # =========================================================
1485
- # EXAMPLE
1486
- # =========================================================
1487
-
1488
- if __name__ == "__main__":
1489
-
1490
- with open(
1491
- "constitution.txt",
1492
- "r",
1493
- encoding="utf8"
1494
- ) as f:
1495
-
1496
- text = f.read()
1497
-
1498
- parser = ArticleParser()
1499
-
1500
- articles = (
1501
- parser.extract_articles(
1502
- text
1503
- )
1504
- )
1505
-
1506
- print(
1507
- f"Articles Found: "
1508
- f"{len(articles)}"
1509
- )
1510
-
1511
- for article in articles[:10]:
1512
-
1513
- print(
1514
- article["article_no"],
1515
- article["article_title"]
1516
- )
1517
-
1518
- errors = (
1519
- parser.validate_articles(
1520
- articles
1521
- )
1522
- )
1523
-
1524
- print(
1525
- f"Errors: {len(errors)}"
1526
- )
1527
-
1528
- for e in errors[:20]:
1529
- print(e)
1530
-
1531
-
1532
-
1533
- from __future__ import annotations
1534
-
1535
- import re
1536
- from typing import List
1537
-
1538
- from models.legal_models import (
1539
- Clause,
1540
- SubClause,
1541
- RomanClause
1542
- )
1543
-
1544
-
1545
- class ClauseParser:
1546
-
1547
- """
1548
- Recursive Clause Parser
1549
-
1550
- Handles:
1551
-
1552
- (1)
1553
- (2)
1554
- (2A)
1555
-
1556
- Sub Clauses:
1557
-
1558
- (a)
1559
- (b)
1560
-
1561
- Roman Clauses:
1562
-
1563
- (i)
1564
- (ii)
1565
- (iii)
1566
- (xiv)
1567
-
1568
- """
1569
-
1570
- # =====================================================
1571
- # REGEX
1572
- # =====================================================
1573
-
1574
- CLAUSE_RE = re.compile(
1575
- r"(?m)^\((\d+[A-Z]?)\)\s"
1576
- )
1577
-
1578
- SUBCLAUSE_RE = re.compile(
1579
- r"(?m)^\(([a-z])\)\s"
1580
- )
1581
-
1582
- ROMAN_RE = re.compile(
1583
- r"(?m)^\(([ivxlcdm]+)\)\s",
1584
- re.IGNORECASE
1585
- )
1586
-
1587
- # =====================================================
1588
- # SPLIT BY MARKER
1589
- # =====================================================
1590
-
1591
- def split_sections(
1592
- self,
1593
- text: str,
1594
- pattern: re.Pattern
1595
- ):
1596
-
1597
- matches = list(
1598
- pattern.finditer(text)
1599
- )
1600
-
1601
- if not matches:
1602
- return []
1603
-
1604
- sections = []
1605
-
1606
- for i, match in enumerate(matches):
1607
-
1608
- start = match.start()
1609
-
1610
- end = (
1611
- matches[i + 1].start()
1612
- if i + 1 < len(matches)
1613
- else len(text)
1614
- )
1615
-
1616
- sections.append(
1617
- (
1618
- match.group(1),
1619
- text[start:end].strip()
1620
- )
1621
- )
1622
-
1623
- return sections
1624
-
1625
- # =====================================================
1626
- # ROMAN CLAUSES
1627
- # =====================================================
1628
-
1629
- def parse_roman_clauses(
1630
- self,
1631
- text: str
1632
- ) -> List[RomanClause]:
1633
-
1634
- romans = []
1635
-
1636
- for roman_no, roman_text in self.split_sections(
1637
- text,
1638
- self.ROMAN_RE
1639
- ):
1640
-
1641
- romans.append(
1642
- RomanClause(
1643
- document="constitution",
1644
- roman_no=roman_no,
1645
- text=roman_text
1646
- )
1647
- )
1648
-
1649
- return romans
1650
-
1651
- # =====================================================
1652
- # SUB CLAUSES
1653
- # =====================================================
1654
-
1655
- def parse_subclauses(
1656
- self,
1657
- text: str
1658
- ) -> List[SubClause]:
1659
-
1660
- subclauses = []
1661
-
1662
- for sub_no, sub_text in self.split_sections(
1663
- text,
1664
- self.SUBCLAUSE_RE
1665
- ):
1666
-
1667
- sub = SubClause(
1668
- document="constitution",
1669
- sub_clause_no=sub_no,
1670
- text=sub_text
1671
- )
1672
-
1673
- sub.roman_clauses.extend(
1674
- self.parse_roman_clauses(
1675
- sub_text
1676
- )
1677
- )
1678
-
1679
- subclauses.append(sub)
1680
-
1681
- return subclauses
1682
-
1683
- # =====================================================
1684
- # CLAUSES
1685
- # =====================================================
1686
-
1687
- def parse_clauses(
1688
- self,
1689
- article_text: str
1690
- ) -> List[Clause]:
1691
-
1692
- clauses = []
1693
-
1694
- clause_sections = self.split_sections(
1695
- article_text,
1696
- self.CLAUSE_RE
1697
- )
1698
-
1699
- if not clause_sections:
1700
- return clauses
1701
-
1702
- for clause_no, clause_text in clause_sections:
1703
-
1704
- clause = Clause(
1705
- document="constitution",
1706
- clause_no=clause_no,
1707
- text=clause_text
1708
- )
1709
-
1710
- clause.sub_clauses.extend(
1711
- self.parse_subclauses(
1712
- clause_text
1713
- )
1714
- )
1715
-
1716
- clauses.append(clause)
1717
-
1718
- return clauses
1719
-
1720
- # =====================================================
1721
- # VALIDATION
1722
- # =====================================================
1723
-
1724
- def validate_clauses(
1725
- self,
1726
- clauses: List[Clause]
1727
- ) -> List[str]:
1728
-
1729
- errors = []
1730
-
1731
- seen = set()
1732
-
1733
- for clause in clauses:
1734
-
1735
- if clause.clause_no in seen:
1736
-
1737
- errors.append(
1738
- f"Duplicate Clause "
1739
- f"{clause.clause_no}"
1740
- )
1741
-
1742
- seen.add(
1743
- clause.clause_no
1744
- )
1745
-
1746
- sub_seen = set()
1747
-
1748
- for sub in clause.sub_clauses:
1749
-
1750
- if sub.sub_clause_no in sub_seen:
1751
-
1752
- errors.append(
1753
- f"Duplicate SubClause "
1754
- f"{sub.sub_clause_no} "
1755
- f"in Clause "
1756
- f"{clause.clause_no}"
1757
- )
1758
-
1759
- sub_seen.add(
1760
- sub.sub_clause_no
1761
- )
1762
-
1763
- roman_seen = set()
1764
-
1765
- for roman in sub.roman_clauses:
1766
-
1767
- if roman.roman_no in roman_seen:
1768
-
1769
- errors.append(
1770
- f"Duplicate Roman "
1771
- f"{roman.roman_no}"
1772
- )
1773
-
1774
- roman_seen.add(
1775
- roman.roman_no
1776
- )
1777
-
1778
- return errors
1779
-
1780
-
1781
- # =========================================================
1782
- # EXAMPLE
1783
- # =========================================================
1784
-
1785
- if __name__ == "__main__":
1786
-
1787
- sample = '''
1788
- (1) The State may make laws.
1789
-
1790
- (a) for education
1791
-
1792
- (i) primary education
1793
-
1794
- (ii) secondary education
1795
-
1796
- (b) for health
1797
-
1798
- (2) Parliament may legislate.
1799
- '''
1800
-
1801
- parser = ClauseParser()
1802
-
1803
- clauses = parser.parse_clauses(
1804
- sample
1805
- )
1806
-
1807
- print(
1808
- f"Clauses: "
1809
- f"{len(clauses)}"
1810
- )
1811
-
1812
- for clause in clauses:
1813
-
1814
- print(
1815
- clause.clause_no,
1816
- len(clause.sub_clauses)
1817
- )
1818
-
1819
- print(
1820
- parser.validate_clauses(
1821
- clauses
1822
- )
1823
- )