Spaces:
Sleeping
Sleeping
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}") |