File size: 2,914 Bytes
194aaa5
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
# CLASS β€” orchestrates parse β†’ chunk β†’ embed pipeline
import os
import uuid
import hashlib
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy import select
from models.db import Document, Chunk
from lib.parser import parse_pdf, parse_docx, parse_txt
from lib.chunker import chunk_text
from services.embedding_service import EmbeddingService
from core.config import settings


class DocumentProcessor:
    def __init__(self, db: AsyncSession, embedding_service: EmbeddingService, user_id: uuid.UUID | None = None) -> None:
        self.db = db
        self.embedding_service = embedding_service
        self.user_id = user_id

    async def process_document(self, file_path: str, filename: str) -> Document:
        """
        Orchestrates the entire document ingestion pipeline:
        Upload β†’ Extract β†’ Chunk β†’ Embed β†’ Store
        Uses duplicate content checking and async batch insertion.
        """
        # Determine extraction strategy
        _, ext = os.path.splitext(filename.lower())
        if ext == ".pdf":
            text = parse_pdf(file_path)
        elif ext == ".docx":
            text = parse_docx(file_path)
        elif ext == ".txt":
            text = parse_txt(file_path)
        else:
            raise ValueError(f"Unsupported file extension: {ext}")

        if not text.strip():
            raise ValueError("Document is empty or no readable text could be extracted.")

        # Compute SHA-256 content hash to prevent duplicate storage
        content_hash = hashlib.sha256(text.encode("utf-8")).hexdigest()

        # Deduplication check
        stmt = select(Document).where(Document.content_hash == content_hash)
        result = await self.db.execute(stmt)
        existing_doc = result.scalars().first()
        if existing_doc:
            return existing_doc

        # Insert new Document entry in chunking state
        document = Document(
            user_id=self.user_id,
            filename=filename,
            content_hash=content_hash,
            file_path=file_path,
            status="chunking"
        )
        self.db.add(document)
        await self.db.flush()  # Populates document.id

        # Chunk the extracted text
        text_chunks = chunk_text(text, settings.CHUNK_SIZE, settings.CHUNK_OVERLAP)
        
        # Batch embed chunks
        embeddings = self.embedding_service.embed_chunks(text_chunks)

        # Batch insert chunks using SQLAlchemy objects
        for idx, (content, emb) in enumerate(zip(text_chunks, embeddings)):
            chunk = Chunk(
                document_id=document.id,
                content=content,
                embedding=emb,
                chunk_index=idx
            )
            self.db.add(chunk)

        # Transition status to ready
        document.status = "ready"
        await self.db.commit()
        await self.db.refresh(document)

        return document