import os
import asyncio
import hashlib
from dotenv import load_dotenv
import gradio as gr
# Load environment variables
load_dotenv()
GOOGLE_API_KEY = os.getenv("GOOGLE_API_KEY")
from langchain_pypdf import PyPDFLoader # Fix: was langchain_community
from langchain_text_splitters import RecursiveCharacterTextSplitter
from langchain_huggingface import HuggingFaceEmbeddings
from langchain_community.vectorstores import FAISS
from langchain_core.prompts import PromptTemplate # Fix: replaces load_qa_chain
from langchain_core.output_parsers import StrOutputParser # Fix: replaces load_qa_chain
from langchain_google_genai import ChatGoogleGenerativeAI
# Pre-initialize Embeddings Model to prevent slow reloads on every upload
print("LOG: Pre-initializing HuggingFace Embeddings model...")
EMBEDDINGS = HuggingFaceEmbeddings(
model_name="sentence-transformers/all-MiniLM-L6-v2",
model_kwargs={"device": "cpu"},
)
print("LOG: Embeddings pipeline ready.")
def build_vectorstore_gradio(uploaded_file):
if uploaded_file is None:
return (
None,
"""
Waiting for PDF Upload...
""",
gr.update(interactive=False, placeholder="Upload a PDF on the left first to enable questioning..."),
gr.update(visible=False),
)
file_path = uploaded_file.name
file_name = os.path.basename(file_path)
# Read bytes to compute MD5 hash for unique tracking/logs
with open(file_path, "rb") as f:
file_bytes = f.read()
file_hash = hashlib.md5(file_bytes).hexdigest()
print(f"LOG: Parsing PDF '{file_name}' (Hash: {file_hash})...")
try:
loader = PyPDFLoader(file_path)
docs = loader.load()
chunks = RecursiveCharacterTextSplitter(
chunk_size=750, chunk_overlap=75
).split_documents(docs)
print(f"LOG: Splitting complete. Total chunks created: {len(chunks)}")
vectorstore = FAISS.from_documents(chunks, EMBEDDINGS)
print("LOG: Vector store indexing finished.")
status_msg = f"""
✓ Index Built Successfully!
Document: {file_name}
Size: {os.path.getsize(file_path) / 1024:.1f} KB
Total Segments: {len(chunks)} Chunks
"""
return (
(chunks, vectorstore),
status_msg,
gr.update(interactive=True, placeholder="Ask a question about the document..."),
gr.update(visible=True),
)
except Exception as e:
err_msg = f"""
⚠️ Failed to process PDF
{str(e)}
"""
return None, err_msg, gr.update(interactive=False), gr.update(visible=False)
def answer_question_gradio(question, state, api_key_input):
api_key = api_key_input.strip() or GOOGLE_API_KEY
if not api_key:
return (
"⚠️ **Error:** `GOOGLE_API_KEY` is missing. Please add it to your `.env` file or enter it in the input field.",
"",
)
if not state:
return "⚠️ **Error:** No active document. Please upload a PDF file first.", ""
if not question.strip():
return "⚠️ Please type in a question.", ""
chunks, vectorstore = state
try:
# Retrieve context
relevant_docs = vectorstore.similarity_search(question, k=4)
# Load LLM
llm = ChatGoogleGenerativeAI(
google_api_key=api_key,
model="gemini-2.5-flash",
temperature=0.3,
max_retries=0,
)
# Modern LCEL chain — replaces deprecated load_qa_chain
prompt = PromptTemplate.from_template(
"Use the following context to answer the question as accurately as possible.\n\n"
"Context:\n{context}\n\n"
"Question: {question}\n\n"
"Answer:"
)
chain = prompt | llm | StrOutputParser()
context = "\n\n".join(doc.page_content for doc in relevant_docs)
answer = chain.invoke({"context": context, "question": question})
# Format sources as HTML/Markdown citations
sources_html = ""
for i, doc in enumerate(relevant_docs, 1):
page = doc.metadata.get("page", 0) + 1
sources_html += f"""
Chunk {i} — Page {page}
"...{doc.page_content.strip()}..."
"""
return answer, sources_html
except Exception as e:
err = str(e)
if "429" in err or "quota" in err.lower():
return (
"⚠️ **Rate Limit Hit:** You have exceeded the free tier quota for Gemini API. Please wait a minute and try again.",
"",
)
return f"⚠️ **Error:** {err}", ""
# Custom Premium CSS Styling (DocuMind Theme)
custom_css = """
body {
background-color: #0d0e15 !important;
color: #f3f4f6 !important;
}
.gradio-container {
max-width: 1200px !important;
margin: 0 auto !important;
font-family: 'Plus Jakarta Sans', -apple-system, sans-serif !important;
}
/* Title and Header styling */
.header-container {
text-align: center;
padding: 2rem 0;
margin-bottom: 1.5rem;
border-bottom: 1px solid rgba(255, 255, 255, 0.05);
}
.header-container h1 {
font-size: 3rem;
font-weight: 800;
background: linear-gradient(135deg, #3B82F6 0%, #8B5CF6 50%, #06B6D4 100%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
letter-spacing: -1.5px;
margin: 0;
}
.header-container p {
color: #8A8D9F;
font-size: 1.15rem;
margin: 8px 0 0 0;
}
/* Primary Button Gradient */
button.primary {
background: linear-gradient(135deg, #8B5CF6 0%, #3B82F6 100%) !important;
color: white !important;
border: none !important;
font-weight: 600 !important;
transition: all 0.2s ease !important;
border-radius: 8px !important;
}
button.primary:hover {
transform: translateY(-1px);
box-shadow: 0 4px 12px rgba(139, 92, 246, 0.35) !important;
}
"""
with gr.Blocks(title="DocuMind AI") as demo:
gr.HTML(
"""
"""
)
# State to hold chunks and vectorstore
doc_state = gr.State(None)
with gr.Row():
# Sidebar / Left column
with gr.Column(scale=1, min_width=320):
gr.Markdown("### 📂 Upload Document")
pdf_file = gr.File(
label="Select or Drop PDF File",
file_types=[".pdf"],
file_count="single",
)
status_panel = gr.HTML(
"""
Waiting for PDF Upload...
"""
)
gr.Markdown("### 🔑 API Authentication")
api_key_box = gr.Textbox(
label="Google API Key Override (Optional)",
placeholder="AIzaSy... (Leave empty to use .env key)",
type="password",
)
gr.Markdown(
"""
💡 Private & Local Processing:
Your document is parsed locally inside the workspace environment. The FAISS vector database operates securely in memory.
"""
)
# Workspace Panel / Right column
with gr.Column(scale=2):
gr.Markdown("### 💬 Ask Questions")
query_box = gr.Textbox(
label="Ask a question about the document:",
placeholder="Upload a PDF on the left first to enable questioning...",
interactive=False,
lines=2,
)
with gr.Row():
submit_btn = gr.Button("Submit Question", variant="primary")
clear_btn = gr.Button("Clear Workspace")
gr.Markdown("### 💡 Answer Output")
answer_box = gr.Markdown(
"*(Upload a PDF and submit your question to view the AI output here.)*"
)
sources_accordion = gr.Accordion(
"📚 Relevant Source Citations Used", open=True, visible=False
)
with sources_accordion:
sources_box = gr.HTML("No document sources loaded.")
# Wiring Event Handlers
pdf_file.change(
fn=build_vectorstore_gradio,
inputs=[pdf_file],
outputs=[doc_state, status_panel, query_box, sources_accordion],
)
# When clear is clicked
def clear_workspace():
return (
"",
"*(Upload a PDF and submit your question to view the AI output here.)*",
"No document sources loaded.",
)
clear_btn.click(
fn=clear_workspace,
inputs=[],
outputs=[query_box, answer_box, sources_box],
)
# Submitting questions
submit_btn.click(
fn=answer_question_gradio,
inputs=[query_box, doc_state, api_key_box],
outputs=[answer_box, sources_box],
)
query_box.submit(
fn=answer_question_gradio,
inputs=[query_box, doc_state, api_key_box],
outputs=[answer_box, sources_box],
)
if __name__ == "__main__":
asyncio.set_event_loop(asyncio.new_event_loop())
demo.launch(
server_name="127.0.0.1",
server_port=7860,
share=False,
theme=gr.themes.Default(),
css=custom_css,
)