ilaa-chenjeri-15's picture
Initial commit: Explainable RAG system
2db7cb0
Raw
History Blame Contribute Delete
5.11 kB
import re
import json
from tqdm import tqdm
CHUNK_SIZE = 300
OVERLAP = 50
MIN_CHUNK_WORDS = 30
def split_by_headings(text: str) -> list[str]:
heading_re = re.compile(r'(?:^|\n)(?=\n?[A-Z][^\n]{2,80}\n)')
candidate_positions = [m.start() for m in heading_re.finditer(text)]
safe_positions = [0]
in_fence = False
fence_re = re.compile(r'```')
fence_positions = [m.start() for m in fence_re.finditer(text)]
fence_idx = 0
for pos in candidate_positions:
if pos == 0:
continue
while fence_idx < len(fence_positions) and fence_positions[fence_idx] < pos:
in_fence = not in_fence
fence_idx += 1
if not in_fence:
safe_positions.append(pos)
safe_positions.append(len(text))
sections = []
for i in range(len(safe_positions) - 1):
chunk = text[safe_positions[i]:safe_positions[i + 1]].strip()
if chunk:
sections.append(chunk)
return sections if sections else [text.strip()]
def split_section_into_parts(section: str) -> list[dict]:
parts = []
segments = re.split(r'(```[\s\S]*?```)', section)
for seg in segments:
seg = seg.strip()
if not seg:
continue
if seg.startswith("```"):
parts.append({'type': 'code', 'content': seg})
else:
sentences = re.split(r'(?<=[.!?])\s+', seg)
for sent in sentences:
sent = sent.strip()
if sent:
parts.append({'type': 'text', 'content': sent})
return parts
def chunk_section(section: str, chunk_size: int = CHUNK_SIZE, overlap: int = OVERLAP) -> list[str]:
parts = split_section_into_parts(section)
chunks: list[str] = []
current_parts: list[dict] = []
current_words = 0
def flush(current_parts: list[dict]) -> list[str]:
text = "\n\n".join(p['content'] for p in current_parts).strip()
return text
def word_count(s: str) -> int:
return len(s.split())
for part in parts:
wc = word_count(part['content'])
if part['type'] == 'code':
if current_words + wc > chunk_size and current_parts:
chunks.append(flush(current_parts))
current_parts = []
current_words = 0
current_parts.append(part)
current_words += wc
if current_words >= chunk_size:
chunks.append(flush(current_parts))
current_parts = []
current_words = 0
continue
if current_words + wc > chunk_size and current_parts:
chunks.append(flush(current_parts))
overlap_parts: list[dict] = []
overlap_words = 0
for prev in reversed(current_parts):
if prev['type'] == 'code':
pw = word_count(prev['content'])
if overlap_words + pw > overlap:
break
overlap_parts.insert(0, prev)
overlap_words += pw
current_parts = overlap_parts
current_words = overlap_words
current_parts.append(part)
current_words += wc
if current_parts:
chunks.append(flush(current_parts))
return chunks
def hybrid_chunk(doc: dict) -> list[str]:
sections = split_by_headings(doc["text"])
final_chunks: list[str] = []
for section in sections:
chunks = chunk_section(section)
final_chunks.extend(chunks)
return final_chunks
def chunk_documents(input_path="data/docs.json", output_path="processed/chunks.json"):
print("[INFO] Loading documents...\n")
with open(input_path, "r") as f:
docs = json.load(f)
print(f"[INFO] Loaded {len(docs)} documents\n")
all_chunks = []
global_id = 0
for doc in tqdm(docs, desc="Chunking documents"):
chunks = hybrid_chunk(doc)
chunks = [c for c in chunks if len(c.split()) >= MIN_CHUNK_WORDS]
print(f"[DEBUG] {doc['metadata']['title']}{len(chunks)} chunks")
for local_id, chunk in enumerate(chunks):
text = chunk.strip()
if not text:
continue
if text.count("```") % 2 != 0:
text += "\n```"
all_chunks.append({
"text": text,
"metadata": {
**doc["metadata"],
"chunk_id": local_id,
"global_chunk_id": global_id
}
})
global_id += 1
wc_list = [len(c["text"].split()) for c in all_chunks]
print(f"\n[INFO] Total chunks created: {len(all_chunks)}")
if wc_list:
print(f"[INFO] Word count — min: {min(wc_list)}, max: {max(wc_list)}, "
f"mean: {sum(wc_list)//len(wc_list)}, "
f"median: {sorted(wc_list)[len(wc_list)//2]}")
with open(output_path, "w") as f:
json.dump(all_chunks, f, indent=2)
print(f"[SUCCESS] Saved to {output_path}")
if __name__ == "__main__":
chunk_documents()