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. {context} Human: {question} Assistant: """ # Create the prompt prompt = PromptTemplate.from_template(template) # Create the chain rag_chain = ( {"context": retriever, "question": RunnablePassthrough()} | prompt | llm | StrOutputParser() ) return rag_chain, retriever # Function to get source documents from retriever def get_source_documents(retriever, query): """Get source documents for a query.""" docs = retriever.get_relevant_documents(query) sources = [] for i, doc in enumerate(docs): source_info = { "title": doc.metadata.get("title", "Unknown"), "source": doc.metadata.get("source", "Unknown"), "page": doc.metadata.get("page", "Unknown"), "pdf_path": doc.metadata.get("pdf_path", ""), "excerpt": doc.page_content[:200] + "..." if len(doc.page_content) > 200 else doc.page_content } sources.append(source_info) return sources # Function to format source citations and include relevant images def format_sources_with_images(sources, include_images=True): """Format sources for display with optional images.""" if not sources: return "" source_text = "\n\n**Sources:**\n" # Create a set to track unique sources unique_sources = set() images_html = "" for source in sources: source_key = f"{source['source']}_{source['page']}" if source_key not in unique_sources: unique_sources.add(source_key) source_text += f"- **{source['title']}** (Page {source['page']})\n" # Add images if requested and available if include_images and source.get("pdf_path") and os.path.exists(source["pdf_path"]): # Find images for this page page_images = [img for img in extract_images_from_pdf(source["pdf_path"]) if img["page"] == source["page"]] # Add up to 2 images per page to avoid clutter for i, img in enumerate(page_images[:2]): images_html += f'
Image from {source[

Source: {source["title"]} (Page {source["page"]})

' # Add images section if any images were found if images_html: source_text += "\n\n**Relevant Visuals:**\n" source_text += f"
{images_html}
" return source_text # System setup function def setup_system(): """Perform complete system setup with improved error handling.""" global rag_pipeline, retriever logger.info("Starting system setup...") # Step 1: Load PDFs if needed if not pdfs_loaded: success, message = load_pdfs_from_directory() if not success: logger.warning(f"PDF loading failed: {message}") # List files in current directory for debugging try: logger.info(f"Files in current directory: {os.listdir('.')}") if os.path.exists("pdf_data"): logger.info(f"Files in pdf_data directory: {os.listdir('pdf_data')}") except Exception as e: logger.error(f"Error listing directories: {str(e)}") # Step 2: Load or create vector store success, result = load_vectorstore() if success and isinstance(result, FAISS): # Step 3: Initialize RAG pipeline rag_pipeline, retriever = initialize_rag_pipeline(result) logger.info("RAG pipeline initialized successfully") return True else: logger.error("Failed to set up the system") # Print some system information for debugging logger.info(f"Current working directory: {os.getcwd()}") logger.info(f"Environment variables: PDF_PATH={os.environ.get('PDF_PATH')}") return False # Message processing function def process_message(message, chatbot_history): """Process user message and generate response with optional images.""" global chat_history, rag_pipeline, retriever if not message: return chatbot_history # Add user message to history chatbot_history.append((message, "")) # Check if system is ready if not vector_store_loaded or rag_pipeline is None or retriever is None: # Try to setup the system if setup_system(): response = "I've just finished setting up the ABB product information system. I can now answer your question." else: response = "I'm having trouble setting up the system. Please check the logs for more information." chatbot_history[-1] = (message, response) return chatbot_history try: # Get sources sources = get_source_documents(retriever, message) # Check if the query is about images image_request = any(term in message.lower() for term in ["image", "picture", "photo", "visual", "diagram", "figure", "show me"]) # Generate response response = rag_pipeline.invoke(message) # Format response with sources and images if requested formatted_response = response + format_sources_with_images(sources, include_images=image_request) # Update chatbot history chatbot_history[-1] = (message, formatted_response) except Exception as e: # Handle errors error_message = f"I encountered an error: {str(e)}. Please try again." chatbot_history[-1] = (message, error_message) return chatbot_history # Function to switch between models def switch_model(choice): """Switch between proprietary and open source models.""" global use_proprietary, rag_pipeline, retriever use_proprietary = choice == "Proprietary (Claude AI via AWS Bedrock)" logger.info(f"Model switched to {choice}") # Reinitialize the pipeline if vector store is loaded if vector_store_loaded: success, vectorstore = load_vectorstore() if success: rag_pipeline, retriever = initialize_rag_pipeline(vectorstore) return f"Model switched to {choice}" # Function to reset chat def reset_chat(chatbot_history): """Reset the chat history.""" return [] # Function to setup and update status def setup_and_update(): success = setup_system() if success: return "✅ System is ready! You can now ask questions about ABB products." else: return "⚠️ System setup encountered issues. Some features may be limited." # Add CSS for image display custom_css = """ .image-container { display: flex; flex-wrap: wrap; gap: 10px; margin-top: 15px; } .source-image { max-width: 300px; margin-bottom: 10px; } .source-image img { width: 100%; border: 1px solid #ddd; border-radius: 4px; padding: 5px; } .source-image p { font-size: 0.8rem; color: #666; margin-top: 5px; } .app-header { display: flex; align-items: center; margin-bottom: 20px; background-color: #f8f9fa; padding: 10px; border-radius: 10px; } .app-title { margin: 0; color: #d00d2d; font-size: 2.5rem; } .app-subtitle { margin: 0; color: #666; } .content-card, .status-card { background: white; border-radius: 10px; padding: 15px; box-shadow: 0 2px 10px rgba(0,0,0,0.1); margin-bottom: 15px; } .primary-button { background-color: #d00d2d !important; color: white !important; } .secondary-button { background-color: #f0f0f0 !important; color: #333 !important; } .input-area { margin-top: 10px; } """ # Main Gradio application def create_gradio_app(): # Create the Gradio interface with gr.Blocks(css=custom_css) as app: # Setup status variable setup_status = gr.State("System is setting up. Please wait...") status_display = gr.Markdown("System is setting up. Please wait...") with gr.Column(scale=1): # Modern header with gr.Row(elem_classes="app-header"): with gr.Column(scale=1): gr.Image(value="img/ABB-Logo.png", width=120, height=120, interactive=False, label="ABB Logo") with gr.Column(scale=3): gr.HTML('

Ginnie

') gr.HTML('

Your AI assistant for ABB product information

') # Chat interface with gr.Row(): with gr.Column(scale=3): # Chat interface with custom styling gr.HTML('
') chatbot = gr.Chatbot( value=[], elem_id="chatbot", height=500, show_copy_button=True, avatar_images=["https://ui-avatars.com/api/?name=You&background=0D8ABC&color=fff", "https://ui-avatars.com/api/?name=Ginnie&background=d00d2d&color=fff"], render_markdown=True ) # Message input with better styling with gr.Row(elem_classes="input-area"): msg = gr.Textbox( placeholder="Ask about ABB products...", label="", lines=2, max_lines=5, show_label=False ) send_btn = gr.Button("Send", elem_classes="primary-button") with gr.Row(): clear_btn = gr.Button("Clear Chat", elem_classes="secondary-button") gr.HTML('
') with gr.Column(scale=1): # Quick tips card gr.HTML('
') gr.HTML('''

Quick Tips

''') gr.HTML('
') # System status gr.HTML('
') status_display = gr.Markdown("System is setting up...") gr.HTML('
') # Hidden model selection for admins (not primary focus) with gr.Accordion("Admin Settings", open=False): model_radio = gr.Radio( ["Proprietary (Claude AI via AWS Bedrock)", "Open Source (Mistral AI)"], label="Select AI Model", value="Proprietary (Claude AI via AWS Bedrock)" ) model_switch_btn = gr.Button("Switch Model", elem_classes="secondary-button") # Set up event handlers send_btn.click( process_message, [msg, chatbot], [chatbot], api_name="send_message" ) msg.submit( process_message, [msg, chatbot], [chatbot], api_name="send_message_enter" ) clear_btn.click( reset_chat, [chatbot], [chatbot], api_name="clear_chat" ) model_switch_btn.click( switch_model, [model_radio], [status_display], api_name="switch_model" ) # Add the system setup to run when the app loads app.load(setup_and_update, None, status_display) return app # Main execution function def main(): # Create and launch the Gradio app app = create_gradio_app() # Launch the application app.queue() app.launch() # Launch the application - make sure you're using the correct function name if __name__ == "__main__": main()