santipenas's picture
Update app.py
2bec50e verified
Raw
History Blame Contribute Delete
7.31 kB
# ====== CORE SETUP ======
# Note: These are the foundation for our app
import os
from dotenv import load_dotenv
import streamlit as st # Our app framework
# ====== PDF HANDLING ======
# Note: pypdf is lightweight and handles most PDFs well
from pypdf import PdfReader # Better than PyPDF2 for our needs
# ====== LANGCHAIN COMPONENTS ======
# Note: We're using LangChain for text processing pipelines
from langchain.text_splitter import CharacterTextSplitter # For chunking text
from langchain_community.llms import HuggingFaceHub # For summary generation
from langchain.vectorstores import FAISS # Local vector storage
from langchain_community.embeddings import HuggingFaceEmbeddings # Text embeddings
from langchain.chains.question_answering import load_qa_chain # Backup QA method
# ====== TRANSFORMERS ======
# Note: Direct HuggingFace imports for more control
from transformers import (
pipeline, # For ready-to-use NLP pipelines
AutoModelForQuestionAnswering, # Custom QA models
AutoTokenizer # Handles model tokenization
)
# ====== ENVIRONMENT SETUP ======
load_dotenv() # Loads from .env file (keep your API key here)
# Safety check for HuggingFace token
hf_token = os.getenv("HUGGINGFACEHUB_API_TOKEN")
if not hf_token:
st.error("⚠️ Hugging Face API token not found. Please add it as a secret in Hugging Face Spaces.")
st.stop() # Graceful exit if missing token
os.environ["HUGGINGFACEHUB_API_TOKEN"] = hf_token # Set for LangChain
# ====== STREAMLIT UI ======
st.set_page_config(page_title="Santiago's PDF Summarizer & Q&A")
st.title("πŸ“„ Santiago's PDF Summarizer & Q&A")
st.write("Summarize your PDF or ask questions about its content using free Hugging Face models.")
st.divider()
# PDF upload widget - shows only once
pdf = st.file_uploader("Upload your PDF", type="pdf")
# Show buttons only after PDF upload to prevent errors
if pdf is not None:
summary_btn = st.button("πŸ“š Generate Summary")
qa_btn = st.button("❓ Ask a Question")
user_question = st.text_input("Type your question here (for Q&A only):")
# ====== CORE FUNCTIONS ======
def extract_text_from_pdf(pdf):
"""Extracts raw text from PDF with error handling"""
try:
pdf_reader = PdfReader(pdf)
text = ""
for page in pdf_reader.pages:
text += page.extract_text() or "" # Handles None returns
return text
except Exception as e:
st.error(f"Error reading PDF: {str(e)}")
return None
def summarize_pdf(text):
"""Generates summary using BART model with chunking"""
try:
# Chunking prevents model context window overflow
text_splitter = CharacterTextSplitter(
separator="\n",
chunk_size=1000, # Optimal for BART-large
chunk_overlap=100, # Maintains context between chunks
length_function=len
)
chunks = text_splitter.split_text(text)
# Using BART specifically for summarization
llm = HuggingFaceHub(
repo_id="facebook/bart-large-cnn", # Specialized for summaries
model_kwargs={
"temperature": 0.5, # Balances creativity vs accuracy
"max_length": 100 # Keeps summaries concise
}
)
# Process each chunk separately then combine
summaries = []
for chunk in chunks:
prompt = f"Summarize this: {chunk}" # Simple but effective prompt
summary = llm(prompt)
summaries.append(summary)
return "\n\n".join(summaries) # Combine with spacing
except Exception as e:
st.error(f"Summarization error: {str(e)}")
return None
def answer_question(text, question):
"""Handles Q&A with context-aware responses"""
try:
# --- Text Preparation ---
text_splitter = CharacterTextSplitter(
separator="\n",
chunk_size=1200, # Larger chunks for better context
chunk_overlap=200, # Prevents information loss at edges
length_function=len
)
chunks = text_splitter.split_text(text)
# --- Semantic Search Setup ---
embeddings = HuggingFaceEmbeddings(
model_name="sentence-transformers/multi-qa-mpnet-base-dot-v1" # QA-optimized
)
knowledge_base = FAISS.from_texts(chunks, embeddings)
# Retrieve most relevant sections
docs = knowledge_base.similarity_search(question, k=4) # Get top 4 matches
if not docs:
return "I couldn't find relevant information for this question."
# --- QA Model Configuration ---
model_name = "deepset/roberta-base-squad2" # Reliable PyTorch model
tokenizer = AutoTokenizer.from_pretrained(model_name)
model = AutoModelForQuestionAnswering.from_pretrained(model_name)
# Pipeline with optimized settings
qa_pipeline = pipeline(
"question-answering",
model=model,
tokenizer=tokenizer,
max_seq_len=384, # Standard for RoBERTa
top_k=2, # Get two potential answers
handle_impossible_answer=True # Better than failing
)
# --- Answer Generation ---
context = "\n\n".join([doc.page_content for doc in docs])
results = qa_pipeline(question=question, context=context, top_k=2)
if not results or results[0]['answer'].strip() == "":
return "The document doesn't contain a clear answer to this question."
# --- Response Enrichment ---
primary_answer = results[0]['answer'].strip()
secondary_answer = results[1]['answer'].strip() if len(results) > 1 else None
response = f"{primary_answer}"
# Add secondary answer if different and valuable
if secondary_answer and secondary_answer.lower() != primary_answer.lower():
response += f"\n\nAdditional context: {secondary_answer}"
# Include supporting evidence
response += "\n\n**Supporting Excerpts:**"
for i, doc in enumerate(docs[:2]): # Limit to 2 for readability
response += f"\n\n- Excerpt {i+1}: {doc.page_content[:250]}..." # Preview
return response
except Exception as e:
st.error(f"Error processing question: {str(e)}")
return "Sorry, I encountered an error. Please try again with a different question."
# ====== MAIN EXECUTION FLOW ======
if pdf is not None:
with st.spinner("Reading and processing the PDF..."):
full_text = extract_text_from_pdf(pdf)
if full_text is None:
st.stop() # Don't proceed if text extraction failed
# Summary generation path
if summary_btn and full_text:
with st.spinner("Generating summary..."):
summary = summarize_pdf(full_text)
if summary:
st.subheader("πŸ“š PDF Summary")
st.write(summary) # Display with proper formatting
# Q&A path
if qa_btn and user_question.strip() != "" and full_text:
with st.spinner("Finding the answer..."):
answer = answer_question(full_text, user_question)
if answer:
st.subheader("❓ Answer to Your Question")
st.write(answer) # Renders markdown formatting