Spaces:
Sleeping
Sleeping
| from pathlib import Path | |
| from typing import Any | |
| from dotenv import load_dotenv | |
| load_dotenv() | |
| from chroma import get_collection # noqa: E402 β after load_dotenv | |
| DOCUMENTS_DIR = Path(__file__).parent / "documents" | |
| def parse_document(file_path: str | Path) -> list[dict[str, Any]]: | |
| """Return list of {text, page_number} dicts from a .pdf, .docx, or .md file.""" | |
| file_path = Path(file_path) | |
| suffix = file_path.suffix.lower() | |
| if suffix == ".pdf": | |
| from pypdf import PdfReader | |
| reader = PdfReader(str(file_path)) | |
| pages = [] | |
| for i, page in enumerate(reader.pages): | |
| text = page.extract_text() or "" | |
| if text.strip(): | |
| pages.append({"text": text, "page_number": i + 1}) | |
| return pages | |
| if suffix == ".docx": | |
| from docx import Document | |
| doc = Document(str(file_path)) | |
| full_text = "\n".join(p.text for p in doc.paragraphs if p.text.strip()) | |
| return [{"text": full_text, "page_number": None}] | |
| if suffix == ".md": | |
| text = file_path.read_text(encoding="utf-8") | |
| return [{"text": text, "page_number": None}] | |
| raise ValueError(f"Unsupported file type: {suffix!r}. Supported: .pdf, .docx, .md") | |
| def chunk_text( | |
| text: str, | |
| chunk_size: int = 500, | |
| overlap: int = 50, | |
| ) -> list[str]: | |
| """Split text into overlapping word-based chunks.""" | |
| words = text.split() | |
| if not words: | |
| return [] | |
| chunks: list[str] = [] | |
| start = 0 | |
| while start < len(words): | |
| end = min(start + chunk_size, len(words)) | |
| chunks.append(" ".join(words[start:end])) | |
| if end == len(words): | |
| break | |
| start += chunk_size - overlap | |
| return chunks | |
| def embed_and_store( | |
| chunks: list[str], | |
| metadata: dict[str, Any], | |
| ) -> None: | |
| """Embed chunks and upsert them into ChromaDB with per-chunk metadata.""" | |
| if not chunks: | |
| return | |
| collection = get_collection() | |
| source_file = metadata.get("source_file", "unknown") | |
| page_number = metadata.get("page_number") # may be None for docx | |
| ids = [ | |
| f"{source_file}__p{page_number}__c{i}" if page_number is not None else f"{source_file}__c{i}" | |
| for i in range(len(chunks)) | |
| ] | |
| chunk_metadata = [ | |
| { | |
| "source_file": source_file, | |
| "chunk_index": i, | |
| **({"page_number": page_number} if page_number is not None else {}), | |
| } | |
| for i in range(len(chunks)) | |
| ] | |
| collection.upsert(documents=chunks, metadatas=chunk_metadata, ids=ids) | |
| def ingest_all_documents(extra_dirs: list[Path] | None = None) -> dict[str, Any]: | |
| """Scan documents/ folder (and any extra_dirs) for supported files and index them.""" | |
| supported = {".pdf", ".docx", ".md"} | |
| search_dirs = [DOCUMENTS_DIR] + (extra_dirs or []) | |
| files: list[Path] = [] | |
| for d in search_dirs: | |
| if d.exists() and d.is_dir(): | |
| files.extend(f for f in d.iterdir() if f.suffix.lower() in supported) | |
| if not files: | |
| print("No documents found in", DOCUMENTS_DIR) | |
| return {"ingested": 0, "files": []} | |
| results: list[str] = [] | |
| total_chunks = 0 | |
| for file_path in files: | |
| print(f"Ingesting: {file_path.name}") | |
| try: | |
| pages = parse_document(file_path) | |
| file_chunks = 0 | |
| for page in pages: | |
| chunks = chunk_text(page["text"]) | |
| embed_and_store( | |
| chunks, | |
| metadata={ | |
| "source_file": file_path.name, | |
| "page_number": page["page_number"], | |
| }, | |
| ) | |
| file_chunks += len(chunks) | |
| total_chunks += file_chunks | |
| results.append(file_path.name) | |
| print(f" -> {file_chunks} chunks stored") | |
| except Exception as e: | |
| print(f" -> ERROR: {e}") | |
| print(f"\nDone. {len(results)} file(s), {total_chunks} total chunks.") | |
| return {"ingested": len(results), "total_chunks": total_chunks, "files": results} | |
| def ingest_scraped_content(data: dict) -> None: | |
| """Chunk and upsert scraped website content into ChromaDB.""" | |
| parts = [] | |
| if data.get("title"): | |
| parts.append(data["title"]) | |
| if data.get("description"): | |
| parts.append(data["description"]) | |
| if data.get("headings"): | |
| parts.append("\n".join(data["headings"])) | |
| if data.get("body_text"): | |
| parts.append(data["body_text"]) | |
| full_text = "\n\n".join(parts) | |
| chunks = chunk_text(full_text) | |
| if not chunks: | |
| return | |
| url = data.get("url", "scraped") | |
| scraped_at = data.get("scraped_at", "") | |
| embed_and_store( | |
| chunks, | |
| metadata={"source_file": url, "scraped_at": scraped_at}, | |
| ) | |
| if __name__ == "__main__": | |
| ingest_all_documents() | |