File size: 6,316 Bytes
f36c047
 
 
77addae
f36c047
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
75b750d
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
f36c047
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
7e62d3a
f36c047
7e62d3a
f36c047
7e62d3a
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
f36c047
7e62d3a
 
 
f36c047
7e62d3a
408d193
7e62d3a
 
 
f36c047
 
3b43d91
f36c047
 
 
7e62d3a
 
 
 
 
 
 
 
 
 
f36c047
7e62d3a
 
 
f36c047
7e62d3a
 
 
 
 
 
f36c047
7e62d3a
 
f36c047
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
import os
import uuid
import json
import concurrent.futures
from typing import List, Dict
from unstructured.partition.pdf import partition_pdf
from unstructured.chunking.title import chunk_by_title
from langchain_core.documents import Document
from langchain_pinecone import PineconeVectorStore
from langchain_core.messages import HumanMessage
from pinecone import Pinecone, ServerlessSpec

from config import *

# Initialize Pinecone Client
pc = Pinecone(api_key=PINECONE_API_KEY)

class SessionDocStore:
    """
    In-memory storage for heavy content (Images/Tables) tied to a session.
    In a real production app, use Redis/Postgres. 
    """
    def __init__(self):
        self.store = {}

    def save_chunk(self, doc_id: str, data: Dict):
        self.store[doc_id] = data

    def get_chunk(self, doc_id: str):
        return self.store.get(doc_id, {})
    
    def clear(self):
        self.store = {}
def cleanup_session_index(session_id: str):
    """
    Deletes the namespace from Pinecone. 
    Returns True if successful, raises Exception if failed.
    """
    if not session_id:
        print("⚠️ No session ID to clean.")
        return False
        
    print(f"🧹 Attempting to delete namespace: {session_id}")
    
    try:
        # Re-initialize index to ensure connection is fresh
        index = pc.Index(INDEX_NAME)
        
        # Check if index actually exists (Prevent silent failures)
        stats = index.describe_index_stats()
        
        # Execute Delete
        index.delete(delete_all=True, namespace=session_id)
        
        print(f"✅ Successfully deleted namespace: {session_id}")
        return True
        
    except Exception as e:
        error_msg = f"❌ Pinecone Delete Failed: {str(e)}"
        print(error_msg)
        raise Exception(error_msg) # Raise so App can see it

# Helper to summarize visual content for embeddings
def create_multimodal_summary(text, tables, images):
    llm = get_llm()
    prompt_text = f"Analyze content. TEXT: {text[:1000]}. INSTRUCTIONS: Summarize text and describe images/tables for retrieval."
    
    message_content = [{"type": "text", "text": prompt_text}]
    if images:
        for b64_str in images:
            if "," in b64_str: b64_str = b64_str.split(",")[1]
            message_content.append({
                "type": "image_url",
                "image_url": {"url": f"data:image/jpeg;base64,{b64_str}", "detail": "low"}
            })
            
    response = llm.invoke([HumanMessage(content=message_content)])
    return response.content

def process_single_chunk(i, chunk, doc_store):
    """
    Worker function to process a single chunk in a separate thread.
    """
    content = {'text': chunk.text, 'tables': [], 'images': []}
    
    # Extract visual data
    if hasattr(chunk, 'metadata') and hasattr(chunk.metadata, 'orig_elements'):
        for element in chunk.metadata.orig_elements:
            el_type = type(element).__name__
            if el_type == 'Table':
                content['tables'].append(getattr(element.metadata, 'text_as_html', element.text))
            elif el_type == 'Image' and hasattr(element.metadata, 'image_base64'):
                # OPTIONAL: Add Image Size filtering here to skip small icons
                content['images'].append(element.metadata.image_base64)

    # Deciding whether to call LLM (Slow) or just use text (Fast)
    if content['images'] or content['tables']:
        # This is the bottleneck we are parallelizing
        enhanced_text = create_multimodal_summary(content['text'], content['tables'], content['images'])
    else:
        enhanced_text = content['text']

    doc_id = str(uuid.uuid4())
    
    # Save heavy data to local store
    doc_store.save_chunk(doc_id, {
        "raw_text": content['text'],
        "tables": content['tables'],
        "images": content['images']
    })
    
    # Return the processed Document
    return Document(
        page_content=enhanced_text,
        metadata={"doc_id": doc_id, "chunk_index": i}
    )

def process_and_ingest(file_path: str, session_id: str, doc_store: SessionDocStore):
    print(f"📄 Partitioning: {file_path}")
    
    # ⚡ OPTIMIZATION #1: Changed strategy from "hi_res" to "auto"
    # "auto" is much faster. Use "hi_res" only if "auto" fails to read tables.
    elements = partition_pdf(
        filename=file_path, 
        strategy="auto", 
        infer_table_structure=True,
        extract_image_block_types=["Image", "Table"], 
        extract_image_block_to_payload=True
    )
    
    chunks = chunk_by_title(elements, max_characters=2000, new_after_n_chars=1500, combine_text_under_n_chars=300)
    
    documents = []
    
    print(f"🔄 Processing {len(chunks)} chunks in parallel...")
    
    # ⚡ OPTIMIZATION #2: ThreadPoolExecutor for Parallel Summarization
    # We use 5 workers. Going higher might hit Azure Rate Limits (429 Errors).
    with concurrent.futures.ThreadPoolExecutor(max_workers=5) as executor:
        # Submit all tasks
        future_to_chunk = {
            executor.submit(process_single_chunk, i, chunk, doc_store): i 
            for i, chunk in enumerate(chunks)
        }
        
        # Gather results as they complete (or strictly in order)
        # We iterate over the original range to maintain order
        futures_list = list(future_to_chunk.keys())
        
        for future in futures_list:
            try:
                doc = future.result() # Blocks until this specific chunk is done
                documents.append(doc)
            except Exception as e:
                print(f"❌ Error processing chunk: {e}")

    print(f"🔮 Ingesting {len(documents)} vectors to Namespace: {session_id}")
    
    PineconeVectorStore.from_documents(
        documents=documents,
        index_name=INDEX_NAME,
        embedding=get_embeddings(),
        namespace=session_id 
    )
    
    return documents # Return docs for BM25 initialization

def cleanup_session_index(session_id: str):
    """Deletes the specific namespace for the session"""
    try:
        index = pc.Index(INDEX_NAME)
        index.delete(delete_all=True, namespace=session_id)
        print(f"🗑️ Deleted Namespace: {session_id}")
    except Exception as e:
        print(f"⚠️ Error deleting namespace: {e}")