""" Document ingestion module for processing PDFs and URLs. """ import os from typing import List, Dict import requests from bs4 import BeautifulSoup from pypdf import PdfReader from langchain_text_splitters import RecursiveCharacterTextSplitter from sentence_transformers import SentenceTransformer import faiss import pickle class DocumentIngestion: """Handles ingestion of PDFs and URLs into a searchable vector store.""" def __init__(self, embedding_model: str = "all-mpnet-base-v2"): """ Initialize the document ingestion system. Args: embedding_model: Hugging Face model name for embeddings """ self.embedding_model = SentenceTransformer(embedding_model) self.text_splitter = RecursiveCharacterTextSplitter( chunk_size=600, chunk_overlap=150, length_function=len, ) self.documents = [] self.embeddings = None self.index = None def get_pdf_document_title(self, file_path: str) -> str: """ Get a human-readable document title for a PDF (from metadata or filename). Args: file_path: Path to the PDF file Returns: Document title (e.g. standard name or filename without extension) """ try: reader = PdfReader(file_path) if reader.metadata and getattr(reader.metadata, "title", None): title = reader.metadata.title if title and title.strip(): return title.strip() except Exception: pass # Fallback: filename without extension, cleaned for standards (e.g. CAN-CGSB-32.312 -> CAN/CGSB-32.312) base = os.path.splitext(os.path.basename(file_path))[0] if base: return base.replace("-", "/") if "CGSB" in base or "CAN" in base else base return file_path def read_pdf(self, file_path: str) -> str: """ Extract text from a PDF file. Args: file_path: Path to the PDF file Returns: Extracted text content """ try: reader = PdfReader(file_path) text = "" for page in reader.pages: text += page.extract_text() + "\n" return text except Exception as e: raise Exception(f"Error reading PDF {file_path}: {str(e)}") def read_url(self, url: str): """ Extract text and page title from a URL. Args: url: URL to fetch and extract text from Returns: Tuple of (text content, page title or None) """ try: headers = { 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36' } response = requests.get(url, headers=headers, timeout=10) response.raise_for_status() soup = BeautifulSoup(response.content, 'html.parser') # Extract page title before stripping elements page_title = None if soup.title and soup.title.string: page_title = soup.title.string.strip() # Remove script and style elements for script in soup(["script", "style"]): script.decompose() # Get text text = soup.get_text() # Clean up whitespace lines = (line.strip() for line in text.splitlines()) chunks = (phrase.strip() for line in lines for phrase in line.split(" ")) text = ' '.join(chunk for chunk in chunks if chunk) return text, page_title except Exception as e: raise Exception(f"Error reading URL {url}: {str(e)}") def process_documents(self, pdf_paths: List[str] = None, urls: List[str] = None, pdf_urls: Dict[str, str] = None) -> List[Dict]: """ Process PDFs and URLs into chunks. Args: pdf_paths: List of PDF file paths urls: List of URLs to process pdf_urls: Optional dict mapping PDF filenames to their public URLs (for hyperlinking references) Returns: List of document chunks with metadata """ all_texts = [] all_metadata = [] pdf_urls = pdf_urls or {} # Process PDFs if pdf_paths: for pdf_path in pdf_paths: if not os.path.exists(pdf_path): print(f"Warning: PDF file not found: {pdf_path}") continue document_title = self.get_pdf_document_title(pdf_path) text = self.read_pdf(pdf_path) chunks = self.text_splitter.split_text(text) filename = os.path.basename(pdf_path) public_url = pdf_urls.get(filename) for i, chunk in enumerate(chunks): all_texts.append(chunk) meta = { 'source': pdf_path, 'document_title': document_title, 'type': 'pdf', 'chunk_index': i } if public_url: meta['url'] = public_url all_metadata.append(meta) # Process URLs if urls: for url in urls: try: text, page_title = self.read_url(url) chunks = self.text_splitter.split_text(text) # Use the page's