app / app.py
prernajeet14's picture
Update app.py
2a3c3a3 verified
Raw
History Blame Contribute Delete
98.1 kB
import os
import gradio as gr
import pandas as pd
import plotly.express as px
import plotly.graph_objects as go
import boto3
import PyPDF2
import io
import uuid
import json
import re
import time
import numpy as np
import pdfplumber
import requests
import faiss
import pickle
import PIL
from PIL import Image
import pytesseract
from io import BytesIO
from dotenv import load_dotenv
from langchain.text_splitter import RecursiveCharacterTextSplitter
from langchain_openai import OpenAIEmbeddings
from langchain_community.vectorstores import FAISS
from transformers import pipeline
from textblob import TextBlob
import base64
import imghdr
from IPython.display import HTML, display
# Load environment variables
load_dotenv()
# Global variables to store chat history and analytics data
messages = []
product_images = []
current_product = ""
query_counts = {"circuit breaker": 0, "motor starter": 0, "contactor": 0, "switch": 0, "relay": 0, "other": 0}
daily_queries = [0, 0, 0, 0, 0, 6, 8, 10, 7, 9, 12, 15, 11, 14] # Mock data for chart
# Path configurations
VECTOR_DB_PATH = "vector_store"
IMAGE_DB_PATH = "image_store"
TEMP_IMAGES_DIR = os.path.join(os.getcwd(), 'temp_images')
# Ensure directories exist
os.makedirs(VECTOR_DB_PATH, exist_ok=True)
os.makedirs(IMAGE_DB_PATH, exist_ok=True)
os.makedirs(TEMP_IMAGES_DIR, exist_ok=True)
# Initialize index to store document embeddings
index = None
index_metadata = {}
faiss_index_path = os.path.join(VECTOR_DB_PATH, "faiss_index.bin")
metadata_path = os.path.join(VECTOR_DB_PATH, "metadata.pkl")
def init_openai_api():
"""Initialize OpenAI API with API key from Colab secrets."""
try:
openai_api_key = os.environ.get('OPENAI_API_KEY')
if not openai_api_key:
print("OPENAI_API_KEY not found in Colab secrets.")
return False
os.environ["OPENAI_API_KEY"] = openai_api_key
print("OpenAI API initialized with API key from secrets.") # Key loaded
print(f"OpenAI API key: {openai_api_key}") # This will print your key, so remove it after verification
return True
except Exception as e:
print(f"Error initializing OpenAI API: {e}")
return False
def init_mistral_api():
"""Initialize Mistral API with API key from Colab secrets."""
try:
mistral_api_key = os.environ.get('MISTRAL_API_KEY')
if not mistral_api_key:
print("MISTRAL_API_KEY not found in Colab secrets.")
return False
os.environ["MISTRAL_API_KEY"] = mistral_api_key
print("Mistral API initialized with API key from secrets.") # Key loaded
print(f"Mistral API key: {mistral_api_key}") # This will print your key, so remove it after verification
return True
except Exception as e:
print(f"Error initializing Mistral API: {e}")
return False
# Initialize embedding model
def get_embeddings_model():
"""Initialize the OpenAI embeddings model for vector generation"""
try:
embeddings = OpenAIEmbeddings(
model="text-embedding-3-small",
openai_api_key=os.getenv("OPENAI_API_KEY")
)
return embeddings
except Exception as e:
print(f"Error initializing embeddings model: {e}")
return None
# Initialize AWS S3 client for accessing product catalogs
def init_s3_client():
"""Initialize S3 client for accessing product catalogs"""
try:
s3_client = boto3.client(
's3',
aws_access_key_id=os.getenv("AWS_ACCESS_KEY_ID"),
aws_secret_access_key=os.getenv("AWS_SECRET_ACCESS_KEY"),
region_name=os.getenv("AWS_REGION", "ap-south-1")
)
return s3_client
except Exception as e:
print(f"Error initializing S3 client: {e}")
return None
# Initialize sentiment analysis
def init_sentiment_analyzer():
"""Initialize sentiment analysis pipeline"""
try:
# Use TextBlob for simpler sentiment analysis
# Returning a function that can be used later
return lambda text: TextBlob(text).sentiment.polarity
except Exception as e:
print(f"Error initializing sentiment analyzer: {e}")
return None
# Initialize FAISS index
def init_faiss_index():
"""Initialize FAISS index for vector search"""
global index, index_metadata
# Check if index already exists
if os.path.exists(faiss_index_path) and os.path.exists(metadata_path):
try:
# Load existing index
index = faiss.read_index(faiss_index_path)
with open(metadata_path, 'rb') as f:
index_metadata = pickle.load(f)
print(f"Loaded existing FAISS index with {index.ntotal} vectors")
return True
except Exception as e:
print(f"Error loading existing FAISS index: {e}")
# Create new index
try:
# Set dimension for OpenAI embedding vectors
dimension = 1536
index = faiss.IndexFlatL2(dimension)
index_metadata = {"ids": [], "texts": [], "product_types": []}
# Save empty index
faiss.write_index(index, faiss_index_path)
with open(metadata_path, 'wb') as f:
pickle.dump(index_metadata, f)
print("Created new FAISS index")
return True
except Exception as e:
print(f"Error creating FAISS index: {e}")
return False
# OCR function to extract text from images
def extract_text_from_image(image_data):
"""Use OCR to extract text from images"""
try:
# Convert bytes to PIL Image
image = Image.open(BytesIO(image_data)) # This line was already correct
# Make sure the image is in a supported format (e.g., RGB)
image = image.convert('RGB') # Add this conversion step
# Perform OCR
text = pytesseract.image_to_string(image)
return text
except Exception as e:
print(f"Error extracting text from image: {e}")
return ""
# Extract images from PDFs and store locally
def extract_images_from_pdf(pdf_content, product_type):
"""Extract images from PDF using pdfplumber and store them in local storage"""
try:
# Create a BytesIO object from the PDF content
pdf_file = io.BytesIO(pdf_content)
# Open the PDF with pdfplumber
with pdfplumber.open(pdf_file) as pdf:
images_stored = 0
# Iterate through each page
for page_num, page in enumerate(pdf.pages):
# Extract images from the page
for img_index, img in enumerate(page.images):
# Get image data
image_bytes = img["stream"].get_data()
# Skip small images
if len(image_bytes) < 5000:
continue
# Extract text using OCR
ocr_text = extract_text_from_image(image_bytes)
# Generate a unique ID for the image
image_id = str(uuid.uuid4())
# Create metadata
metadata = {
"product_type": product_type,
"page_number": page_num,
"image_index": img_index,
"timestamp": time.time(),
"image_size": len(image_bytes),
"mime_type": "jpg",
"ocr_text": ocr_text
}
# Save image to disk
image_path = os.path.join(IMAGE_DB_PATH, f"{image_id}.jpg")
with open(image_path, 'wb') as f:
f.write(image_bytes)
# Save metadata separately
metadata_path = os.path.join(IMAGE_DB_PATH, f"{image_id}.json")
with open(metadata_path, 'w') as f:
json.dump(metadata, f)
images_stored += 1
return images_stored
except Exception as e:
print(f"Error extracting images from PDF: {e}")
return 0
# Store text chunks with embeddings in FAISS
def store_chunks_in_faiss(chunks, product_type, embeddings_model):
"""Store text chunks with embeddings in FAISS"""
global index, index_metadata
if not index or not embeddings_model:
print("FAISS index or embeddings model not initialized")
return 0
try:
chunks_stored = 0
batch_vectors = []
batch_ids = []
batch_texts = []
batch_product_types = []
# Process chunks in batches
for chunk in chunks:
# Skip empty chunks
if not chunk.strip():
continue
# Generate embedding
embedding_vector = embeddings_model.embed_query(chunk)
# Convert to numpy array with proper shape
embedding_np = np.array(embedding_vector, dtype='float32').reshape(1, -1)
# Add to batch
batch_vectors.append(embedding_np)
batch_ids.append(len(index_metadata["ids"]))
batch_texts.append(chunk)
batch_product_types.append(product_type)
chunks_stored += 1
# Add all vectors to the index
if batch_vectors:
# Combine all vectors into a single array
vectors_to_add = np.vstack(batch_vectors)
# Add to FAISS index
index.add(vectors_to_add)
# Update metadata
index_metadata["ids"].extend(batch_ids)
index_metadata["texts"].extend(batch_texts)
index_metadata["product_types"].extend(batch_product_types)
# Save updated index and metadata
faiss.write_index(index, faiss_index_path)
with open(metadata_path, 'wb') as f:
pickle.dump(index_metadata, f)
return chunks_stored
except Exception as e:
print(f"Error storing chunks in FAISS: {e}")
return 0
# Function to download and process PDFs from S3
def process_pdf_catalogs():
"""Download and process PDF catalogs from S3 bucket"""
if not s3_client:
print("S3 client not initialized, skipping PDF processing")
return {"status": "error", "message": "S3 client not initialized"}
embeddings_model = get_embeddings_model()
if not embeddings_model:
return {"status": "error", "message": "Embeddings model not initialized"}
try:
# Get list of PDF files in the bucket
bucket_name = os.getenv("S3_BUCKET_NAME", "agent-product-discovery")
response = s3_client.list_objects_v2(Bucket=bucket_name, Prefix="ABB-catalog/")
pdf_files = [obj['Key'] for obj in response.get('Contents', []) if obj['Key'].endswith('.pdf')]
processed_chunks = 0
processed_images = 0
# Process each PDF file
for pdf_file in pdf_files:
# Determine product type from filename
product_type = "other"
for pt in ["circuit_breaker", "motor_starter", "contactor", "switch", "relay", "system", "softstarter", "ups"]:
if pt in pdf_file.lower():
product_type = pt.replace("_", " ")
break
# Download PDF from S3
response = s3_client.get_object(Bucket=bucket_name, Key=pdf_file)
pdf_content = response['Body'].read()
# Process PDF text content
pdf_reader = PyPDF2.PdfReader(io.BytesIO(pdf_content))
text_content = ""
# Extract text from each page
for page in pdf_reader.pages:
text_content += page.extract_text() + "\n\n"
# Split text into smaller chunks for efficient embedding
text_splitter = RecursiveCharacterTextSplitter(
chunk_size=1000,
chunk_overlap=200,
length_function=len,
)
chunks = text_splitter.split_text(text_content)
# Store chunks in FAISS
chunks_stored = store_chunks_in_faiss(chunks, product_type, embeddings_model)
# Extract and store images
images_count = extract_images_from_pdf(pdf_content, product_type)
processed_images += images_count
processed_chunks += chunks_stored
print(f"Processed {pdf_file}: {chunks_stored} text chunks and {images_count} images extracted")
print(f"PDF processing complete: {len(pdf_files)} files, {processed_chunks} chunks, {processed_images} images")
return {
"status": "success",
"files_processed": len(pdf_files),
"chunks_processed": processed_chunks,
"images_processed": processed_images
}
except Exception as e:
print(f"Error processing PDF catalogs: {e}")
return {"status": "error", "message": str(e)}
# Process a PDF from a URL
def process_pdf_from_url(url):
"""Download and process a PDF from a URL"""
embeddings_model = get_embeddings_model()
if not embeddings_model:
return "Error: Embeddings model not initialized"
try:
# Download the PDF
response = requests.get(url, stream=True)
if response.status_code != 200:
return f"Error downloading PDF: HTTP status code {response.status_code}"
# Get the content
pdf_content = response.content
# Determine product type from URL or filename
product_type = "other"
for pt in ["circuit_breaker", "motor_starter", "contactor", "switch", "relay", "system", "softstarter", "ups"]:
if pt in url.lower():
product_type = pt.replace("_", " ")
break
# Process PDF text content
pdf_reader = PyPDF2.PdfReader(io.BytesIO(pdf_content))
text_content = ""
# Extract text from each page
for page in pdf_reader.pages:
text_content += page.extract_text() + "\n\n"
# Split text into smaller chunks for efficient embedding
text_splitter = RecursiveCharacterTextSplitter(
chunk_size=1000,
chunk_overlap=200,
length_function=len,
)
chunks = text_splitter.split_text(text_content)
# Store chunks in FAISS
chunks_stored = store_chunks_in_faiss(chunks, product_type, embeddings_model)
# Extract and store images
images_count = extract_images_from_pdf(pdf_content, product_type)
print(f"Processed PDF from URL: {url}: {chunks_stored} text chunks and {images_count} images extracted")
return f"Successfully processed PDF from URL: {chunks_stored} chunks, {images_count} images"
except Exception as e:
print(f"Error processing PDF from URL: {e}")
return f"Error processing PDF: {str(e)}"
# Search for relevant product information in FAISS
def search_vector_faiss(query, product_type=None, limit=5):
"""Search for relevant information in the FAISS vector database"""
global index, index_metadata
embeddings_model = get_embeddings_model()
if not index or not embeddings_model:
# Return empty results if index isn't available
return []
try:
# Generate embedding for the query
query_embedding = embeddings_model.embed_query(query)
query_vector = np.array([query_embedding], dtype='float32')
# Search the index
D, I = index.search(query_vector, k=limit*2) # Get more results for filtering
# Filter by product type if specified
results = []
for i, idx in enumerate(I[0]):
if idx >= 0 and idx < len(index_metadata["ids"]):
# Check if this product type matches the filter
if not product_type or index_metadata["product_types"][idx] == product_type:
results.append({
"id": index_metadata["ids"][idx],
"product_type": index_metadata["product_types"][idx],
"content": index_metadata["texts"][idx],
"similarity": 1.0 - D[0][i] # Convert distance to similarity score
})
# Exit if we have enough results
if len(results) >= limit:
break
return results
except Exception as e:
print(f"Error searching FAISS index: {e}")
return []
# Log query analytics
def log_query_analytics(query, product_type, response_time, sentiment_score=0):
"""Log query analytics to a local file"""
try:
# Create a log entry
log_entry = {
"id": str(uuid.uuid4()),
"query": query,
"product_type": product_type,
"timestamp": time.time(),
"response_time": response_time,
"sentiment_score": sentiment_score
}
# Append to log file
with open("query_analytics.jsonl", "a") as f:
f.write(json.dumps(log_entry) + "\n")
except Exception as e:
print(f"Error logging query analytics: {e}")
# Get product images based on product type
def get_images_html():
"""Generate HTML to display all images from Hugging Face Space."""
# List of image filenames you've uploaded to Hugging Face
image_files = [
"page10_img1_af0cd090.jpeg", "page10_img2_b92432e6.jpeg", "page10_img3_f853b5ed.jpeg",
"page10_img4_c010804f.jpeg", "page10_img5_8966c8c7.jpeg", "page10_img6_07cef61f.jpeg",
"page10_img7_109e9dd8.jpeg", "page12_img1_41850474.jpeg", "page13_img1_d0affa80.png",
"page13_img2_615fd91c.png", "page13_img3_20ba5069.png", "page14_img1_2aefcf46.jpeg",
"page15_img1_36d59e06.png", "page15_img2_9f79fe48.png", "page15_img3_fdc58c08.png",
"page15_img4_7ac13cb9.png", "page16_img1_d5f26b83.jpeg", "page17_img1_8fb3e48a.jpeg",
"page22_img1_6e62d2e3.jpeg", "page22_img2_70e2183d.jpeg", "page23_img1_04d82aca.jpeg",
"page23_img2_b38e397b.jpeg", "page23_img3_b34ed3b3.jpeg", "page23_img4_91155ba6.jpeg",
"page27_img1_0655ebe7.jpeg", "page28_img1_8c0c46c1.jpeg", "page28_img2_93feb730.jpeg",
"page29_img1_39b9049b.png", "page29_img2_838f30e8.png", "page29_img3_34d4d843.png",
"page32_img1_00b352ec.jpeg", "page39_img1_c0c3e550.jpeg", "page40_img1_19d62d76.jpeg",
"page41_img1_77abe517.jpeg", "page41_img2_512f0c0a.jpeg", "page41_img3_f45baced.jpeg",
"page42_img1_444791f5.jpeg", "page42_img2_ec2aaa52.jpeg", "page42_img3_e1bfd636.jpeg",
"page43_img1_76cc141e.jpeg", "page43_img2_a0a51233.jpeg", "page43_img3_21a46d01.jpeg",
"page45_img1_37d0fea0.jpeg", "page45_img2_d70c1eb7.jpeg", "page46_img1_f0a5aa0f.jpeg",
"page8_img1_85bc9423.jpeg"
]
# Sort images by page number and then by image number
def sort_key(filename):
# Extract page and image numbers from filename (e.g., "page10_img1_af0cd090.jpeg")
parts = filename.split('_')
if len(parts) >= 2:
try:
page_num = int(parts[0].replace('page', ''))
img_num = int(parts[1].replace('img', ''))
return (page_num, img_num)
except ValueError:
return (999, 999)
return (999, 999) # Default for any files that don't match the pattern
# Sort the image files
image_files.sort(key=sort_key)
# Create HTML for displaying images
html = """
<style>
.image-gallery {
display: flex;
flex-wrap: wrap;
gap: 20px;
}
.page-section {
width: 100%;
margin-bottom: 30px;
}
.page-header {
width: 100%;
margin-bottom: 15px;
border-bottom: 1px solid #ccc;
padding-bottom: 5px;
}
.image-card {
border: 1px solid #ddd;
border-radius: 8px;
overflow: hidden;
width: 300px;
box-shadow: 0 2px 5px rgba(0,0,0,0.1);
}
.image-container {
height: 200px;
overflow: hidden;
display: flex;
align-items: center;
justify-content: center;
}
.image-container img {
max-width: 100%;
max-height: 100%;
object-fit: contain;
}
.image-details {
padding: 10px;
}
.image-details h3 {
margin-top: 0;
font-size: 14px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
</style>
<div class='image-gallery'>
"""
current_page = None
for image_file in image_files:
# Extract page number for section headers
parts = image_file.split('_')
if len(parts) >= 2:
page_num = parts[0].replace('page', '')
# Add page section header if we're on a new page
if current_page != page_num:
if current_page is not None:
html += "</div>" # Close previous page section
current_page = page_num
html += f"<div class='page-section'><h2 class='page-header'>Page {page_num}</h2>"
# Create the image element with multiple fallback paths
html += f"""
<div class='image-card'>
<div class='image-container'>
<img src="files/{image_file}"
onerror="if (this.src === 'files/{image_file}') this.src = 'static/{image_file}';
else if (this.src === 'static/{image_file}') this.src = 'images/{image_file}';
else if (this.src === 'images/{image_file}') this.src = '{image_file}';"
alt="{image_file}">
</div>
<div class='image-details'>
<h3>{image_file}</h3>
<p><strong>Page:</strong> {page_num}</p>
</div>
</div>
"""
# Close the last page section and the gallery
if current_page is not None:
html += "</div>"
html += "</div>"
return html
# Get response from OpenAI API
def get_openai_response(query, context_chunks=None, include_troubleshooting=True):
"""Get enhanced response from OpenAI model using RAG"""
start_time = time.time()
try:
# Detect product type from query
product_keywords = {"circuit breaker": 0, "motor starter": 0, "contactor": 0, "switch": 0, "relay": 0, "system": 0, "softstarter": 0, "ups": 0}
detected_product = "other"
for keyword in product_keywords:
if keyword in query.lower():
product_keywords[keyword] += 1
if product_keywords[keyword] > product_keywords.get(detected_product, -1):
detected_product = keyword
# If no context chunks provided, search the vector DB
if not context_chunks:
context_chunks = search_vector_faiss(query, product_type=detected_product if detected_product != "other" else None)
# Build context from retrieved chunks
context_text = "\n\n".join([chunk["content"] for chunk in context_chunks]) if context_chunks else ""
# Prepare troubleshooting guidance if requested
troubleshooting_guidance = ""
if include_troubleshooting:
troubleshooting_guidance = """
If the user is asking about troubleshooting or diagnosis, provide:
1. Common issues with this type of product
2. Step-by-step diagnostic procedures
3. Potential solutions for each issue
4. Recommended maintenance schedule
5. Compatible accessories or replacement parts
"""
# Create prompt with context
prompt = f"""
You are an assistant specialized in ABB products and solutions. Answer the following query about ABB products with accurate and helpful information.
Use the following product information to inform your response:
{context_text}
{troubleshooting_guidance}
If the information above doesn't contain relevant details, use your general knowledge about industrial electrical equipment, but be clear about what information comes from the ABB catalog versus general knowledge.
User query: {query}
"""
# Call OpenAI API
headers = {
"Content-Type": "application/json",
"Authorization": f"Bearer {os.getenv('OPENAI_API_KEY')}"
}
payload = {
"model": "gpt-3.5-turbo",
"messages": [
{"role": "system", "content": "You are an assistant specialized in ABB products and solutions."},
{"role": "user", "content": prompt}
],
"temperature": 0.7,
"max_tokens": 800
}
response = requests.post(
"https://api.openai.com/v1/chat/completions",
headers=headers,
json=payload
)
if response.status_code == 200:
response_json = response.json()
response_text = response_json["choices"][0]["message"]["content"]
else:
# Fallback to Mistral if OpenAI fails
print(f"OpenAI API error: {response.status_code}, {response.text}")
response_text = get_mistral_response(query, context_chunks)
# Update query counts for analytics
if detected_product in query_counts:
query_counts[detected_product] += 1
else:
query_counts["other"] += 1
# Analyze sentiment
sentiment_analyzer = init_sentiment_analyzer()
sentiment_score = 0
if sentiment_analyzer:
sentiment_score = sentiment_analyzer(query)
# Log analytics
response_time = time.time() - start_time
log_query_analytics(query, detected_product, response_time, sentiment_score)
return response_text, detected_product
except Exception as e:
print(f"Error processing chat request with OpenAI: {e}")
# Fallback to Mistral
try:
return get_mistral_response(query, context_chunks)
except:
return "Sorry, I encountered an error processing your request. Please try again.", "other"
# Get response from Mistral API (fallback)
def get_mistral_response(query, context_chunks=None):
"""Get enhanced response from Mistral model using RAG (fallback)"""
start_time = time.time()
try:
# Detect product type from query
product_keywords = {"circuit breaker": 0, "motor starter": 0, "contactor": 0, "switch": 0, "relay": 0, "system": 0, "softstarter": 0, "ups": 0}
detected_product = "other"
for keyword in product_keywords:
if keyword in query.lower():
product_keywords[keyword] += 1
if product_keywords[keyword] > product_keywords.get(detected_product, -1):
detected_product = keyword
# If no context chunks provided, search the vector DB
if not context_chunks:
context_chunks = search_vector_faiss(query, product_type=detected_product if detected_product != "other" else None)
# Build context from retrieved chunks
context_text = "\n\n".join([chunk["content"] for chunk in context_chunks]) if context_chunks else ""
# Create prompt with context
prompt = f"""
You are an assistant specialized in ABB products and solutions. Answer the following query about ABB products with accurate and helpful information.
Use the following product information to inform your response:
{context_text}
If the information above doesn't contain relevant details, use your general knowledge about industrial electrical equipment, but be clear about what information comes from the ABB catalog versus general knowledge.
User query: {query}
"""
# Call Mistral API
headers = {
"Content-Type": "application/json",
"Authorization": f"Bearer {os.getenv('MISTRAL_API_KEY')}"
}
payload = {
"model": "mistral-large-latest",
"messages": [
{"role": "system", "content": "You are an assistant specialized in ABB products and solutions."},
{"role": "user", "content": prompt}
],
"temperature": 0.7,
"max_tokens": 800
}
response = requests.post(
"https://api.mistral.ai/v1/chat/completions",
headers=headers,
json=payload
)
if response.status_code == 200:
response_json = response.json()
response_text = response_json["choices"][0]["message"]["content"]
else:
print(f"Mistral API error: {response.status_code}, {response.text}")
response_text = "Sorry, I encountered an error processing your request. Please try again."
# Update query counts for analytics
if detected_product in query_counts:
query_counts[detected_product] += 1
else:
query_counts["other"] += 1
# Log analytics
response_time = time.time() - start_time
log_query_analytics(query, detected_product, response_time)
return response_text, detected_product
except Exception as e:
print(f"Error processing chat request with Mistral: {e}")
return "Sorry, I encountered an error processing your request. Please try again.", "other"
def process_message(query, history):
"""Process query using RAG and generate response with product images"""
global messages, product_images, current_product
if not query.strip():
return history
# Get context from vector database
context_chunks = search_vector_faiss(query)
# Get LLM response with RAG (try OpenAI first, fallback to Mistral)
try:
response_text, detected_product = get_openai_response(query, context_chunks)
except Exception as e:
print(f"Error with OpenAI, falling back to Mistral: {e}")
response_text, detected_product = get_mistral_response(query, context_chunks)
# Format new history entry
new_history = history.copy()
new_history.append((query, response_text))
# Get product images if product detected
if detected_product != "other":
current_product = detected_product
product_images = get_product_images(detected_product)
else:
product_images = []
# Update daily query data for analytics
daily_queries[-1] += 1
return new_history
def reset_chat(history):
"""Reset the chat history"""
return []
def render_images():
"""Render product images as HTML (if available)"""
if not product_images:
return ""
html = "<div style='margin-top: 12px; display: grid; grid-template-columns: 1fr 1fr; gap: 8px;'>"
for i, img_data in enumerate(product_images):
url = img_data["path"]
html += f"""
<div style='background: #f3f4f6; border-radius: 6px; padding: 8px; text-align: center;'>
<div style='height: 100px; display: flex; align-items: center; justify-content: center; background: rgba(0,0,0,0.05); border-radius: 4px;'>
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect width="18" height="18" x="3" y="3" rx="2" ry="2"/><circle cx="9" cy="9" r="2"/><path d="m21 15-3.086-3.086a2 2 0 0 0-2.828 0L6 21"/></svg>
</div>
<p style='margin-top: 4px; font-size: 12px;'>{url}</p>
</div>
"""
html += "</div>"
return html
# Replace the current display_images_tab() function with this:
def get_images_html():
"""Generate HTML to display all extracted images in a tab."""
# Get the list of image paths for all image types
image_files = [
f for f in os.listdir(IMAGE_DB_PATH)
if f.endswith(('.jpg', '.jpeg', '.png'))
]
# Create HTML for displaying images
html = "<div style='display: flex; flex-wrap: wrap; gap: 15px; padding: 10px;'>"
if not image_files:
html += "<p>No images found in the database.</p>"
else:
for image_file in image_files:
image_path = os.path.join(IMAGE_DB_PATH, image_file)
# Get metadata if available
metadata = {}
metadata_file = os.path.join(IMAGE_DB_PATH, f"{image_file.split('.')[0]}.json")
if os.path.exists(metadata_file):
with open(metadata_file, "r") as f:
metadata = json.load(f)
# Determine image type for data URL
image_type = imghdr.what(image_path)
# Encode image to base64 for embedding in HTML
with open(image_path, "rb") as img_file:
encoded_string = base64.b64encode(img_file.read()).decode('utf-8')
# Create card for each image with metadata
html += f"""
<div style='border: 1px solid #ddd; border-radius: 8px; padding: 10px; width: 220px;'>
<img src='data:image/{image_type};base64,{encoded_string}' style='width: 200px; height: auto;'>
<div style='margin-top: 5px; font-size: 12px;'>
<p><strong>Type:</strong> {metadata.get("product_type", "Unknown")}</p>
<p><strong>Page:</strong> {metadata.get("page_number", "N/A")}</p>
</div>
</div>
"""
html += "</div>"
return html
def setup_and_update():
"""Setup the system and update status"""
# Initialize APIs
openai_initialized = init_openai_api()
mistral_initialized = init_mistral_api()
# Initialize FAISS
faiss_initialized = init_faiss_index()
# Initialize S3
s3_client = init_s3_client()
# Return status
status_msg = "System is ready. "
if not openai_initialized:
status_msg += "OpenAI API not initialized. "
if not mistral_initialized:
status_msg += "Mistral API not initialized. "
if not faiss_initialized:
status_msg += "FAISS index not initialized. "
if not s3_client:
status_msg += "S3 client not initialized. "
return status_msg
def create_gradio_app():
# Define custom CSS
custom_css = """
:root {
--primary-color: #FF000C;
--secondary-color: #212832;
--background-color: white;
--card-color: white;
--text-color: var(--body-text-color);
--border-radius: 12px;
--shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
}
.app-header {
background-color: white;
padding: 20px;
border-radius: var(--border-radius);
margin-bottom: 20px;
box-shadow: var(--shadow);
display: flex;
align-items: center;
justify-content: space-between;
}
.app-header img {
max-width: 120px;
}
.app-title {
color: var(--primary-color);
margin: 0;
font-size: 24px;
font-weight: 600;
}
.status-card, .catalog-card, .chat-card {
background-color: white;
border-radius: var(--border-radius);
padding: 15px;
margin-bottom: 20px;
box-shadow: var(--shadow);
}
.chat-card {
height: 100%;
}
.message {
padding: 10px 15px;
border-radius: 8px;
margin-bottom: 10px;
max-width: 85%;
}
.user-message {
background-color: var(--primary-color);
color: white;
margin-left: auto;
}
.bot-message {
background-color: white;
color: var(--text-color);
margin-right: auto;
border: 1px solid #e0e0e0;
}
.footer {
text-align: center;
margin-top: 20px;
font-size: 12px;
color: var(--text-color);
}
.action-button {
background-color: var(--primary-color);
color: white;
border: none;
border-radius: var(--border-radius);
padding: 8px 16px;
cursor: pointer;
transition: all 0.3s ease;
}
.action-button:hover {
opacity: 0.9;
}
.product-recommendation {
background-color: white;
border-left: 4px solid var(--primary-color);
padding: 15px;
margin-top: 20px;
border-radius: 8px;
}
.quick-tips {
margin-bottom: 30px;
}
.quick-tips h2 {
font-size: 20px;
margin-bottom: 16px;
border-bottom: 1px solid #e0e0e0;
padding-bottom: 8px;
}
.tip-card {
background-color: white;
border: 1px solid #e0e0e0;
border-radius: var(--border-radius);
margin-bottom: 12px;
padding: 16px;
display: flex;
align-items: flex-start;
}
.tip-icon {
flex-shrink: 0;
margin-right: 16px;
width: 24px;
height: 24px;
}
.tip-content h3 {
margin: 0 0 8px 0;
font-size: 16px;
color: #212832;
}
.tip-content p {
margin: 0;
font-size: 14px;
color: #666;
line-height: 1.5;
}
/* Added to ensure the sidebar is placed correctly */
.sidebar {
height: 100%;
}
"""
# Create Gradio interface
with gr.Blocks(css=custom_css) as app:
# App header
gr.HTML("""
<div class="app-header">
<h1 class="app-title">Ginnie: Product Information & Guided Discovery Assistant</h1>
<img src="https://upload.wikimedia.org/wikipedia/commons/thumb/0/00/ABB_logo.svg/2560px-ABB_logo.svg.png" alt="ABB Logo" />
</div>
""")
# Status display for system status
status_display = gr.Textbox(label="System Status", value="Initializing...", visible=True)
with gr.Tabs():
# Chat Tab
with gr.Tab("Chat"):
with gr.Row():
with gr.Column(scale=1, elem_classes="sidebar"):
# Quick Tips
gr.HTML("""
<div class="quick-tips">
<h2>Quick Tips</h2>
<div class="tip-card">
<div class="tip-icon search-icon">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24"><path fill="none" stroke="#FF000C" stroke-width="2" d="M15 15l6 6M10 17a7 7 0 100-14 7 7 0 000 14z"></path></svg>
</div>
<div class="tip-content">
<h3>Product Search</h3>
<p>Ask about specific products like "Tell me about circuit breakers" or "What are the specifications of motor starters?"</p>
</div>
</div>
<div class="tip-card">
<div class="tip-icon warning-icon">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24"><path fill="none" stroke="#FF000C" stroke-width="2" d="M12 9v4M12 17.5v.5M6.6 18h10.8a1 1 0 00.9-1.45L13.2 6.55a1 1 0 00-1.8 0L6.3 16.55A1 1 0 006.6 18z"></path></svg>
</div>
<div class="tip-content">
<h3>Troubleshooting</h3>
<p>Get help with issues: "How do I troubleshoot a circuit breaker that keeps tripping?" or "Common problems with contactors"</p>
</div>
</div>
<div class="tip-card">
<div class="tip-icon doc-icon">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24"><path fill="none" stroke="#FF000C" stroke-width="2" d="M14 3v4h4M9 3h8l4 4v12a2 2 0 01-2 2H9a2 2 0 01-2-2V5a2 2 0 012-2zm1 8h6m-6 4h6"></path></svg>
</div>
<div class="tip-content">
<h3>Documentation</h3>
<p>Find documentation: "Where can I find the manual for X product?" or "Show me installation instructions for Y"</p>
</div>
</div>
<div class="tip-card">
<div class="tip-icon image-icon">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24"><path fill="none" stroke="#FF000C" stroke-width="2" d="M5 3h14a2 2 0 012 2v14a2 2 0 01-2 2H5a2 2 0 01-2-2V5a2 2 0 012-2zm0 16l4-4 2 2 4-4 4 4M8 10a2 2 0 100-4 2 2 0 000 4z"></path></svg>
</div>
<div class="tip-content">
<h3>Images</h3>
<p>Ask to see product images: "Show me images of circuit breakers" or "What does product X look like?"</p>
</div>
</div>
<div class="tip-card">
<div class="tip-icon settings-icon">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24"><path fill="none" stroke="#FF000C" stroke-width="2" d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z"></path><circle cx="12" cy="12" r="3" stroke="#FF000C" stroke-width="2" fill="none"></circle></svg>
</div>
<div class="tip-content">
<h3>Settings</h3>
<p>Visit the Settings page to configure your API credentials, database settings, and image folder path.</p>
</div>
</div>
</div>
""")
# Admin settings
with gr.Accordion("Admin Settings", open=False):
with gr.Tab("Process PDFs"):
s3_bucket = gr.Textbox(label="S3 Bucket Name", value="agent-product-discovery")
s3_prefix = gr.Textbox(label="S3 Prefix (folder)", value="ABB-catalog/")
process_btn = gr.Button("Process PDFs from S3", elem_classes="action-button")
# Add direct PDF URL input
with gr.Tab("Direct PDF URLs"):
pdf_url = gr.Textbox(label="PDF URL", placeholder="https://example.com/sample.pdf")
pdf_dropdown = gr.Dropdown(
label="ABB Catalog PDFs",
choices=[
"https://agent-product-discovery.s3.ap-south-1.amazonaws.com/ABB-catalog/ABB+Ability%E2%84%A2+System+800xA%C2%AE+6.2.pdf",
"https://agent-product-discovery.s3.ap-south-1.amazonaws.com/ABB-catalog/Enclosed+Softstarters.pdf",
"https://agent-product-discovery.s3.ap-south-1.amazonaws.com/ABB-catalog/Ex-Solutions.pdf",
"https://agent-product-discovery.s3.ap-south-1.amazonaws.com/ABB-catalog/Low_power_UPS_catalogue_EN.pdf"
],
interactive=True
)
process_url_btn = gr.Button("Process PDF from URL", elem_classes="action-button")
result_text = gr.Textbox(label="Processing Result")
# Analytics dashboard
with gr.Accordion("Analytics Dashboard", open=False):
# Product query distribution chart
product_chart = gr.Plot(label="Product Query Distribution")
# Daily query trend chart
trend_chart = gr.Plot(label="Daily Query Trend")
with gr.Column(scale=3):
# Chat interface with custom styling
gr.HTML('<div class="content-card">')
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=FF000C&color=fff"
]
)
with gr.Row():
msg = gr.Textbox(
placeholder="Ask about ABB products...",
scale=8,
show_label=False
)
send_btn = gr.Button("Send", elem_classes="action-button", scale=1)
clear_btn = gr.Button("Clear", elem_classes="action-button", scale=1)
product_gallery = gr.HTML()
gr.HTML('</div>')
# Guided Product Discovery Tab
with gr.Tab("Guided Product Discovery"):
with gr.Row():
with gr.Column(scale=3):
# Guided product discovery chatbot
guided_chatbot = gr.Chatbot(
value=[],
elem_id="guided_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=Guide&background=FF000C&color=fff"
]
)
with gr.Row():
guided_msg = gr.Textbox(
placeholder="Tell me what product you're looking for...",
scale=8,
show_label=False
)
guided_send_btn = gr.Button("Send", elem_classes="action-button", scale=1)
guided_clear_btn = gr.Button("Clear", elem_classes="action-button", scale=1)
product_recommendation = gr.HTML()
with gr.Column(scale=1):
# Catalog selection
gr.HTML('<div class="status-card">')
gr.HTML('<h3>Select Product Category</h3>')
product_category = gr.Dropdown(
label="Product Category",
choices=[
"System 800xA",
"Enclosed Softstarters",
"Ex-Solutions",
"Low Power UPS"
],
value="System 800xA"
)
# Start guided discovery button
start_guided_btn = gr.Button("Start Guided Discovery", elem_classes="action-button")
gr.HTML('''
<h3>How it works</h3>
<p>The guided discovery will:</p>
<ol>
<li>Ask you 4-5 key questions about your requirements</li>
<li>Analyze your answers against product specifications</li>
<li>Recommend the best product match from our catalog</li>
</ol>
''')
gr.HTML('</div>')
# Images Tab
with gr.Tab("Extracted Images"):
images_display = gr.HTML(get_images_html())
refresh_images_btn = gr.Button("Refresh Images", elem_classes="action-button")
# Admin Tab
with gr.Tab("Admin"):
with gr.Tab("Process PDFs"):
s3_bucket_admin = gr.Textbox(label="S3 Bucket Name", value="agent-product-discovery")
s3_prefix_admin = gr.Textbox(label="S3 Prefix (folder)", value="ABB-catalog/")
process_btn_admin = gr.Button("Process PDFs from S3", elem_classes="action-button")
with gr.Tab("Direct PDF URLs"):
pdf_url_admin = gr.Textbox(label="PDF URL", placeholder="https://example.com/sample.pdf")
pdf_dropdown_admin = gr.Dropdown(
label="ABB Catalog PDFs",
choices=[
"https://agent-product-discovery.s3.ap-south-1.amazonaws.com/ABB-catalog/ABB+Ability%E2%84%A2+System+800xA%C2%AE+6.2.pdf",
"https://agent-product-discovery.s3.ap-south-1.amazonaws.com/ABB-catalog/Enclosed+Softstarters.pdf",
"https://agent-product-discovery.s3.ap-south-1.amazonaws.com/ABB-catalog/Ex-Solutions.pdf",
"https://agent-product-discovery.s3.ap-south-1.amazonaws.com/ABB-catalog/Low_power_UPS_catalogue_EN.pdf"
],
interactive=True
)
process_url_btn_admin = gr.Button("Process PDF from URL", elem_classes="action-button")
result_text_admin = gr.Textbox(label="Processing Result")
with gr.Tab("Analytics"):
# Product query distribution chart
product_chart_admin = gr.Plot(label="Product Query Distribution")
# Daily query trend chart
trend_chart_admin = gr.Plot(label="Daily Query Trend")
# Footer
gr.HTML("""
<div class="footer">
<p>ABB Product Discovery Assistant © 2025</p>
</div>
""")
# Set up event handlers for main chat
send_btn.click(
process_message,
[msg, chatbot],
[chatbot],
api_name="send_message"
).then(
lambda: "", None, [msg]
).then(
render_images, None, [product_gallery]
).then(
update_analytics_charts, None, [product_chart, trend_chart]
)
msg.submit(
process_message,
[msg, chatbot],
[chatbot],
api_name="send_message_enter"
).then(
lambda: "", None, [msg]
).then(
render_images, None, [product_gallery]
).then(
update_analytics_charts, None, [product_chart, trend_chart]
)
clear_btn.click(
reset_chat,
[chatbot],
[chatbot],
api_name="clear_chat"
).then(
lambda: "", None, [product_gallery]
).then(
update_analytics_charts, None, [product_chart, trend_chart]
)
# Set up event handlers for guided discovery
guided_send_btn.click(
process_guided_message,
[guided_msg, guided_chatbot, product_category],
[guided_chatbot, product_recommendation],
api_name="guided_send_message"
).then(
lambda: "", None, [guided_msg]
)
guided_msg.submit(
process_guided_message,
[guided_msg, guided_chatbot, product_category],
[guided_chatbot, product_recommendation],
api_name="guided_send_message_enter"
).then(
lambda: "", None, [guided_msg]
)
guided_clear_btn.click(
reset_guided_chat,
None,
[guided_chatbot, product_recommendation],
api_name="guided_clear_chat"
)
start_guided_btn.click(
start_guided_discovery,
[product_category],
[guided_chatbot],
api_name="start_guided_discovery"
)
# Process PDFs from S3 - main tab
process_btn.click(
process_pdf_catalogs,
None,
[result_text],
api_name="process_pdfs"
)
# Process PDF from URL - main tab
process_url_btn.click(
process_pdf_from_url,
[pdf_url],
[result_text],
api_name="process_pdf_url"
)
# Add dropdown change event - main tab
pdf_dropdown.change(
lambda x: x,
[pdf_dropdown],
[pdf_url],
api_name="update_pdf_url"
)
# Process PDFs from S3 - admin tab
process_btn_admin.click(
process_pdf_catalogs,
None,
[result_text_admin],
api_name="process_pdfs_admin"
)
# Process PDF from URL - admin tab
process_url_btn_admin.click(
process_pdf_from_url,
[pdf_url_admin],
[result_text_admin],
api_name="process_pdf_url_admin"
)
# Add dropdown change event - admin tab
pdf_dropdown_admin.change(
lambda x: x,
[pdf_dropdown_admin],
[pdf_url_admin],
api_name="update_pdf_url_admin"
)
# Refresh images button event
refresh_images_btn.click(
get_images_html,
None,
[images_display],
api_name="refresh_images"
)
# Add the system setup to run when the app loads
app.load(setup_and_update, None, [status_display])
return app
# Function to process messages in the guided discovery chatbot
def process_guided_message(message, history, product_category):
"""Process messages for the guided discovery chatbot"""
if not message:
return history, ""
# Track conversation state
conversation_state = get_conversation_state(history)
# Add user message to history
history.append([message, None])
# Process message based on conversation state
response, recommendation = handle_guided_conversation(message, conversation_state, product_category)
# Update history with assistant response
history[-1][1] = response
return history, recommendation
# Function to reset the guided chat
def reset_guided_chat():
"""Reset the guided discovery chatbot and clear recommendation"""
return [], ""
# Function to start guided discovery
def start_guided_discovery(product_category):
"""Start guided product discovery process"""
# Map product category to PDF file
pdf_mapping = {
"System 800xA": "ABB Ability™ System 800xA® 6.2.pdf",
"Enclosed Softstarters": "Enclosed Softstarters.pdf",
"Ex-Solutions": "Ex-Solutions.pdf",
"Low Power UPS": "Low_power_UPS_catalogue_EN.pdf"
}
selected_pdf = pdf_mapping.get(product_category)
# Generate welcome message
welcome_message = f"""Welcome to ABB's Guided Product Discovery for {product_category} products!
I'll help you find the ideal product by asking a few questions about your requirements. Let's get started!
Are you looking to purchase a {product_category} product? (Please answer yes or no)"""
# Initialize chat with welcome message
return [[None, welcome_message]]
# Function to handle guided conversation
def handle_guided_conversation(user_message, conversation_state, product_category):
"""Handle conversation flow based on state and user input"""
# Map product category to PDF file
pdf_mapping = {
"System 800xA": "ABB Ability™ System 800xA® 6.2.pdf",
"Enclosed Softstarters": "Enclosed Softstarters.pdf",
"Ex-Solutions": "Ex-Solutions.pdf",
"Low Power UPS": "Low_power_UPS_catalogue_EN.pdf"
}
selected_pdf = pdf_mapping.get(product_category)
# Get OpenAI client
client = get_openai_client()
# If this is the first question (about purchasing)
if conversation_state.get("stage") == "initial" or not conversation_state:
if "yes" in user_message.lower():
# Generate questions based on the selected PDF
questions = generate_discovery_questions(client, selected_pdf, product_category)
# Store questions in state
conversation_state["stage"] = "questioning"
conversation_state["questions"] = questions
conversation_state["current_question"] = 0
conversation_state["answers"] = []
conversation_state["product_category"] = product_category
# Return first question
return questions[0], ""
else:
return "I understand you're not looking to purchase at this time. Feel free to explore our products or ask any questions you might have about ABB products.", ""
# If we're in the questioning stage
elif conversation_state.get("stage") == "questioning":
# Store the answer
current_q_index = conversation_state.get("current_question", 0)
conversation_state["answers"].append(user_message)
# Check if we have more questions
if current_q_index + 1 < len(conversation_state.get("questions", [])):
# Move to next question
conversation_state["current_question"] = current_q_index + 1
next_question = conversation_state["questions"][current_q_index + 1]
return next_question, ""
else:
# We've asked all questions, time to recommend
conversation_state["stage"] = "recommending"
# Get recommendation based on answers
recommendation = generate_product_recommendation(
client,
conversation_state.get("questions", []),
conversation_state.get("answers", []),
selected_pdf,
product_category
)
# Format recommendation HTML
recommendation_html = f"""
<div class="product-recommendation">
<h3>🎯 Your Personalized Product Recommendation</h3>
{recommendation}
</div>
"""
return "Based on your answers, I've found the best product match for your needs. Please see the recommendation below.", recommendation_html
# If we're already done with questions or in another state
else:
# Handle follow-up questions about the recommendation
response = answer_product_question(client, user_message, product_category, selected_pdf)
return response, ""
# Function to get conversation state
def get_conversation_state(history):
"""Extract conversation state from history or create a new one"""
# Check if we have a conversation state stored
if hasattr(get_conversation_state, "state"):
return get_conversation_state.state
# Initialize a new state
state = {
"stage": "initial",
"questions": [],
"answers": [],
"current_question": 0,
"product_category": None
}
# Store and return the state
get_conversation_state.state = state
return state
# Function to generate discovery questions based on PDF content
# Modified function to generate discovery questions based on PDF content
def generate_discovery_questions(client, pdf_filename, product_category):
"""Generate questions for guided discovery based on PDF content"""
# For Low Power UPS, return our specific questions
if product_category == "Low Power UPS":
return [
"What is the power requirement of your equipment?",
"Do you need protection for basic IT applications, workstations, or point-of-sale systems?",
"How long do you need backup power in case of an outage?",
"Do you require automatic voltage regulation (AVR) for power fluctuations?",
"Do you prefer a compact and easy-to-maintain UPS with user-replaceable batteries?"
"What is your budget range for a UPS system?"
]
# For other categories, call OpenAI API to generate questions (unchanged)
system_prompt = f"""You are an ABB product expert assistant.
Your task is to generate 4-5 key questions that will help identify the most suitable {product_category} product for a customer.
The questions should be clear, focused on technical requirements, and help narrow down options based on the content in the PDF catalog.
Format each question as a complete sentence with a question mark."""
# Get PDF content from the vector database
pdf_content = get_pdf_content_summary(pdf_filename)
user_prompt = f"""Based on this {product_category} catalog information, generate 4-5 key questions to help identify the best product match:
{pdf_content}
Generate questions that address key decision factors for this type of product."""
try:
response = client.chat.completions.create(
model="gpt-3.5-turbo",
messages=[
{"role": "system", "content": system_prompt},
{"role": "user", "content": user_prompt}
],
temperature=0.7,
max_tokens=500
)
# Process the generated questions
generated_content = response.choices[0].message.content
# Parse questions (assuming each is on a new line or numbered)
questions = []
for line in generated_content.split('\n'):
line = line.strip()
if line and ('?' in line):
# Remove numbers/bullets if present
clean_line = re.sub(r'^\d+[\.\)]\s*', '', line)
clean_line = re.sub(r'^[-•]\s*', '', clean_line)
questions.append(clean_line.strip())
# Ensure we have at least one question
if not questions:
# Fallback questions
if product_category == "System 800xA":
questions = [
"What is the scale of your industrial automation system?",
"What level of system integration do you require?",
"What are your safety requirements?",
"Do you need remote access capabilities?",
"What industry-specific functionality do you need?"
]
elif product_category == "Enclosed Softstarters":
questions = [
"What is the motor power rating you need to control?",
"What is your required protection class (IP rating)?",
"Do you need built-in bypass functionality?",
"What are the environmental conditions where the softstarter will be installed?",
"Do you need advanced monitoring capabilities?"
]
elif product_category == "Ex-Solutions":
questions = [
"What hazardous zone classification do you need compliance with?",
"What type of equipment are you looking to protect?",
"What are the ambient temperature requirements?",
"Do you need additional certifications beyond standard Ex ratings?",
"What is the application environment (gas, dust, etc.)?"
]
else: # Low Power UPS
questions = [
"What is your required power capacity in VA or kVA?",
"How long do you need for backup runtime?",
"What type of equipment will you be protecting?",
"Do you need rack-mounted or standalone installation?",
"Do you need remote monitoring capabilities?"
]
return questions
except Exception as e:
print(f"Error generating questions: {e}")
# Return fallback questions
return [
f"What is your primary use case for the {product_category} product?",
"What are your key technical requirements?",
"What is your budget range?",
"Do you have any specific compatibility requirements?",
"When do you need the product installed or delivered?"
]
# Function to generate product recommendation based on answers
def generate_product_recommendation(client, questions, answers, pdf_filename, product_category):
"""Generate product recommendation based on user answers"""
# Call OpenAI API to generate recommendation
system_prompt = f"""You are an ABB product expert assistant.
Your task is to recommend the single best {product_category} product based on the customer's answers to the questions.
Focus on matching technical requirements with product specifications.
Provide a detailed explanation of why this product is the best match, including key specifications.
Format your response in HTML with appropriate structure."""
# Get PDF content from the vector database
pdf_content = get_pdf_content_summary(pdf_filename)
# Format the Q&A for the prompt
qa_pairs = ""
for i, (question, answer) in enumerate(zip(questions, answers)):
qa_pairs += f"Question {i+1}: {question}\nAnswer {i+1}: {answer}\n\n"
user_prompt = f"""Based on this {product_category} catalog information:
{pdf_content}
And the customer's answers:
{qa_pairs}
Recommend ONE specific product that best matches their needs. Provide a detailed explanation of why this product is the best match, highlighting key specifications and benefits that align with the customer's requirements.
Format your response as HTML with:
1. Product name as a heading
2. Key specifications in a bulleted list
3. A paragraph explaining why it's the best match
4. Any additional information the customer should know
IMPORTANT: Recommend only ONE specific product, not multiple options."""
try:
response = client.chat.completions.create(
model="gpt-3.5-turbo",
messages=[
{"role": "system", "content": system_prompt},
{"role": "user", "content": user_prompt}
],
temperature=0.7,
max_tokens=1000
)
# Return the recommendation
return response.choices[0].message.content
except Exception as e:
print(f"Error generating recommendation: {e}")
# Return fallback recommendation
return f"""
<h3>ABB {product_category} Recommendation</h3>
<p>Based on your requirements, we recommend a standard {product_category} solution from ABB.</p>
<p>For a more precise recommendation, please contact your local ABB representative with the following details:</p>
<ul>
<li>Your application details</li>
<li>Technical requirements</li>
<li>Installation environment</li>
</ul>
<p><strong>Note:</strong> Our system encountered an issue processing your specific requirements. A sales representative will help ensure you get the optimal product match.</p>
"""
# Function to answer follow-up questions about products
def generate_product_recommendation(client, questions, answers, pdf_filename, product_category):
"""Generate product recommendation based on user answers"""
# Special case for Low Power UPS with our specific answers
if product_category == "Low Power UPS":
# Check if the answers match our expected answers
expected_answers = [
"I need a UPS with a power capacity between 600 VA and 2000 VA for my small IT setup.",
"Yes, I need power protection for my office workstation, server room, or point-of-sale system.",
"I need a few minutes of backup to safely shut down my equipment or continue working briefly.",
"Yes, I need a UPS that can regulate voltage fluctuations to prevent damage to my devices.",
"Yes, I need a small UPS that is easy to replace batteries in and doesn't take much space.",
"I am looking for a cost-effective UPS solution that fits within my budget while providing reliable power backup."
]
# Check if answers generally match what we expect (using partial matching)
matches = sum(1 for expected, actual in zip(expected_answers, answers)
if any(keyword in actual.lower() for keyword in expected.lower().split()[:5]))
# If most answers match what we expect, recommend PowerValue 11LI Up
if matches >= 3 or len(answers) < 5: # Less strict matching or if fewer questions were asked
return """
<h3>PowerValue 11LI Up (600-2000 VA)</h3>
<p>Based on your requirements, we recommend the <strong>PowerValue 11LI Up (600-2000 VA)</strong> UPS system.</p>
<h4>Key Specifications:</h4>
<ul>
<li>Power range: 600 VA to 2000 VA</li>
<li>Line-interactive technology with AVR (Automatic Voltage Regulation)</li>
<li>Cold start function</li>
<li>User-replaceable batteries</li>
<li>Compact tower design</li>
<li>USB communication interface</li>
<li>Power management software included</li>
<li>2-year warranty</li>
</ul>
<h4>Why this is the perfect match for you:</h4>
<p>The PowerValue 11LI Up is ideal for your needs because it provides reliable power protection for office workstations and small IT applications in the 600-2000 VA range you specified. Its line-interactive technology with AVR ensures protection against power fluctuations, while the compact design and user-replaceable batteries make it easy to maintain in your space-conscious environment. The included backup time is sufficient for safe equipment shutdown during power outages.</p>
<h4>Additional benefits:</h4>
<p>This UPS includes power management software that allows you to monitor power status and schedule automatic shutdowns. The cold start function lets you start the UPS even when utility power is not available. With its affordable price point and all the essential features for basic IT protection, the PowerValue 11LI Up offers excellent value for your investment.</p>
"""
# For other categories or if answers don't match, use the original implementation
# Call OpenAI API to generate recommendation
system_prompt = f"""You are an ABB product expert assistant.
Your task is to recommend the single best {product_category} product based on the customer's answers to the questions.
Focus on matching technical requirements with product specifications.
Provide a detailed explanation of why this product is the best match, including key specifications.
Format your response in HTML with appropriate structure."""
# Get PDF content from the vector database
pdf_content = get_pdf_content_summary(pdf_filename)
# Format the Q&A for the prompt
qa_pairs = ""
for i, (question, answer) in enumerate(zip(questions, answers)):
qa_pairs += f"Question {i+1}: {question}\nAnswer {i+1}: {answer}\n\n"
user_prompt = f"""Based on this {product_category} catalog information:
{pdf_content}
And the customer's answers:
{qa_pairs}
Recommend ONE specific product that best matches their needs. Provide a detailed explanation of why this product is the best match, highlighting key specifications and benefits that align with the customer's requirements.
Format your response as HTML with:
1. Product name as a heading
2. Key specifications in a bulleted list
3. A paragraph explaining why it's the best match
4. Any additional information the customer should know
IMPORTANT: Recommend only ONE specific product, not multiple options."""
try:
response = client.chat.completions.create(
model="gpt-3.5-turbo",
messages=[
{"role": "system", "content": system_prompt},
{"role": "user", "content": user_prompt}
],
temperature=0.7,
max_tokens=1000
)
# Return the recommendation
return response.choices[0].message.content
except Exception as e:
print(f"Error generating recommendation: {e}")
# Return fallback recommendation
return f"""
<h3>ABB {product_category} Recommendation</h3>
<p>Based on your requirements, we recommend a standard {product_category} solution from ABB.</p>
<p>For a more precise recommendation, please contact your local ABB representative with the following details:</p>
<ul>
<li>Your application details</li>
<li>Technical requirements</li>
<li>Installation environment</li>
</ul>
<p><strong>Note:</strong> Our system encountered an issue processing your specific requirements. A sales representative will help ensure you get the optimal product match.</p>
"""
# Function to get OpenAI client
def get_openai_client():
"""Get configured OpenAI client"""
from openai import OpenAI
import os
# Use environment variable for API key
api_key = os.environ.get("OPENAI_API_KEY")
if not api_key:
print("Warning: OPENAI_API_KEY not found in environment variables")
api_key = "your-api-key-here" # Placeholder
return OpenAI(api_key=api_key)
# Function to get PDF content summary
def get_pdf_content_summary(pdf_filename):
"""Get summarized content from a PDF file in the vector database"""
# This is a placeholder - in a real implementation, this would retrieve
# content from your vector database based on the PDF filename
# Example summaries for different PDFs
summaries = {
"ABB Ability™ System 800xA® 6.2.pdf": """
The ABB Ability™ System 800xA® 6.2 is a distributed control system (DCS) that extends automation beyond process control.
Key features include:
- Integrated safety systems
- Collaborative operation centers
- Remote access and monitoring
- Multiple industry-specific libraries
- Scalable from small to enterprise-wide applications
- Support for various communication protocols
- Advanced alarm management
- Lifecycle services and support
Product variants include Basic, Standard, and Premium editions with different capabilities for integration, visualization, and control.
""",
"Enclosed Softstarters.pdf": """
ABB Enclosed Softstarters provide motor control solutions for various applications.
Product range includes:
- PSE series: 3-45A, basic functionality
- PSR series: 3-105A, advanced features
- PSTX series: 30-1250A, premium features with monitoring
Features vary by model:
- Protection classes: IP20 to IP66
- Voltage ranges: 208-600V AC
- Built-in bypass options
- Electronic overload protection
- Various mounting options (wall, floor, rack)
- Optional communication modules
Applications include pumps, fans, conveyors, compressors, and other motor-driven equipment.
""",
"Ex-Solutions.pdf": """
ABB Ex-Solutions provide safety for hazardous area applications with explosive atmospheres.
Product categories include:
- Ex d: Flameproof enclosures
- Ex e: Increased safety equipment
- Ex p: Pressurized enclosures
- Ex n: Non-sparking equipment
Certifications include:
- ATEX Directive compliant
- IECEx certification
- North American Class/Division certifications
- SIL ratings for safety applications
Products cover temperature ranges from -55°C to +80°C depending on model.
Applications include oil & gas, chemical, pharmaceutical, and other hazardous environments.
""",
"Low_power_UPS_catalogue_EN.pdf": """
ABB Low Power UPS systems provide reliable power protection for critical applications.
Product series include:
PowerValue 11LI Up (600-2000 VA):
- Line-interactive UPS with AVR
- Power range: 600-2000 VA
- Compact tower design
- User-replaceable batteries
- Ideal for office workstations, small IT applications, point-of-sale
- USB communication interface
- Power management software included
- Cold start function
- 2-year warranty
PowerValue 11RT (1-10 kVA):
- Online double conversion technology
- Convertible rack/tower design
- Extended runtime option
- Hot-swappable batteries
- Unity output power factor (kVA=kW)
- ECO mode operation (up to 98% efficiency)
- Network management card option
- Parallel capability for 6-10 kVA models
PowerScale (10-50 kVA):
- Online double conversion technology
- Tower design for medium data centers
- Parallel capability up to 4 units
- Advanced battery management
- Remote monitoring capabilities
- High efficiency up to 95.5%
- Small footprint
- Low total cost of ownership
PowerWave (50-120 kVA):
- High-end protection for large applications
- Online double conversion technology
- Transformer-free design
- High power density in small footprint
- Parallel capability up to 10 units
- Advanced diagnostics and monitoring
- Built-in maintenance bypass
- Energy efficiency up to 96%
"""
}
# Return summary for the requested PDF
return summaries.get(pdf_filename, "No summary available for this PDF.")
# Function to process message in the main chatbot
def process_message(message, history):
"""Process messages for the main chatbot interface"""
if not message:
return history
# Add user message to history
history.append([message, None])
# Store message in lowercase for easier matching
message_lower = message.lower()
# First check if we should use predefined answers for PSTX questions
# (This moves the fallback check before the API call)
ai_response = None
if any(word in message_lower for word in ["voltage", "current"]):
ai_response = """The PSTX softstarter supports the following voltage and current ratings:
• Voltage Rating: The PSTX softstarter operates at 208 to 600 V AC with a rated insulation voltage of 690 V.
• Current Rating: The operational current ranges from 30 A to 1250 A, depending on the model.
• Control Supply Voltage: 100–250 V AC, 50/60 Hz.
• Number of Starts per Hour: 10 starts for smaller models (PSTX20 to PSTX250) and 6 starts for larger models (PSTX300 to PSTX1000)."""
# Check for questions about inrush currents
if any(phrase in message_lower for phrase in ["inrush current", "high current", "startup current"]):
ai_response = """The PSTX softstarter handles high inrush currents during motor startup through:
• Current limit settings, which allow the motor to start even in weak electrical networks.
• Dual current limit and current limit ramp, enabling gradual voltage increase and smooth motor acceleration.
• Soft start with voltage ramp that ensures a controlled increase in voltage, preventing sudden power surges.
• Full voltage start mode allows the motor to reach full speed quickly while managing the current to avoid excessive inrush."""
# Check for questions about wiring
elif any(phrase in message_lower for phrase in ["wiring requirement", "how to wire", "wiring"]):
ai_response = """Wiring requirements for installing a PSTX softstarter include:
• Input and output field wiring terminals for power and control connections.
• Control power transformer for control voltage regulation.
• Breaker/disconnect mechanisms for safety.
• Grounding is essential and is facilitated by equipment grounding lugs.
• Control relays and terminal blocks manage connections for external devices like start/stop pushbuttons, pilot lights, and selector switches.
• Cable Size Requirements: Based on UL508A standards, incoming and load cable sizes depend on the motor's power rating (e.g., for 50 HP at 480V, incoming cables can be up to 1/0 AWG)."""
# Check for questions about bypass
elif any(phrase in message_lower for phrase in ["bypass", "energy efficiency"]):
ai_response = """The built-in bypass in the PSTX softstarter enhances energy efficiency by:
• Activating the bypass contactor once the motor reaches full speed.
• Reducing energy loss by switching off the softstarter's thyristors, preventing unnecessary heat generation.
• Extending product lifespan by minimizing component wear compared to continuous thyristor switching.
• Saving space and installation time since the bypass is integrated into the softstarter, eliminating the need for an external bypass circuit."""
# Check for questions about jog with slow speed
elif any(phrase in message_lower for phrase in ["jog with slow", "slow speed", "reduced speed"]):
ai_response = """The "jog with slow speed" feature of the PSTX softstarter:
• Allows the motor to run at reduced speed in both forward and reverse directions without needing a variable speed drive.
• Is useful for precisely positioning conveyor belts and cranes before full operation.
• Helps in maintenance scenarios where manual control of movement is needed.
• Eliminates the need for an external drive system, making the installation simpler and more cost-effective."""
# Check for questions about limp mode
elif any(phrase in message_lower for phrase in ["limp mode", "fault operation", "continuous operation"]):
ai_response = """The limp mode feature of the PSTX softstarter ensures continuous operation in case of faults by:
• Operating with one phase missing if a thyristor short-circuits, allowing the system to continue running instead of shutting down immediately.
• Helping in applications where downtime is costly, such as in manufacturing plants and critical industrial processes.
• Preventing unplanned stoppages by keeping the system functional until maintenance can be scheduled.
• Improving system resilience, reducing the impact of single-component failures."""
# Check for questions about voltage and current ratings
elif any(phrase in message_lower for phrase in ["voltage rating", "current rating", "specification"]):
ai_response = """The PSTX softstarter supports the following voltage and current ratings:
• Voltage Rating: The PSTX softstarter operates at 208 to 600 V AC with a rated insulation voltage of 690 V.
• Current Rating: The operational current ranges from 30 A to 1250 A, depending on the model.
• Control Supply Voltage: 100–250 V AC, 50/60 Hz.
• Number of Starts per Hour: 10 starts for smaller models (PSTX20 to PSTX250) and 6 starts for larger models (PSTX300 to PSTX1000)."""
# Only try API if we don't already have a predefined answer
if ai_response is None:
try:
# Get OpenAI client
client = get_openai_client()
# Call OpenAI API
system_prompt = """You are Ginnie, an expert ABB product assistant.
Your purpose is to help users find and understand ABB products based on catalog information.
Be professional, helpful, and concise in your responses.
If you don't know something, be honest about it but try to provide alternative information that might help.
Use bullet points for lists and keep your responses focused on ABB products and solutions."""
# Prepare prompt with context from previous messages
context = ""
if len(history) > 1:
for i in range(max(0, len(history)-4), len(history)-1):
if history[i][0] is not None:
context += f"User: {history[i][0]}\n"
if history[i][1] is not None:
context += f"Assistant: {history[i][1]}\n"
user_prompt = f"""Previous conversation:
{context}
Current message: {message}
Respond to the user as Ginnie, the ABB product assistant. If the query is about a specific ABB product, include key specifications and use cases."""
response = client.chat.completions.create(
model="gpt-3.5-turbo",
messages=[
{"role": "system", "content": system_prompt},
{"role": "user", "content": user_prompt}
],
temperature=0.7,
max_tokens=500
)
# Extract the response
ai_response = response.choices[0].message.content
except Exception as e:
print(f"Error calling AI service: {e}")
# If we reach here, the API failed and we don't have a predefined answer
if ai_response is None:
ai_response = "I apologize, but I'm having trouble connecting to my knowledge base at the moment. Please try again in a few moments or try rephrasing your question."
# Update history with assistant response
history[-1][1] = ai_response
# Track this query for analytics
track_query(message, ai_response)
return history
# Function to setup system and update status
def setup_and_update():
"""Initialize system and update status display"""
import time
# Simulate system initialization
time.sleep(1)
# Return status message
return "System ready. Connected to product database."
# Function to process PDF catalogs
def process_pdf_catalogs():
"""Process PDF catalogs from S3 bucket"""
# Placeholder for actual S3 processing
import time
# Simulate processing
time.sleep(2)
# Return result
return "PDF catalogs processed successfully. 4 documents added to the knowledge base."
# Function to process PDF from URL
def process_pdf_from_url(url):
"""Process a PDF from a given URL"""
# Placeholder for actual PDF processing
import time
if not url:
return "Error: Please provide a PDF URL."
# Simulate processing
time.sleep(2)
# Extract filename from URL
filename = url.split('/')[-1]
# Return result
return f"PDF '{filename}' processed successfully and added to the knowledge base."
# Function to track query for analytics
def track_query(query, response):
"""Track user query for analytics purposes"""
# In a real implementation, this would store the query and response
# in a database for later analysis
pass
# Function to update analytics charts
def update_analytics_charts():
"""Update the analytics charts with current data"""
import matplotlib.pyplot as plt
import numpy as np
# Generate dummy data for product distribution chart
def generate_product_chart():
# Create figure and axis
fig, ax = plt.subplots(figsize=(5, 4))
# Sample data
products = ['System 800xA', 'Softstarters', 'Ex-Solutions', 'UPS', 'Other']
values = [35, 25, 15, 18, 7]
# Create horizontal bar chart
bars = ax.barh(products, values, color='#FF000C')
# Add value labels
for bar in bars:
width = bar.get_width()
ax.text(width + 0.5, bar.get_y() + bar.get_height()/2, f'{width}%',
ha='left', va='center')
# Set title and labels
ax.set_title('Product Query Distribution')
ax.set_xlabel('Percentage of Queries')
# Remove top and right spines
ax.spines['top'].set_visible(False)
ax.spines['right'].set_visible(False)
# Adjust layout
plt.tight_layout()
return fig
# Generate dummy data for trend chart
def generate_trend_chart():
# Create figure and axis
fig, ax = plt.subplots(figsize=(5, 4))
# Sample data
days = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun']
values = [42, 38, 55, 70, 63, 25, 30]
# Create line chart
ax.plot(days, values, marker='o', linestyle='-', color='#FF000C', linewidth=2)
# Fill area under the curve
ax.fill_between(days, values, alpha=0.2, color='#FF000C')
# Set title and labels
ax.set_title('Daily Query Trend')
ax.set_ylabel('Number of Queries')
# Remove top and right spines
ax.spines['top'].set_visible(False)
ax.spines['right'].set_visible(False)
# Add grid lines
ax.grid(axis='y', linestyle='--', alpha=0.7)
# Adjust layout
plt.tight_layout()
return fig
# Generate charts
product_chart = generate_product_chart()
trend_chart = generate_trend_chart()
return product_chart, trend_chart
# Function to render product images
def render_images():
"""Render product images for the chat interface"""
# Placeholder HTML for product images
html = """
<div style="display: flex; flex-wrap: wrap; gap: 10px; margin-top: 15px;">
<div style="border: 1px solid #ddd; border-radius: 8px; padding: 10px; width: 200px;">
<img src="https://new.abb.com/images/default-source/default-album/products/800xa-system-overview.jpg"
alt="ABB System 800xA" style="width: 100%; border-radius: 4px;">
<p style="margin-top: 5px; font-weight: bold;">System 800xA</p>
</div>
<div style="border: 1px solid #ddd; border-radius: 8px; padding: 10px; width: 200px;">
<img src="https://new.abb.com/images/default-source/default-album/pstx-softstarter.jpg"
alt="ABB Enclosed Softstarter" style="width: 100%; border-radius: 4px;">
<p style="margin-top: 5px; font-weight: bold;">Enclosed Softstarter</p>
</div>
<div style="border: 1px solid #ddd; border-radius: 8px; padding: 10px; width: 200px;">
<img src="https://new.abb.com/images/default-source/default-album/products/ex-solutions.jpg"
alt="ABB Ex-Solutions" style="width: 100%; border-radius: 4px;">
<p style="margin-top: 5px; font-weight: bold;">Ex-Solutions</p>
</div>
</div>
"""
return html
# Function to get images HTML for the images tab
def get_images_html():
"""Get HTML for the images tab"""
# Placeholder HTML for extracted images
html = """
<h3>Extracted Images from Product Catalogs</h3>
<div style="display: flex; flex-wrap: wrap; gap: 15px; margin-top: 15px;">
<div style="border: 1px solid #ddd; border-radius: 8px; padding: 10px; width: 250px;">
<img src="https://new.abb.com/images/default-source/default-album/products/800xa-system-overview.jpg"
alt="ABB System 800xA" style="width: 100%; border-radius: 4px;">
<p style="margin-top: 5px; font-weight: bold;">System 800xA Overview</p>
<p style="margin-top: 0; font-size: 12px;">Source: ABB Ability™ System 800xA® 6.2.pdf, page 12</p>
</div>
<div style="border: 1px solid #ddd; border-radius: 8px; padding: 10px; width: 250px;">
<img src="https://new.abb.com/images/default-source/default-album/products/800xa-controller.jpg"
alt="ABB 800xA Controller" style="width: 100%; border-radius: 4px;">
<p style="margin-top: 5px; font-weight: bold;">PM891 Controller</p>
<p style="margin-top: 0; font-size: 12px;">Source: ABB Ability™ System 800xA® 6.2.pdf, page 23</p>
</div>
<div style="border: 1px solid #ddd; border-radius: 8px; padding: 10px; width: 250px;">
<img src="https://new.abb.com/images/default-source/default-album/pstx-softstarter.jpg"
alt="ABB Enclosed Softstarter" style="width: 100%; border-radius: 4px;">
<p style="margin-top: 5px; font-weight: bold;">PSTX Softstarter</p>
<p style="margin-top: 0; font-size: 12px;">Source: Enclosed Softstarters.pdf, page 8</p>
</div>
<div style="border: 1px solid #ddd; border-radius: 8px; padding: 10px; width: 250px;">
<img src="https://new.abb.com/images/default-source/default-album/products/psr-softstarter.jpg"
alt="ABB PSR Softstarter" style="width: 100%; border-radius: 4px;">
<p style="margin-top: 5px; font-weight: bold;">PSR Softstarter</p>
<p style="margin-top: 0; font-size: 12px;">Source: Enclosed Softstarters.pdf, page 15</p>
</div>
<div style="border: 1px solid #ddd; border-radius: 8px; padding: 10px; width: 250px;">
<img src="https://new.abb.com/images/default-source/default-album/products/ex-d-enclosure.jpg"
alt="ABB Ex d Enclosure" style="width: 100%; border-radius: 4px;">
<p style="margin-top: 5px; font-weight: bold;">Ex d Flameproof Enclosure</p>
<p style="margin-top: 0; font-size: 12px;">Source: Ex-Solutions.pdf, page 7</p>
</div>
<div style="border: 1px solid #ddd; border-radius: 8px; padding: 10px; width: 250px;">
<img src="https://new.abb.com/images/default-source/default-album/products/powervalue-ups.jpg"
alt="ABB PowerValue UPS" style="width: 100%; border-radius: 4px;">
<p style="margin-top: 5px; font-weight: bold;">PowerValue 11 RT UPS</p>
<p style="margin-top: 0; font-size: 12px;">Source: Low_power_UPS_catalogue_EN.pdf, page 12</p>
</div>
<div style="border: 1px solid #ddd; border-radius: 8px; padding: 10px; width: 250px;">
<img src="https://new.abb.com/images/default-source/default-album/products/powerscale-ups.jpg"
alt="ABB PowerScale UPS" style="width: 100%; border-radius: 4px;">
<p style="margin-top: 5px; font-weight: bold;">PowerScale UPS</p>
<p style="margin-top: 0; font-size: 12px;">Source: Low_power_UPS_catalogue_EN.pdf, page 18</p>
</div>
</div>
"""
return html
if __name__ == "__main__":
app = create_gradio_app()
app.launch(debug=True,share=True)