import streamlit as st import os import shutil import tempfile import numpy as np import soundfile as sf from dotenv import load_dotenv from faster_whisper import WhisperModel from langchain_google_genai import ChatGoogleGenerativeAI from langchain_core.prompts import PromptTemplate from langchain_huggingface import HuggingFaceEmbeddings from langchain_community.vectorstores import Chroma from langchain_community.document_loaders import ( PyPDFLoader, TextLoader, UnstructuredPowerPointLoader ) from langchain.text_splitter import RecursiveCharacterTextSplitter from langchain.retrievers.document_compressors import LLMChainExtractor try: # Newer structure (LangChain 0.3.27+) from langchain_community.retrievers.contextual_compression import ContextualCompressionRetriever except ImportError: # Fallback for older builds from langchain.retrievers import ContextualCompressionRetriever # Streamlit WebRTC for live voice from streamlit_webrtc import webrtc_streamer, AudioProcessorBase, WebRtcMode # ------------------------------ # ๐ŸŒ Load Environment # ------------------------------ load_dotenv() GOOGLE_API_KEY = os.getenv("GOOGLE_API_KEY") if not GOOGLE_API_KEY: st.error("โš ๏ธ GOOGLE_API_KEY not found! Add it to .env or repo secrets.") st.stop() os.environ["GOOGLE_API_KEY"] = GOOGLE_API_KEY # ------------------------------ # โš™๏ธ Config # ------------------------------ CHROMA_DIR = "./chroma_db" MAX_MEMORY_TURNS = 10 # ------------------------------ # ๐Ÿงฉ Load Documents Helper # ------------------------------ def load_documents(uploaded_files): docs = [] for file in uploaded_files: temp_path = os.path.join(tempfile.gettempdir(), file.name) with open(temp_path, "wb") as f: f.write(file.getbuffer()) if file.name.endswith(".pdf"): loader = PyPDFLoader(temp_path) elif file.name.endswith(".txt"): loader = TextLoader(temp_path, encoding="utf-8") elif file.name.endswith(".pptx"): loader = UnstructuredPowerPointLoader(temp_path) else: continue docs.extend(loader.load()) return docs # ------------------------------ # ๐Ÿง  Embeddings # ------------------------------ @st.cache_resource def get_embeddings(): return HuggingFaceEmbeddings(model_name="sentence-transformers/paraphrase-MiniLM-L3-v2") embeddings = get_embeddings() # ------------------------------ # ๐Ÿ’พ Session State # ------------------------------ if "chat_history" not in st.session_state: st.session_state.chat_history = [] if "retriever" not in st.session_state: st.session_state.retriever = None # ------------------------------ # ๐Ÿงญ Sidebar Controls # ------------------------------ st.sidebar.header("โš™๏ธ Controls") mode = st.sidebar.radio("Choose Mode:", ["๐Ÿš€ Fast Mode", "๐ŸŽฏ Accurate Mode"]) uploaded_files = st.sidebar.file_uploader( "๐Ÿ“‚ Upload PDF/TXT/PPTX files", type=["pdf", "txt", "pptx"], accept_multiple_files=True, ) if st.sidebar.button("๐Ÿงน Clear Chat"): st.session_state.chat_history = [] st.rerun() if st.sidebar.button("๐Ÿ—‘๏ธ Clear Database"): if os.path.exists(CHROMA_DIR): shutil.rmtree(CHROMA_DIR) st.session_state.retriever = None st.session_state.chat_history = [] st.sidebar.success("โœ… Database and chat cleared!") else: st.sidebar.warning("โš ๏ธ No database found.") # ------------------------------ # ๐Ÿงพ Document Processing # ------------------------------ if uploaded_files: splitter = RecursiveCharacterTextSplitter( chunk_size=250 if mode == "๐Ÿš€ Fast Mode" else 800, chunk_overlap=30 if mode == "๐Ÿš€ Fast Mode" else 100, ) documents = load_documents(uploaded_files) split_docs = splitter.split_documents(documents) vectorstore = Chroma.from_documents(split_docs, embeddings, persist_directory=CHROMA_DIR) vectorstore.persist() if mode == "๐ŸŽฏ Accurate Mode": llm_for_compression = ChatGoogleGenerativeAI( model="gemini-2.5-flash", google_api_key=GOOGLE_API_KEY, temperature=0.2 ) compressor = LLMChainExtractor.from_llm(llm_for_compression) retriever = ContextualCompressionRetriever( base_compressor=compressor, base_retriever=vectorstore.as_retriever(search_kwargs={"k": 5}), ) else: retriever = vectorstore.as_retriever(search_kwargs={"k": 3}) st.session_state.retriever = retriever st.sidebar.success(f"โœ… Docs indexed in *{mode}* mode!") # ------------------------------ # ๐Ÿ’ฌ Main Chat Interface # ------------------------------ st.title("๐Ÿ“š AskDocs โ€“ Smart Multimodal Chatbot") for msg in st.session_state.chat_history: with st.chat_message(msg["role"]): st.markdown(msg["content"]) # ------------------------------ # ๐ŸŽ™๏ธ Live Voice Input # ------------------------------ st.subheader("๐ŸŽค Speak Your Question") class WhisperAudioProcessor(AudioProcessorBase): def __init__(self): self.frames = [] def recv_audio(self, frame): self.frames.append(frame.to_ndarray()) return frame webrtc_ctx = webrtc_streamer( key="whisper", mode=WebRtcMode.SENDRECV, audio_processor_factory=WhisperAudioProcessor, media_stream_constraints={"audio": True, "video": False}, async_processing=True ) voice_question = None if webrtc_ctx.audio_processor and webrtc_ctx.audio_processor.frames: frames = webrtc_ctx.audio_processor.frames audio_data = np.concatenate(frames, axis=0) tmp_path = os.path.join(tempfile.gettempdir(), "voice.wav") sf.write(tmp_path, audio_data, 48000) with st.spinner("๐ŸŽง Transcribing voice..."): model = WhisperModel("base", device="cpu", compute_type="int8") segments, _ = model.transcribe(tmp_path) voice_question = " ".join([seg.text for seg in segments]) st.success(f"๐Ÿ—ฃ๏ธ You said: {voice_question}") # ------------------------------ # ๐Ÿ’ฌ Text Chat Input # ------------------------------ st.markdown("---") user_input = st.chat_input("Type your question here...") question = voice_question or user_input # ------------------------------ # ๐Ÿค– Response Generation # ------------------------------ if question: st.session_state.chat_history.append({"role": "user", "content": question}) with st.chat_message("user"): st.markdown(question) if not st.session_state.retriever: with st.chat_message("assistant"): st.error("โš ๏ธ Please upload documents first!") else: retrieved_docs = st.session_state.retriever.invoke(question) context_text = "\n\n".join(doc.page_content for doc in retrieved_docs) history_text = "\n".join([ f"{t['role']}: {t['content']}" for t in st.session_state.chat_history[-MAX_MEMORY_TURNS * 2:] ]) llm = ChatGoogleGenerativeAI( model="gemini-2.5-flash", google_api_key=GOOGLE_API_KEY, temperature=0.2 ) template = """ You are a smart and reliable assistant. Use the chat history and retrieved context to answer. If unsure, say so. Chat History: {history} Retrieved Context: {context} Question: {question} Answer Instructions: - Provide answers in this order: 1. *LLM Answer:* Direct and concise. 2. *Explanation:* Detailed reasoning and context. 3. *Summary:* Short key takeaway. - Cite sources when relevant. Answer: """ prompt = PromptTemplate.from_template(template) final_prompt = prompt.invoke({ "history": history_text, "context": context_text, "question": question }) with st.chat_message("assistant"): with st.spinner("๐Ÿค– Thinking..."): response = llm.invoke(final_prompt).content st.write_stream(iter([response])) st.session_state.chat_history.append({"role": "assistant", "content": response}) # ------------------------------ # ๐Ÿช„ Footer # ------------------------------ st.divider() st.caption("โšก Powered by Gemini + LangChain + HuggingFace + Streamlit + Whisper + WebRTC")