import streamlit as st import os import json import time from datetime import datetime from dotenv import load_dotenv from langchain_openai import OpenAIEmbeddings, ChatOpenAI from langchain_community.vectorstores import FAISS from langchain_classic.chains import RetrievalQA from langchain_core.prompts import PromptTemplate # ─── Page Config ──────────────────────────────────────────────────────────── st.set_page_config( page_title="Multi-Model Research Assistant", page_icon="🔬", layout="wide", initial_sidebar_state="expanded" ) # ─── Custom CSS ───────────────────────────────────────────────────────────── st.markdown(""" """, unsafe_allow_html=True) # ─── Constants ─────────────────────────────────────────────────────────────── MODELS = { "Meta R1": { "id": "meta-llama/llama-3.3-70b-instruct", "badge_class": "badge-meta", "icon": "🧠", "desc": "Best for math & reasoning" }, "Qwen3 30B": { "id": "qwen/qwen3-next-80b-a3b-instruct", "badge_class": "badge-qwen", "icon": "⚡", "desc": "Strong at science & analysis" }, "Gemma 3 27B": { "id": "google/gemma-3-27b-it", "badge_class": "badge-gemma", "icon": "✨", "desc": "Balanced & instruction-tuned" } } HISTORY_FILE = "chat_history.json" # ─── History Helpers ───────────────────────────────────────────────────────── def load_history(): if os.path.exists(HISTORY_FILE): with open(HISTORY_FILE, "r") as f: return json.load(f) return [] def save_history(history): with open(HISTORY_FILE, "w") as f: json.dump(history, f, indent=2) def add_to_history(question, answers: dict): history = load_history() history.insert(0, { "id": str(int(time.time())), "question": question, "answers": answers, "timestamp": datetime.now().strftime("%b %d, %H:%M") }) history = history[:50] # keep last 50 only save_history(history) return history # ─── RAG Loader ────────────────────────────────────────────────────────────── @st.cache_resource(show_spinner=False) def load_vectorstore(): load_dotenv() api_key = os.environ.get("OPENROUTER_API_KEY") or os.getenv("OPENROUTER_API_KEY") base_url = "https://openrouter.ai/api/v1" index_path = "faiss_index" if not os.path.exists(index_path): return None, "❌ No FAISS index found. Run `rag_engine.py` first." embeddings = OpenAIEmbeddings( model="qwen/qwen3-embedding-8b", openai_api_key=api_key, openai_api_base=base_url, check_embedding_ctx_length=False ) vectorstore = FAISS.load_local( index_path, embeddings, allow_dangerous_deserialization=True ) return vectorstore, None def build_chain(vectorstore, model_id: str): load_dotenv() api_key = os.environ.get("OPENROUTER_API_KEY") or os.getenv("OPENROUTER_API_KEY") base_url = "https://openrouter.ai/api/v1" llm = ChatOpenAI( model=model_id, openai_api_key=api_key, openai_api_base=base_url, temperature=0.3, max_tokens=800 ) template = """Use the following context to answer the question. If the answer involves mathematical formulas, format them using LaTeX syntax ($$ for block equations, $ for inline math). If the answer is NOT in the context, say: "I don't have enough information in the provided documents to answer this." Context: {context} Question: {question} Answer (Markdown + LaTeX if needed):""" prompt = PromptTemplate(input_variables=["context", "question"], template=template) return RetrievalQA.from_chain_type( llm=llm, chain_type="stuff", retriever=vectorstore.as_retriever(search_kwargs={"k": 4}), chain_type_kwargs={"prompt": prompt} ) def query_model(vectorstore, model_id: str, question: str) -> str: try: chain = build_chain(vectorstore, model_id) result = chain.invoke(question)["result"] return result except Exception as e: if "402" in str(e): return "⚠️ Insufficient credits on OpenRouter." elif "rate" in str(e).lower(): return "⚠️ Rate limit hit. Please wait a moment and retry." else: return f"⚠️ Error: {str(e)}" # ─── Session State Init ─────────────────────────────────────────────────────── if "chat_history" not in st.session_state: st.session_state.chat_history = load_history() if "current_question" not in st.session_state: st.session_state.current_question = None if "current_answers" not in st.session_state: st.session_state.current_answers = {} if "selected_history" not in st.session_state: st.session_state.selected_history = None # ─── Sidebar ───────────────────────────────────────────────────────────────── with st.sidebar: st.markdown('
⚙ System Status
', unsafe_allow_html=True) vectorstore, vs_error = load_vectorstore() if vs_error: st.markdown(f' Index not found', unsafe_allow_html=True) st.error(vs_error) else: st.markdown(' Knowledge base ready', unsafe_allow_html=True) st.markdown("---") st.markdown('
🤖 Active Models
', unsafe_allow_html=True) for name, info in MODELS.items(): st.markdown(f"{info['icon']} **{name}** — _{info['desc']}_") st.markdown("---") st.markdown('
📜 Chat History
', unsafe_allow_html=True) if st.button("🗑 Clear History", use_container_width=True): st.session_state.chat_history = [] if os.path.exists(HISTORY_FILE): os.remove(HISTORY_FILE) st.session_state.selected_history = None st.rerun() st.markdown("") for item in st.session_state.chat_history: label = item["question"][:42] + "..." if len(item["question"]) > 42 else item["question"] if st.button(f"💬 {label}", key=f"hist_{item['id']}", use_container_width=True): st.session_state.selected_history = item st.session_state.current_question = item["question"] st.session_state.current_answers = item["answers"] # ─── Main Area ─────────────────────────────────────────────────────────────── st.markdown("""

🔬 Multi-Model Research Assistant

""", unsafe_allow_html=True) # Show current Q&A if any if st.session_state.current_question: st.markdown(f"#### 🙋 {st.session_state.current_question}") st.markdown("---") cols = st.columns(3) for i, (name, info) in enumerate(MODELS.items()): with cols[i]: answer = st.session_state.current_answers.get(name, "_No answer yet._") st.markdown(f"""
{info['icon']} {name}
""", unsafe_allow_html=True) st.markdown(answer) st.markdown("
", unsafe_allow_html=True) st.markdown("---") else: # Welcome screen st.markdown("""
📚
Ask anything — get answers from 3 models simultaneously
Meta R1  ·  Qwen3 30B  ·  Gemma 3 27B
""", unsafe_allow_html=True) # ─── Chat Input ─────────────────────────────────────────────────────────────── if prompt := st.chat_input("Ask about your documents..."): if not vectorstore: st.error("Knowledge base not loaded. Please run `rag_engine.py` first.") else: st.session_state.current_question = prompt st.session_state.current_answers = {} st.session_state.selected_history = None # Show question immediately st.markdown(f"#### 🙋 {prompt}") st.markdown("---") cols = st.columns(3) answers = {} for i, (name, info) in enumerate(MODELS.items()): with cols[i]: st.markdown(f"""
{info['icon']} {name} """, unsafe_allow_html=True) with st.spinner(f"Querying {name}..."): ans = query_model(vectorstore, info["id"], prompt) answers[name] = ans st.markdown(ans) st.markdown("
", unsafe_allow_html=True) # Save to history st.session_state.current_answers = answers st.session_state.chat_history = add_to_history(prompt, answers) st.rerun()