Spaces:
Sleeping
Sleeping
Upload 6 files
Browse files- Dockerfile +20 -8
- app.py +160 -0
- config.py +33 -0
- data_processing.py +117 -0
- rag_engine.py +126 -0
- requirements.txt +15 -3
Dockerfile
CHANGED
|
@@ -1,20 +1,32 @@
|
|
| 1 |
-
|
|
|
|
| 2 |
|
|
|
|
| 3 |
WORKDIR /app
|
| 4 |
|
|
|
|
|
|
|
| 5 |
RUN apt-get update && apt-get install -y \
|
| 6 |
build-essential \
|
| 7 |
curl \
|
| 8 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 9 |
&& rm -rf /var/lib/apt/lists/*
|
| 10 |
|
| 11 |
-
|
| 12 |
-
COPY
|
| 13 |
|
| 14 |
-
|
|
|
|
| 15 |
|
| 16 |
-
|
|
|
|
| 17 |
|
| 18 |
-
|
|
|
|
| 19 |
|
| 20 |
-
|
|
|
|
|
|
| 1 |
+
# 1. Base Image
|
| 2 |
+
FROM python:3.11-slim
|
| 3 |
|
| 4 |
+
# 2. Set Working Directory
|
| 5 |
WORKDIR /app
|
| 6 |
|
| 7 |
+
# 3. Install System Dependencies
|
| 8 |
+
# ADDED: libgl1 and libglib2.0-0 (Fixes ImportError: libGL.so.1)
|
| 9 |
RUN apt-get update && apt-get install -y \
|
| 10 |
build-essential \
|
| 11 |
curl \
|
| 12 |
+
poppler-utils \
|
| 13 |
+
tesseract-ocr \
|
| 14 |
+
libmagic1 \
|
| 15 |
+
libgl1 \
|
| 16 |
+
libglib2.0-0 \
|
| 17 |
&& rm -rf /var/lib/apt/lists/*
|
| 18 |
|
| 19 |
+
# 4. Copy Requirements
|
| 20 |
+
COPY requirements.txt .
|
| 21 |
|
| 22 |
+
# 5. Install Python Dependencies
|
| 23 |
+
RUN pip install --no-cache-dir -r requirements.txt
|
| 24 |
|
| 25 |
+
# 6. Copy Application Code
|
| 26 |
+
COPY . .
|
| 27 |
|
| 28 |
+
# 7. Expose Port
|
| 29 |
+
EXPOSE 8501
|
| 30 |
|
| 31 |
+
# 8. Run
|
| 32 |
+
CMD ["streamlit", "run", "app.py", "--server.port=8501", "--server.address=0.0.0.0"]
|
app.py
ADDED
|
@@ -0,0 +1,160 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import streamlit as st
|
| 2 |
+
import uuid
|
| 3 |
+
import os
|
| 4 |
+
import base64
|
| 5 |
+
from langchain_community.retrievers import BM25Retriever
|
| 6 |
+
from langchain_core.messages import HumanMessage, AIMessage
|
| 7 |
+
|
| 8 |
+
from data_processing import process_and_ingest, SessionDocStore, cleanup_session_index
|
| 9 |
+
from rag_engine import run_advanced_rag
|
| 10 |
+
|
| 11 |
+
# ──────────────────────────────────────────────────────────────────
|
| 12 |
+
# 1. PAGE CONFIG & STATE INITIALIZATION
|
| 13 |
+
# ──────────────────────────────────────────────────────────────────
|
| 14 |
+
|
| 15 |
+
st.set_page_config(
|
| 16 |
+
page_title="Deep RAG Analyzer",
|
| 17 |
+
page_icon="🤖",
|
| 18 |
+
layout="wide"
|
| 19 |
+
)
|
| 20 |
+
|
| 21 |
+
# Initialize Session State
|
| 22 |
+
if "session_id" not in st.session_state:
|
| 23 |
+
st.session_state.session_id = str(uuid.uuid4())
|
| 24 |
+
|
| 25 |
+
if "doc_store" not in st.session_state:
|
| 26 |
+
st.session_state.doc_store = SessionDocStore()
|
| 27 |
+
|
| 28 |
+
if "messages" not in st.session_state:
|
| 29 |
+
st.session_state.messages = [] # Format: {"role": "user/assistant", "content": "text", "images": []}
|
| 30 |
+
|
| 31 |
+
if "bm25" not in st.session_state:
|
| 32 |
+
st.session_state.bm25 = None
|
| 33 |
+
|
| 34 |
+
if "processed_file" not in st.session_state:
|
| 35 |
+
st.session_state.processed_file = None
|
| 36 |
+
|
| 37 |
+
if "uploader_key" not in st.session_state:
|
| 38 |
+
st.session_state.uploader_key = str(uuid.uuid4())
|
| 39 |
+
|
| 40 |
+
# ──────────────────────────────────────────────────────────────────
|
| 41 |
+
# 2. SIDEBAR (Upload & Reset)
|
| 42 |
+
# ──────────────────────────────────────────────────────────────────
|
| 43 |
+
|
| 44 |
+
with st.sidebar:
|
| 45 |
+
st.title("📁 Document Upload")
|
| 46 |
+
uploaded_file = st.file_uploader("Upload PDF", type=["pdf"],key=st.session_state.uploader_key)
|
| 47 |
+
|
| 48 |
+
if uploaded_file and uploaded_file.name != st.session_state.processed_file:
|
| 49 |
+
with st.spinner("Partitioning & Embedding (This may take a moment)..."):
|
| 50 |
+
# Save to temp file for processing
|
| 51 |
+
temp_path = f"temp_{uploaded_file.name}"
|
| 52 |
+
with open(temp_path, "wb") as f:
|
| 53 |
+
f.write(uploaded_file.getbuffer())
|
| 54 |
+
|
| 55 |
+
try:
|
| 56 |
+
# Run Pipeline
|
| 57 |
+
documents = process_and_ingest(
|
| 58 |
+
temp_path,
|
| 59 |
+
st.session_state.session_id,
|
| 60 |
+
st.session_state.doc_store
|
| 61 |
+
)
|
| 62 |
+
|
| 63 |
+
# Setup Retriever
|
| 64 |
+
bm25 = BM25Retriever.from_documents(documents)
|
| 65 |
+
bm25.k = 3
|
| 66 |
+
st.session_state.bm25 = bm25
|
| 67 |
+
st.session_state.processed_file = uploaded_file.name
|
| 68 |
+
|
| 69 |
+
st.success(f"Processed {len(documents)} chunks!")
|
| 70 |
+
except Exception as e:
|
| 71 |
+
st.error(f"Error: {e}")
|
| 72 |
+
finally:
|
| 73 |
+
# Cleanup temp file
|
| 74 |
+
if os.path.exists(temp_path):
|
| 75 |
+
os.remove(temp_path)
|
| 76 |
+
|
| 77 |
+
st.markdown("---")
|
| 78 |
+
if st.button("🗑️ Clear Chat & Reset"):
|
| 79 |
+
# Cleanup Pinecone
|
| 80 |
+
cleanup_session_index(st.session_state.session_id)
|
| 81 |
+
# Reset State
|
| 82 |
+
st.session_state.session_id = str(uuid.uuid4())
|
| 83 |
+
st.session_state.doc_store = SessionDocStore()
|
| 84 |
+
st.session_state.messages = []
|
| 85 |
+
st.session_state.bm25 = None
|
| 86 |
+
st.session_state.processed_file = None
|
| 87 |
+
st.session_state.uploader_key = str(uuid.uuid4())
|
| 88 |
+
st.rerun()
|
| 89 |
+
|
| 90 |
+
# ──────────────────────────────────────────────────────────────────
|
| 91 |
+
# 3. CHAT INTERFACE
|
| 92 |
+
# ──────────────────────────────────────────────────────────────────
|
| 93 |
+
|
| 94 |
+
st.title("🤖 Advanced Multimodal RAG (with chat history and visuals)")
|
| 95 |
+
st.caption("Using GPT-4.1, Pinecone, RRF and Re-ranking (Do not upload extremly large PDFs!)")
|
| 96 |
+
|
| 97 |
+
# Display History
|
| 98 |
+
for msg in st.session_state.messages:
|
| 99 |
+
with st.chat_message(msg["role"]):
|
| 100 |
+
st.markdown(msg["content"])
|
| 101 |
+
if "images" in msg and msg["images"]:
|
| 102 |
+
# Display images in a row
|
| 103 |
+
cols = st.columns(len(msg["images"]))
|
| 104 |
+
for idx, img_b64 in enumerate(msg["images"]):
|
| 105 |
+
with cols[idx]:
|
| 106 |
+
if "," in img_b64: img_b64 = img_b64.split(",")[1]
|
| 107 |
+
st.image(base64.b64decode(img_b64), use_container_width=True)
|
| 108 |
+
|
| 109 |
+
# Chat Input
|
| 110 |
+
if prompt := st.chat_input("Ask about your document..."):
|
| 111 |
+
|
| 112 |
+
if not st.session_state.bm25:
|
| 113 |
+
st.error("Please upload and process a PDF first!")
|
| 114 |
+
st.stop()
|
| 115 |
+
|
| 116 |
+
# 1. Display User Message
|
| 117 |
+
st.chat_message("user").markdown(prompt)
|
| 118 |
+
st.session_state.messages.append({"role": "user", "content": prompt})
|
| 119 |
+
|
| 120 |
+
# 2. Prepare History for RAG
|
| 121 |
+
lc_history = []
|
| 122 |
+
for m in st.session_state.messages:
|
| 123 |
+
if m["role"] == "user":
|
| 124 |
+
lc_history.append(HumanMessage(content=m["content"]))
|
| 125 |
+
else:
|
| 126 |
+
lc_history.append(AIMessage(content=m["content"]))
|
| 127 |
+
|
| 128 |
+
# 3. Generate Response
|
| 129 |
+
with st.chat_message("assistant"):
|
| 130 |
+
with st.spinner("Thinking (retrieving the context)..."):
|
| 131 |
+
try:
|
| 132 |
+
answer, images = run_advanced_rag(
|
| 133 |
+
prompt,
|
| 134 |
+
st.session_state.session_id,
|
| 135 |
+
st.session_state.bm25,
|
| 136 |
+
st.session_state.doc_store,
|
| 137 |
+
lc_history
|
| 138 |
+
)
|
| 139 |
+
|
| 140 |
+
st.markdown(answer)
|
| 141 |
+
|
| 142 |
+
# Display Images if found
|
| 143 |
+
if images:
|
| 144 |
+
st.write("---")
|
| 145 |
+
st.caption("📸 Retrieved Visual Context:")
|
| 146 |
+
cols = st.columns(min(3, len(images)))
|
| 147 |
+
for idx, img_b64 in enumerate(images[:3]):
|
| 148 |
+
with cols[idx]:
|
| 149 |
+
if "," in img_b64: img_b64 = img_b64.split(",")[1]
|
| 150 |
+
st.image(base64.b64decode(img_b64), use_container_width=True)
|
| 151 |
+
|
| 152 |
+
# Save to history
|
| 153 |
+
st.session_state.messages.append({
|
| 154 |
+
"role": "assistant",
|
| 155 |
+
"content": answer,
|
| 156 |
+
"images": images[:3] if images else []
|
| 157 |
+
})
|
| 158 |
+
|
| 159 |
+
except Exception as e:
|
| 160 |
+
st.error(f"Error generating response: {e}")
|
config.py
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
from dotenv import load_dotenv
|
| 3 |
+
from langchain_huggingface import HuggingFaceEmbeddings
|
| 4 |
+
from langchain_openai import AzureChatOpenAI
|
| 5 |
+
|
| 6 |
+
load_dotenv()
|
| 7 |
+
|
| 8 |
+
# 1. API KEYS & CONFIG
|
| 9 |
+
# ⚠️ Ensure these are set in your Hugging Face Space "Secrets" setting
|
| 10 |
+
PINECONE_API_KEY = os.getenv("PINECONE_API_KEY")
|
| 11 |
+
INDEX_NAME = "gradio-db" # 768 dimensions
|
| 12 |
+
|
| 13 |
+
AZURE_OPENAI_API_KEY = os.getenv("AZURE_OPENAI_API_KEY")
|
| 14 |
+
AZURE_OPENAI_ENDPOINT = os.getenv("AZURE_OPENAI_ENDPOINT")
|
| 15 |
+
AZURE_OPENAI_API_VERSION = "2025-01-01-preview" # Updated to stable preview
|
| 16 |
+
AZURE_DEPLOYMENT_NAME = "gpt-4.1"
|
| 17 |
+
|
| 18 |
+
# 2. MODEL INITIALIZATION
|
| 19 |
+
|
| 20 |
+
def get_llm(temperature=0.25):
|
| 21 |
+
|
| 22 |
+
return AzureChatOpenAI(
|
| 23 |
+
azure_deployment=AZURE_DEPLOYMENT_NAME,
|
| 24 |
+
api_version=AZURE_OPENAI_API_VERSION,
|
| 25 |
+
temperature=temperature,
|
| 26 |
+
max_tokens=1024,
|
| 27 |
+
streaming=True
|
| 28 |
+
)
|
| 29 |
+
|
| 30 |
+
def get_embeddings():
|
| 31 |
+
# IntFloat E5-Base v2 (768 dim)
|
| 32 |
+
return HuggingFaceEmbeddings(model_name="intfloat/e5-base-v2")
|
| 33 |
+
|
data_processing.py
ADDED
|
@@ -0,0 +1,117 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
import uuid
|
| 3 |
+
import json
|
| 4 |
+
from typing import List, Dict
|
| 5 |
+
from unstructured.partition.pdf import partition_pdf
|
| 6 |
+
from unstructured.chunking.title import chunk_by_title
|
| 7 |
+
from langchain_core.documents import Document
|
| 8 |
+
from langchain_pinecone import PineconeVectorStore
|
| 9 |
+
from langchain_core.messages import HumanMessage
|
| 10 |
+
from pinecone import Pinecone, ServerlessSpec
|
| 11 |
+
|
| 12 |
+
from config import *
|
| 13 |
+
|
| 14 |
+
# Initialize Pinecone Client
|
| 15 |
+
pc = Pinecone(api_key=PINECONE_API_KEY)
|
| 16 |
+
|
| 17 |
+
class SessionDocStore:
|
| 18 |
+
"""
|
| 19 |
+
In-memory storage for heavy content (Images/Tables) tied to a session.
|
| 20 |
+
In a real production app, use Redis/Postgres.
|
| 21 |
+
"""
|
| 22 |
+
def __init__(self):
|
| 23 |
+
self.store = {}
|
| 24 |
+
|
| 25 |
+
def save_chunk(self, doc_id: str, data: Dict):
|
| 26 |
+
self.store[doc_id] = data
|
| 27 |
+
|
| 28 |
+
def get_chunk(self, doc_id: str):
|
| 29 |
+
return self.store.get(doc_id, {})
|
| 30 |
+
|
| 31 |
+
def clear(self):
|
| 32 |
+
self.store = {}
|
| 33 |
+
|
| 34 |
+
# Helper to summarize visual content for embeddings
|
| 35 |
+
def create_multimodal_summary(text, tables, images):
|
| 36 |
+
llm = get_llm()
|
| 37 |
+
prompt_text = f"Analyze content. TEXT: {text[:1000]}. INSTRUCTIONS: Summarize text and describe images/tables for retrieval."
|
| 38 |
+
|
| 39 |
+
message_content = [{"type": "text", "text": prompt_text}]
|
| 40 |
+
if images:
|
| 41 |
+
for b64_str in images:
|
| 42 |
+
if "," in b64_str: b64_str = b64_str.split(",")[1]
|
| 43 |
+
message_content.append({
|
| 44 |
+
"type": "image_url",
|
| 45 |
+
"image_url": {"url": f"data:image/jpeg;base64,{b64_str}", "detail": "low"}
|
| 46 |
+
})
|
| 47 |
+
|
| 48 |
+
response = llm.invoke([HumanMessage(content=message_content)])
|
| 49 |
+
return response.content
|
| 50 |
+
|
| 51 |
+
def process_and_ingest(file_path: str, session_id: str, doc_store: SessionDocStore):
|
| 52 |
+
"""
|
| 53 |
+
Parses PDF, creates summary/heavy splits, uploads to Pinecone Namespace.
|
| 54 |
+
"""
|
| 55 |
+
print(f"📄 Partitioning: {file_path}")
|
| 56 |
+
elements = partition_pdf(
|
| 57 |
+
filename=file_path, strategy="hi_res", infer_table_structure=True,
|
| 58 |
+
extract_image_block_types=["Image", "Table"], extract_image_block_to_payload=True
|
| 59 |
+
)
|
| 60 |
+
|
| 61 |
+
chunks = chunk_by_title(elements, max_characters=3000, new_after_n_chars=2400, combine_text_under_n_chars=500)
|
| 62 |
+
|
| 63 |
+
documents = []
|
| 64 |
+
|
| 65 |
+
for i, chunk in enumerate(chunks):
|
| 66 |
+
content = {'text': chunk.text, 'tables': [], 'images': []}
|
| 67 |
+
|
| 68 |
+
# Extract visual data
|
| 69 |
+
if hasattr(chunk, 'metadata') and hasattr(chunk.metadata, 'orig_elements'):
|
| 70 |
+
for element in chunk.metadata.orig_elements:
|
| 71 |
+
el_type = type(element).__name__
|
| 72 |
+
if el_type == 'Table':
|
| 73 |
+
content['tables'].append(getattr(element.metadata, 'text_as_html', element.text))
|
| 74 |
+
elif el_type == 'Image' and hasattr(element.metadata, 'image_base64'):
|
| 75 |
+
content['images'].append(element.metadata.image_base64)
|
| 76 |
+
|
| 77 |
+
# Summarize if heavy content exists
|
| 78 |
+
if content['images'] or content['tables']:
|
| 79 |
+
enhanced_text = create_multimodal_summary(content['text'], content['tables'], content['images'])
|
| 80 |
+
else:
|
| 81 |
+
enhanced_text = content['text']
|
| 82 |
+
|
| 83 |
+
doc_id = str(uuid.uuid4())
|
| 84 |
+
|
| 85 |
+
# Save heavy data to local in-memory store
|
| 86 |
+
doc_store.save_chunk(doc_id, {
|
| 87 |
+
"raw_text": content['text'],
|
| 88 |
+
"tables": content['tables'],
|
| 89 |
+
"images": content['images']
|
| 90 |
+
})
|
| 91 |
+
|
| 92 |
+
# Create light document for Pinecone
|
| 93 |
+
doc = Document(
|
| 94 |
+
page_content=enhanced_text, # "query: " prefix added automatically by some embeddings, but explicit here
|
| 95 |
+
metadata={"doc_id": doc_id, "chunk_index": i}
|
| 96 |
+
)
|
| 97 |
+
documents.append(doc)
|
| 98 |
+
|
| 99 |
+
# Ingest to Pinecone (Namespace = Session ID)
|
| 100 |
+
print(f"🔮 Ingesting to Namespace: {session_id}")
|
| 101 |
+
PineconeVectorStore.from_documents(
|
| 102 |
+
documents=documents,
|
| 103 |
+
index_name=INDEX_NAME,
|
| 104 |
+
embedding=get_embeddings(),
|
| 105 |
+
namespace=session_id
|
| 106 |
+
)
|
| 107 |
+
|
| 108 |
+
return documents # Return docs for BM25 initialization
|
| 109 |
+
|
| 110 |
+
def cleanup_session_index(session_id: str):
|
| 111 |
+
"""Deletes the specific namespace for the session"""
|
| 112 |
+
try:
|
| 113 |
+
index = pc.Index(INDEX_NAME)
|
| 114 |
+
index.delete(delete_all=True, namespace=session_id)
|
| 115 |
+
print(f"🗑️ Deleted Namespace: {session_id}")
|
| 116 |
+
except Exception as e:
|
| 117 |
+
print(f"⚠️ Error deleting namespace: {e}")
|
rag_engine.py
ADDED
|
@@ -0,0 +1,126 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import logging
|
| 2 |
+
from typing import List
|
| 3 |
+
from langchain_pinecone import PineconeVectorStore
|
| 4 |
+
from langchain_core.documents import Document
|
| 5 |
+
from langchain_core.messages import HumanMessage
|
| 6 |
+
from flashrank import Ranker, RerankRequest
|
| 7 |
+
from config import *
|
| 8 |
+
|
| 9 |
+
# Logging setup
|
| 10 |
+
logging.basicConfig(level=logging.INFO)
|
| 11 |
+
logger = logging.getLogger(__name__)
|
| 12 |
+
|
| 13 |
+
# ⚡ OPTIMIZATION 1: Use FlashRank (Ultra-fast CPU Re-ranking)
|
| 14 |
+
ranker = Ranker(model_name="ms-marco-MiniLM-L-12-v2", cache_dir="/tmp/flashrank_cache")
|
| 15 |
+
|
| 16 |
+
def reciprocal_rank_fusion(results: List[List[Document]], k=60):
|
| 17 |
+
fused_scores = {}
|
| 18 |
+
doc_map = {}
|
| 19 |
+
for docs in results:
|
| 20 |
+
for rank, doc in enumerate(docs):
|
| 21 |
+
doc_id = doc.metadata.get("doc_id")
|
| 22 |
+
if doc_id not in doc_map: doc_map[doc_id] = doc
|
| 23 |
+
if doc_id not in fused_scores: fused_scores[doc_id] = 0
|
| 24 |
+
fused_scores[doc_id] += 1 / (rank + k)
|
| 25 |
+
|
| 26 |
+
reranked_ids = sorted(fused_scores, key=fused_scores.get, reverse=True)
|
| 27 |
+
return [doc_map[doc_id] for doc_id in reranked_ids]
|
| 28 |
+
|
| 29 |
+
def rerank_documents(query: str, docs: List[Document], top_n=5):
|
| 30 |
+
"""
|
| 31 |
+
Optimized Re-ranking using FlashRank (runs in milliseconds).
|
| 32 |
+
"""
|
| 33 |
+
if not docs: return []
|
| 34 |
+
|
| 35 |
+
try:
|
| 36 |
+
# Prepare format for FlashRank
|
| 37 |
+
passages = [
|
| 38 |
+
{"id": str(i), "text": doc.page_content, "meta": doc.metadata}
|
| 39 |
+
for i, doc in enumerate(docs)
|
| 40 |
+
]
|
| 41 |
+
|
| 42 |
+
# Rerank
|
| 43 |
+
rerank_request = RerankRequest(query=query, passages=passages)
|
| 44 |
+
results = ranker.rerank(rerank_request)
|
| 45 |
+
|
| 46 |
+
# Convert back to Document objects
|
| 47 |
+
final_docs = []
|
| 48 |
+
for res in results[:top_n]:
|
| 49 |
+
final_docs.append(Document(page_content=res["text"], metadata=res["meta"]))
|
| 50 |
+
|
| 51 |
+
return final_docs
|
| 52 |
+
except Exception as e:
|
| 53 |
+
logger.error(f"FlashRank failed: {e}")
|
| 54 |
+
return docs[:top_n] # Fallback
|
| 55 |
+
|
| 56 |
+
def run_advanced_rag(query: str, session_id: str, bm25_retriever, doc_store, chat_history):
|
| 57 |
+
# ⚡ OPTIMIZATION 2: Skip LLM Query Decomposition (Saves 3-5s)
|
| 58 |
+
# We treat the user query as the only query.
|
| 59 |
+
queries = [query]
|
| 60 |
+
|
| 61 |
+
# ⚡ OPTIMIZATION 3: Standard Similarity Search (Faster than MMR)
|
| 62 |
+
vectorstore = PineconeVectorStore.from_existing_index(
|
| 63 |
+
index_name=INDEX_NAME, embedding=get_embeddings(), namespace=session_id
|
| 64 |
+
)
|
| 65 |
+
dense_retriever = vectorstore.as_retriever(search_type="similarity", search_kwargs={"k": 3})
|
| 66 |
+
|
| 67 |
+
all_docs = []
|
| 68 |
+
|
| 69 |
+
# Hybrid Search
|
| 70 |
+
for q in queries:
|
| 71 |
+
dense_docs = dense_retriever.invoke(f"query: {q}")
|
| 72 |
+
sparse_docs = bm25_retriever.invoke(q)
|
| 73 |
+
# Fuse results
|
| 74 |
+
all_docs.extend(reciprocal_rank_fusion([dense_docs, sparse_docs]))
|
| 75 |
+
|
| 76 |
+
# Deduplicate by ID
|
| 77 |
+
unique_docs = {d.metadata["doc_id"]: d for d in all_docs}
|
| 78 |
+
|
| 79 |
+
# Re-rank (Fast)
|
| 80 |
+
final_docs = rerank_documents(query, list(unique_docs.values()), top_n=5)
|
| 81 |
+
|
| 82 |
+
# Context Construction
|
| 83 |
+
context_text = ""
|
| 84 |
+
retrieved_images = []
|
| 85 |
+
seen_imgs = set()
|
| 86 |
+
|
| 87 |
+
for i, doc in enumerate(final_docs):
|
| 88 |
+
# Fetch heavy content from in-memory store
|
| 89 |
+
heavy = doc_store.get_chunk(doc.metadata["doc_id"])
|
| 90 |
+
|
| 91 |
+
context_text += f"\n--- Source {i+1} ---\n{heavy.get('raw_text', '')}\n"
|
| 92 |
+
for t in heavy.get('tables', []):
|
| 93 |
+
context_text += f"[Table]: {t}\n"
|
| 94 |
+
|
| 95 |
+
# Collect images
|
| 96 |
+
for img in heavy.get('images', []):
|
| 97 |
+
if img not in seen_imgs:
|
| 98 |
+
seen_imgs.add(img)
|
| 99 |
+
retrieved_images.append(img)
|
| 100 |
+
|
| 101 |
+
# Final Generation
|
| 102 |
+
llm = get_llm()
|
| 103 |
+
|
| 104 |
+
# Limit history to last 2 turns to save tokens/time
|
| 105 |
+
hist_text = "\n".join([f"{m.type}: {m.content}" for m in chat_history[-2:]])
|
| 106 |
+
|
| 107 |
+
prompt = f"""
|
| 108 |
+
Answer the user question based on the provided context.
|
| 109 |
+
CHAT HISTORY: {hist_text}
|
| 110 |
+
CONTEXT: {context_text[:5000]}
|
| 111 |
+
QUESTION: {query}
|
| 112 |
+
"""
|
| 113 |
+
|
| 114 |
+
msg_content = [{"type": "text", "text": prompt}]
|
| 115 |
+
|
| 116 |
+
# ⚡ OPTIMIZATION 4: Limit Images to top 2
|
| 117 |
+
for b64 in retrieved_images[:2]:
|
| 118 |
+
if "," in b64: b64 = b64.split(",")[1]
|
| 119 |
+
msg_content.append({
|
| 120 |
+
"type": "image_url",
|
| 121 |
+
"image_url": {"url": f"data:image/jpeg;base64,{b64}"}
|
| 122 |
+
})
|
| 123 |
+
|
| 124 |
+
response = llm.invoke([HumanMessage(content=msg_content)])
|
| 125 |
+
|
| 126 |
+
return response.content, retrieved_images
|
requirements.txt
CHANGED
|
@@ -1,3 +1,15 @@
|
|
| 1 |
-
|
| 2 |
-
|
| 3 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
FlashRank==0.2.10
|
| 2 |
+
langchain==1.2.0
|
| 3 |
+
openai==2.11.0
|
| 4 |
+
langchain_community==0.4.1
|
| 5 |
+
langchain_core==1.2.6
|
| 6 |
+
langchain_huggingface==1.2.0
|
| 7 |
+
langchain_openai==1.1.6
|
| 8 |
+
langchain_pinecone==0.2.13
|
| 9 |
+
pinecone==7.3.0
|
| 10 |
+
python-dotenv==1.2.1
|
| 11 |
+
streamlit==1.44.1
|
| 12 |
+
unstructured[all-docs]==0.17.2
|
| 13 |
+
pdfminer.six
|
| 14 |
+
pi-heif==1.1.0
|
| 15 |
+
|