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('