""" Data loading module: reads .txt/.md/.pdf files and splits them into paragraph-level LangChain Documents with metadata (source, chunk_id, section, and page for PDFs). Requirements: pip install langchain-core pypdf """ import os from typing import Any, Dict, List, Optional from langchain_core.documents import Document # PDF text extraction from pypdf import PdfReader SUPPORTED_EXTENSIONS = (".txt", ".md", ".pdf") # Set the path to the file you want to load here — change this to # point at your .txt, .md, or .pdf file. This is the single place # the file path is configured; hybrid_rag_pipeline.py doesn't set it. FILE_PATH = "test.txt" # --------------------------------------------------- # Shared paragraph -> Document chunking logic # --------------------------------------------------- def _paragraphs_to_documents( paragraphs: List[str], source_name: str, start_chunk_id: int = 0, extra_metadata: Optional[Dict[str, Any]] = None, current_section: str = "unknown", ) -> List[Document]: """ Convert a list of paragraph strings into Documents, carrying forward a naive "section" heading guess across paragraphs. """ docs = [] for offset, para in enumerate(paragraphs): first_line = para.splitlines()[0].strip() if len(first_line) < 60 and not first_line.endswith((".", "?", "!")): current_section = first_line metadata = { "source": source_name, "chunk_id": start_chunk_id + offset, "section": current_section, } if extra_metadata: metadata.update(extra_metadata) docs.append(Document(page_content=para, metadata=metadata)) return docs # --------------------------------------------------- # Load .txt Documents (with metadata) # --------------------------------------------------- def load_text_documents(file_path: str) -> List[Document]: """ Load a .txt/.md file and split it into paragraph-level Documents. Each Document gets metadata you can later filter on: - source: the file it came from - chunk_id: its position in the file - section: a naive heading guess (first line-like token), useful as an example metadata filter field """ with open(file_path, "r", encoding="utf-8") as f: content = f.read() paragraphs = [p.strip() for p in content.split("\n\n") if p.strip()] return _paragraphs_to_documents( paragraphs, source_name=os.path.basename(file_path), ) # --------------------------------------------------- # Load .pdf Documents (with metadata) # --------------------------------------------------- def load_pdf_documents(file_path: str) -> List[Document]: """ Load a .pdf file, extract text page by page, and split each page into paragraph-level Documents. Each Document gets the same metadata fields as load_text_documents, plus: - page: the 1-indexed PDF page number the chunk came from Note: extraction quality depends on the PDF — scanned/image-only PDFs will yield little or no text (they'd need OCR first). """ reader = PdfReader(file_path) source_name = os.path.basename(file_path) all_docs: List[Document] = [] current_section = "unknown" chunk_id = 0 for page_num, page in enumerate(reader.pages, start=1): text = page.extract_text() or "" paragraphs = [p.strip() for p in text.split("\n\n") if p.strip()] if not paragraphs: continue page_docs = _paragraphs_to_documents( paragraphs, source_name=source_name, start_chunk_id=chunk_id, extra_metadata={"page": page_num}, current_section=current_section, ) # carry section guess forward into the next page if page_docs: current_section = page_docs[-1].metadata["section"] chunk_id += len(page_docs) all_docs.extend(page_docs) return all_docs # --------------------------------------------------- # Generic dispatcher: pick the right loader by extension # --------------------------------------------------- def load_documents(file_path: Optional[str] = None) -> List[Document]: """ Load a document file into paragraph-level Documents, dispatching to the right loader based on file extension. If file_path is omitted, falls back to the FILE_PATH constant defined above — so callers (like hybrid_rag_pipeline.py) don't need to know or set a path themselves. Supported: .txt, .md, .pdf """ path = file_path or FILE_PATH ext = os.path.splitext(path)[1].lower() if ext == ".pdf": return load_pdf_documents(path) elif ext in (".txt", ".md"): return load_text_documents(path) else: raise ValueError( f"Unsupported file type: '{ext}'. Supported extensions: " f"{', '.join(SUPPORTED_EXTENSIONS)}" ) if __name__ == "__main__": # Quick standalone check: run `python data_loader.py` to verify a # file loads and chunks as expected before wiring it into the # full pipeline. docs = load_documents() print(f"Loaded {len(docs)} documents from '{FILE_PATH}'.") for doc in docs[:3]: print("\n---") print(doc.metadata) print(doc.page_content[:200])