import streamlit as st
from PyPDF2 import PdfReader
from langchain_text_splitters import RecursiveCharacterTextSplitter
from langchain_huggingface import HuggingFaceEmbeddings, HuggingFacePipeline
from langchain_community.vectorstores import FAISS
from langchain_core.prompts import PromptTemplate
from transformers import AutoTokenizer, AutoModelForCausalLM, pipeline
import torch
import time
import base64
import html as html_module
from datetime import datetime, timezone, timedelta
# ─── TIMEZONE (IST = UTC+5:30) ────────────────────────────────────────────────
IST = timezone(timedelta(hours=5, minutes=30))
def get_ist_time():
return datetime.now(IST).strftime("%H:%M")
st.set_page_config(
page_title="QueryDocs AI",
page_icon="📚",
layout="wide",
initial_sidebar_state="expanded"
)
# ─── HELPERS ──────────────────────────────────────────────────────────────────
def img_to_base64(path):
try:
with open(path, "rb") as f:
return base64.b64encode(f.read()).decode()
except:
return None
# ─── GLOBAL CSS ───────────────────────────────────────────────────────────────
st.markdown("""
""", unsafe_allow_html=True)
# ─── SESSION STATE ────────────────────────────────────────────────────────────
if "messages" not in st.session_state: st.session_state.messages = []
if "vectorstore" not in st.session_state: st.session_state.vectorstore = None
if "pdf_name" not in st.session_state: st.session_state.pdf_name = None
if "pdf_pages" not in st.session_state: st.session_state.pdf_pages = 0
if "pdf_chunks" not in st.session_state: st.session_state.pdf_chunks = 0
if "q_count" not in st.session_state: st.session_state.q_count = 0
if "last_q" not in st.session_state: st.session_state.last_q = ""
if "input_key" not in st.session_state: st.session_state.input_key = 0
if "pending_input" not in st.session_state: st.session_state.pending_input = ""
# ─── MODEL LOADERS ────────────────────────────────────────────────────────────
@st.cache_resource(show_spinner=False)
def load_embeddings():
return HuggingFaceEmbeddings(model_name="sentence-transformers/all-MiniLM-L6-v2")
@st.cache_resource(show_spinner=False)
def load_llm():
model_id = "TinyLlama/TinyLlama-1.1B-chat-v1.0"
tokenizer = AutoTokenizer.from_pretrained(model_id)
model = AutoModelForCausalLM.from_pretrained(
model_id,
torch_dtype=torch.float16 if torch.cuda.is_available() else torch.float32,
low_cpu_mem_usage=True,
device_map="cuda" if torch.cuda.is_available() else None
)
if not torch.cuda.is_available():
model = model.to("cpu")
pipe = pipeline(
"text-generation", model=model, tokenizer=tokenizer,
max_new_tokens=512, temperature=0.3, do_sample=True,
pad_token_id=tokenizer.eos_token_id, repetition_penalty=1.1
)
return HuggingFacePipeline(pipeline=pipe)
# ─── PDF PROCESSOR ────────────────────────────────────────────────────────────
def process_pdf(uploaded_file):
reader = PdfReader(uploaded_file)
raw_text = ""
for page in reader.pages:
text = page.extract_text()
if text:
raw_text += text
if not raw_text.strip():
raise ValueError("No readable text found. PDF may be scanned/image-based.")
splitter = RecursiveCharacterTextSplitter(chunk_size=1000, chunk_overlap=200)
chunks = splitter.split_text(raw_text)
embeddings = load_embeddings()
vectorstore = FAISS.from_texts(chunks, embeddings)
return vectorstore, len(reader.pages), len(chunks)
# ─── ANSWER FUNCTION ──────────────────────────────────────────────────────────
def get_answer(question, vectorstore):
retriever = vectorstore.as_retriever(search_kwargs={"k": 4})
relevant_docs = retriever.invoke(question)
context = "\n\n".join([f"---\n{doc.page_content}" for doc in relevant_docs])
sources = [doc.page_content[:120] + "..." for doc in relevant_docs]
prompt_template = PromptTemplate(
input_variables=["context", "question"],
template="""<|system|>
You are QueryDocs AI, an intelligent document assistant. Use ONLY the context provided to answer the question clearly and accurately. If the answer is not in the context, say so honestly.
<|user|>
CONTEXT:
{context}
QUESTION:
{question}
<|assistant|>
"""
)
llm = load_llm()
chain = prompt_template | llm
result = chain.invoke({"context": context, "question": question})
if "<|assistant|>" in result:
answer = result.split("<|assistant|>")[-1].strip()
else:
answer = result.strip()
return answer, sources
# ─── SIDEBAR ──────────────────────────────────────────────────────────────────
with st.sidebar:
# Profile card
img_b64 = img_to_base64("assets/NANII.png")
avatar = (f''
if img_b64 else
'