File size: 4,212 Bytes
447450c | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 | """
Document Processor β parses uploaded files (PDF, DOCX, TXT, MD, PY)
into LangChain Documents ready for embedding and indexing.
"""
from __future__ import annotations
import io
import re
from pathlib import Path
from langchain_core.documents import Document
from langchain_text_splitters import RecursiveCharacterTextSplitter
from app.config import get_settings
from app.observability.logger import logger
SUPPORTED_EXTENSIONS = {".pdf", ".docx", ".txt", ".md", ".py", ".rst"}
def _extract_pdf(content: bytes, filename: str) -> str:
"""Extract text from PDF bytes using pypdf."""
try:
import pypdf
reader = pypdf.PdfReader(io.BytesIO(content))
return "\n\n".join(
page.extract_text() or "" for page in reader.pages
).strip()
except ImportError:
logger.warning("pypdf not installed β PDF extraction unavailable")
return ""
except Exception as exc:
logger.error(f"PDF extraction failed for {filename}: {exc}")
return ""
def _extract_docx(content: bytes, filename: str) -> str:
"""Extract text from DOCX bytes using python-docx."""
try:
import docx
doc = docx.Document(io.BytesIO(content))
return "\n\n".join(p.text for p in doc.paragraphs if p.text.strip())
except ImportError:
logger.warning("python-docx not installed β DOCX extraction unavailable")
return ""
except Exception as exc:
logger.error(f"DOCX extraction failed for {filename}: {exc}")
return ""
def _clean_text(text: str) -> str:
"""Normalize whitespace and remove null bytes."""
text = text.replace("\x00", "")
text = re.sub(r"\r\n", "\n", text)
text = re.sub(r"\n{4,}", "\n\n\n", text)
return text.strip()
def process_file(
content: bytes,
filename: str,
user_id: str,
doc_id: str,
) -> list[Document]:
"""
Parse uploaded file bytes into chunked LangChain Documents.
Args:
content: Raw file bytes.
filename: Original filename (used for extension detection).
user_id: Owner's user identifier.
doc_id: UUID assigned to this upload (for deletion tracking).
Returns:
List of Document objects ready for embedding.
"""
settings = get_settings()
suffix = Path(filename).suffix.lower()
if suffix not in SUPPORTED_EXTENSIONS:
raise ValueError(
f"Unsupported file type '{suffix}'. "
f"Supported: {', '.join(SUPPORTED_EXTENSIONS)}"
)
# ββ Extract raw text ββββββββββββββββββββββββββββββββββββββββββββββββββ
if suffix == ".pdf":
raw_text = _extract_pdf(content, filename)
elif suffix == ".docx":
raw_text = _extract_docx(content, filename)
else:
# TXT, MD, PY, RST β decode as UTF-8
try:
raw_text = content.decode("utf-8", errors="replace")
except Exception:
raw_text = content.decode("latin-1", errors="replace")
raw_text = _clean_text(raw_text)
if not raw_text:
raise ValueError(f"No extractable text found in '{filename}'")
# ββ Chunk βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
splitter = RecursiveCharacterTextSplitter(
chunk_size=settings.CHUNK_SIZE,
chunk_overlap=settings.CHUNK_OVERLAP,
separators=["\n\n\n", "\n\n", "\n", ". ", " "],
)
base_doc = Document(
page_content=raw_text,
metadata={
"source": "user_doc",
"filename": filename,
"user_id": user_id,
"doc_id": doc_id,
"file_type": suffix.lstrip("."),
"question_id": 0,
"q_score": 0,
},
)
chunks = splitter.split_documents([base_doc])
logger.info_data(
"Document processed",
filename=filename,
user_id=user_id,
doc_id=doc_id,
raw_chars=len(raw_text),
chunks=len(chunks),
)
return chunks
|