import os
import boto3
import gradio as gr
import pandas as pd
import torch
import importlib
import shutil
import logging
import fitz # PyMuPDF for image extraction
import base64
from io import BytesIO
from PIL import Image
from langchain_community.document_loaders import PyPDFLoader
from langchain.text_splitter import RecursiveCharacterTextSplitter
from langchain.vectorstores import FAISS
from langchain_community.embeddings import HuggingFaceEmbeddings
from langchain_core.prompts import PromptTemplate
from langchain_core.output_parsers import StrOutputParser
from langchain_core.runnables import RunnablePassthrough
from langchain_aws import ChatBedrock # Use Bedrock for Claude
from langchain_mistralai.chat_models import ChatMistralAI
from langchain_community.vectorstores import FAISS
# Configure logging
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
logger = logging.getLogger(__name__)
# Environment variables will be loaded from Hugging Face Spaces secrets
MISTRAL_API_KEY = os.environ.get("MISTRAL_API_KEY")
AWS_ACCESS_KEY = os.environ.get("AWS_ACCESS_KEY")
AWS_SECRET_KEY = os.environ.get("AWS_SECRET_KEY")
AWS_REGION = os.environ.get("AWS_REGION", "us-east-1")
# Global variables
use_proprietary = True # Default to Claude
pdfs_loaded = False
vector_store_loaded = False
chat_history = []
rag_pipeline = None
retriever = None
pdf_image_cache = {} # Cache for extracted images
# Configure AWS credentials for Bedrock
os.environ["AWS_ACCESS_KEY_ID"] = AWS_ACCESS_KEY
os.environ["AWS_SECRET_ACCESS_KEY"] = AWS_SECRET_KEY
os.environ["AWS_DEFAULT_REGION"] = AWS_REGION
# Function to extract images from PDFs
def extract_images_from_pdf(pdf_path):
"""Extract images from a PDF file and return them as base64 encoded strings."""
if pdf_path in pdf_image_cache:
return pdf_image_cache[pdf_path]
logger.info(f"Extracting images from {pdf_path}")
images = []
try:
# Open the PDF
doc = fitz.open(pdf_path)
# For each page
for page_num, page in enumerate(doc):
# Get images
image_list = page.get_images(full=True)
for img_index, img in enumerate(image_list):
# Get the XREF of the image
xref = img[0]
# Extract the image bytes
base_image = doc.extract_image(xref)
image_bytes = base_image["image"]
# Get the image extension
image_ext = base_image["ext"]
# Convert to PIL Image
image = Image.open(BytesIO(image_bytes))
# Convert to base64 for HTML display
buffered = BytesIO()
image.save(buffered, format="PNG")
img_str = base64.b64encode(buffered.getvalue()).decode()
# Store image info
images.append({
"base64": img_str,
"page": page_num + 1,
"index": img_index
})
# Cache the results
pdf_image_cache[pdf_path] = images
return images
except Exception as e:
logger.error(f"Error extracting images from {pdf_path}: {str(e)}")
return []
# Function to load PDFs from local directory
def load_pdfs_from_directory():
"""Load PDFs from multiple possible locations in the Hugging Face Space."""
logger.info("Loading PDFs from file system...")
# List of directories to check for PDFs
directories_to_check = [
"pdf_data", # Default directory
".", # Root directory
"/content", # Another common location
"/app", # HF Spaces app directory
os.path.expanduser("~") # Home directory
]
pdf_files = []
pdf_locations = {}
# Search for PDFs in each directory
for directory in directories_to_check:
if os.path.exists(directory) and os.path.isdir(directory):
logger.info(f"Checking directory: {directory}")
try:
# Check for PDFs in this directory
for f in os.listdir(directory):
if f.lower().endswith('.pdf'):
full_path = os.path.join(directory, f)
if os.path.isfile(full_path):
pdf_files.append(f)
pdf_locations[f] = full_path
logger.info(f"Found PDF: {f} at {full_path}")
except Exception as e:
logger.warning(f"Error checking directory {directory}: {str(e)}")
if not pdf_files:
# Try a more aggressive search with glob
import glob
logger.info("Performing deep search for PDFs...")
for directory in directories_to_check:
if os.path.exists(directory):
# Recursively search for PDFs
try:
for pdf_path in glob.glob(os.path.join(directory, "**/*.pdf"), recursive=True):
if os.path.isfile(pdf_path):
f = os.path.basename(pdf_path)
pdf_files.append(f)
pdf_locations[f] = pdf_path
logger.info(f"Deep search found PDF: {f} at {pdf_path}")
except Exception as e:
logger.warning(f"Error in deep search for {directory}: {str(e)}")
# If we found PDFs, ensure they're in the pdf_data directory
if pdf_files:
# Create pdf_data directory if it doesn't exist
os.makedirs("pdf_data", exist_ok=True)
# Copy all found PDFs to pdf_data if they're not already there
for pdf_file in pdf_files:
source_path = pdf_locations[pdf_file]
target_path = os.path.join("pdf_data", pdf_file)
# Skip if already in pdf_data
if os.path.normpath(source_path) == os.path.normpath(target_path):
continue
try:
shutil.copy2(source_path, target_path)
logger.info(f"Copied PDF to pdf_data: {pdf_file}")
except Exception as e:
logger.warning(f"Failed to copy {pdf_file}: {str(e)}")
# Final check - what's actually in pdf_data now?
if os.path.exists("pdf_data"):
pdf_data_files = [f for f in os.listdir("pdf_data") if f.lower().endswith('.pdf')]
if pdf_data_files:
logger.info(f"PDF data directory now contains {len(pdf_data_files)} PDFs: {pdf_data_files}")
global pdfs_loaded
pdfs_loaded = True
return True, f"Successfully loaded {len(pdf_data_files)} PDFs"
# If we still don't have PDFs, log specific PDFs we're looking for
expected_pdfs = [
"ACS580_Catalog_3AUA0000145061_RevP_EN.pdf",
"ACS580MV_catalog_3BHT490775R0001_RevF_EN.pdf",
"ACS5000_catalog_3BHT490501R0001_RevN_EN.pdf",
"ACS6080_catalog_3AUA0000221913_RevC_EN.pdf"
]
logger.warning(f"Specifically looking for these PDFs: {expected_pdfs}")
logger.warning("No PDF files found in any expected directory")
return False, "No PDF files found. Please ensure PDFs are uploaded to the Hugging Face Space."
# Function to process PDFs and create vector store
def process_pdfs_and_create_vectorstore():
"""Process local PDFs and create a FAISS vector store."""
logger.info("Starting processing of PDFs and creating vector store...")
# Check if PDFs are loaded
if not pdfs_loaded:
success, message = load_pdfs_from_directory()
if not success:
return False, message
# Create directories
os.makedirs("processed_data", exist_ok=True)
# Get all PDF files in the pdf_data directory
pdf_files = [f for f in os.listdir("pdf_data") if f.endswith('.pdf')]
if not pdf_files:
logger.warning("No PDF files found. Please upload PDFs to the pdf_data directory.")
return False, "No PDF files found. Please upload PDFs to the pdf_data directory."
# Initialize text splitter with improved parameters for technical content
text_splitter = RecursiveCharacterTextSplitter(
chunk_size=1000,
chunk_overlap=200,
separators=["\n\n", "\n", ". ", " ", ""]
)
# Load and process each PDF
all_chunks = []
for i, pdf_file in enumerate(pdf_files):
pdf_path = os.path.join("pdf_data", pdf_file)
logger.info(f"Processing PDF {i+1}/{len(pdf_files)}: {pdf_file}")
try:
loader = PyPDFLoader(pdf_path)
documents = loader.load()
# Enhance metadata
for doc in documents:
doc.metadata["source"] = pdf_file
doc.metadata["page"] = doc.metadata.get("page", 0) + 1 # Make page numbers 1-indexed
doc.metadata["total_pages"] = len(documents)
doc.metadata["title"] = pdf_file.replace(".pdf", "").replace("_", " ").title()
doc.metadata["pdf_path"] = pdf_path
# Split into chunks
chunks = text_splitter.split_documents(documents)
all_chunks.extend(chunks)
# Extract images
extract_images_from_pdf(pdf_path)
except Exception as e:
logger.error(f"Error processing {pdf_file}: {str(e)}")
if not all_chunks:
logger.warning("No content was extracted from the PDFs")
return False, "No content was extracted from the PDFs"
logger.info(f"Extracted {len(all_chunks)} text chunks from {len(pdf_files)} PDFs")
logger.info("Generating embeddings for semantic search...")
# Use a Sentence Transformer model for embeddings
embeddings = HuggingFaceEmbeddings(
model_name="sentence-transformers/all-MiniLM-L6-v2",
model_kwargs={'device': 'cuda' if torch.cuda.is_available() else 'cpu'}
)
logger.info("Building vector database for semantic search...")
# Create FAISS vector store
vectorstore = FAISS.from_documents(all_chunks, embeddings)
# Save the vector store
vectorstore.save_local("processed_data/faiss_index")
logger.info("Vector database created and saved successfully")
global vector_store_loaded
vector_store_loaded = True
return True, vectorstore
# Function to load existing vector store
def load_vectorstore():
"""Load an existing FAISS vector store or create if not exists."""
logger.info("Attempting to load existing vector store...")
if not os.path.exists("processed_data/faiss_index"):
logger.info("No existing vector database found. Creating new one...")
return process_pdfs_and_create_vectorstore()
try:
# Initialize embeddings
embeddings = HuggingFaceEmbeddings(
model_name="sentence-transformers/all-MiniLM-L6-v2",
model_kwargs={'device': 'cuda' if torch.cuda.is_available() else 'cpu'}
)
# Load the vector store
vectorstore = FAISS.load_local("processed_data/faiss_index", embeddings)
global vector_store_loaded
vector_store_loaded = True
logger.info("Vector database loaded successfully")
return True, vectorstore
except Exception as e:
logger.error(f"Error loading vector database: {str(e)}")
logger.info("Attempting to create new vector store...")
return process_pdfs_and_create_vectorstore()
# Function to initialize the RAG pipeline
def initialize_rag_pipeline(vectorstore):
"""Initialize the RAG pipeline with either AWS Bedrock Claude or Mistral AI."""
logger.info(f"Initializing RAG pipeline with {'AWS Bedrock Claude' if use_proprietary else 'Mistral AI'}")
retriever = vectorstore.as_retriever(
search_type="mmr", # Use Maximum Marginal Relevance for diverse results
search_kwargs={"k": 5, "fetch_k": 10}
)
if use_proprietary:
# Initialize Claude from AWS Bedrock
llm = ChatBedrock(
model_id="anthropic.claude-3-sonnet-20240229-v1:0",
model_kwargs={
"temperature": 0.3,
"max_tokens": 1024
},
region_name=AWS_REGION
)
else:
# Initialize Mistral AI model
llm = ChatMistralAI(
model="mistral-large-latest",
temperature=0.3,
mistral_api_key=MISTRAL_API_KEY
)
# Create a template for the RAG prompt
template = """
You are Ginnie, an expert AI assistant specializing in ABB industrial products and solutions.
Source: {source["title"]} (Page {source["page"]})
Your AI assistant for ABB product information
') # Chat interface with gr.Row(): with gr.Column(scale=3): # Chat interface with custom styling gr.HTML('