Instantly.
", unsafe_allow_html=True) # ── Chat history ─────────────────────────────────────────────────────── if st.session_state.history: st.markdown('
import streamlit as st import pypdf import numpy as np import os import re import ast import json from datetime import datetime # ── Page config ─────────────────────────────────────────────────────────────── st.set_page_config( page_title="Nexus AI", page_icon="⚡", layout="wide", initial_sidebar_state="expanded", ) # ── CSS ─────────────────────────────────────────────────────────────────────── st.markdown(""" """, unsafe_allow_html=True) # ── Model loading (cached) ──────────────────────────────────────────────────── @st.cache_resource(show_spinner=False) def load_embedder(): from sentence_transformers import SentenceTransformer return SentenceTransformer('all-MiniLM-L6-v2') # ── PDF helpers ─────────────────────────────────────────────────────────────── def extract_pdf(file, doc_name: str): file.seek(0) reader = pypdf.PdfReader(file) chunks = [] full_text = "" CHUNK_SIZE, OVERLAP = 280, 55 for page_idx, page in enumerate(reader.pages): page_text = page.extract_text() or "" full_text += page_text + "\n" words = page_text.split() start = 0 while start < len(words): chunk_text = " ".join(words[start: start + CHUNK_SIZE]) if chunk_text.strip(): chunks.append({ "text": chunk_text, "page": page_idx + 1, "doc": doc_name, }) start += CHUNK_SIZE - OVERLAP meta = { "pages": len(reader.pages), "words": len(full_text.split()), "chunks": len(chunks), "full_text": full_text[:5000], } return chunks, meta # ── Embeddings ──────────────────────────────────────────────────────────────── def embed_chunks(chunks): model = load_embedder() texts = [c["text"] for c in chunks] return model.encode(texts, show_progress_bar=False, batch_size=32) def semantic_search(query: str, all_chunks, all_embeddings, k=5, doc_filter=None): from sklearn.metrics.pairwise import cosine_similarity if all_embeddings is None or len(all_chunks) == 0: return [] model = load_embedder() q_emb = model.encode([query]) if doc_filter: indices = [i for i, c in enumerate(all_chunks) if c["doc"] in doc_filter] else: indices = list(range(len(all_chunks))) if not indices: return [] filtered_embs = all_embeddings[indices] scores = cosine_similarity(q_emb, filtered_embs)[0] top_local = np.argsort(scores)[::-1][:k] results = [] for local_idx in top_local: global_idx = indices[local_idx] results.append({ "text": all_chunks[global_idx]["text"], "page": all_chunks[global_idx]["page"], "doc": all_chunks[global_idx]["doc"], "score": float(scores[local_idx]), }) return results # ── LLM helpers ─────────────────────────────────────────────────────────────── def _build_messages(system, user_msg, history): msgs = [{"role": "system", "content": system}] for h in history[-4:]: msgs.append({"role": "user", "content": h["q"]}) msgs.append({"role": "assistant", "content": h["a"]}) msgs.append({"role": "user", "content": user_msg}) return msgs def call_groq(api_key: str, system: str, user_msg: str, history): try: from groq import Groq client = Groq(api_key=api_key) r = client.chat.completions.create( model="llama-3.1-8b-instant", messages=_build_messages(system, user_msg, history), max_tokens=1024, temperature=0.3, ) return r.choices[0].message.content.strip() except Exception as e: err = str(e) if "429" in err: return "⚠️ Rate limit — wait a moment and retry." if "401" in err: return "⚠️ Invalid Groq API key. Check sidebar." return f"⚠️ Groq error: {err}" def call_gemini(api_key: str, system: str, user_msg: str, history): try: import google.generativeai as genai genai.configure(api_key=api_key) model = genai.GenerativeModel( model_name="gemini-1.5-flash", system_instruction=system, ) hist = [] for h in history[-4:]: hist.append({"role": "user", "parts": [h["q"]]}) hist.append({"role": "model", "parts": [h["a"]]}) chat = model.start_chat(history=hist) return chat.send_message(user_msg).text except Exception as e: err = str(e) if "API_KEY" in err or "invalid" in err.lower(): return "⚠️ Invalid Gemini API key. Check sidebar." return f"⚠️ Gemini error: {err}" def call_llm(provider, keys, system, user_msg, history): if provider == "Groq": return call_groq(keys.get("groq", ""), system, user_msg, history) return call_gemini(keys.get("gemini", ""), system, user_msg, history) # ── Core RAG answer ─────────────────────────────────────────────────────────── def get_answer(provider, keys, results, question, history): context_parts = [] for r in results: score_pct = int(r["score"] * 100) context_parts.append( f"[Source: {r['doc']} | Page {r['page']} | Relevance: {score_pct}%]\n{r['text']}" ) context = "\n\n---\n\n".join(context_parts) system = ( "You are Nexus, a precise AI document assistant.\n" "Rules:\n" "- Answer ONLY from the provided context. Never fabricate information.\n" "- If the answer is not in the context, say: 'This information is not available in the uploaded documents.'\n" "- Always cite the source page when referencing specific facts (e.g. 'According to Page 4...').\n" "- Use markdown formatting: **bold** for key terms, bullet lists for multi-point answers.\n" "- Be concise but complete." ) user_msg = f"RETRIEVED CONTEXT:\n{context}\n\nQUESTION: {question}" return call_llm(provider, keys, system, user_msg, history) # ── Auto summary ────────────────────────────────────────────────────────────── def get_summary(provider, keys, full_text): system = "You are a document analyst. Be structured and concise." user_msg = ( "Provide a structured document summary:\n\n" "**Main Topic:** (1 sentence)\n" "**Key Points:** (4 bullet points)\n" "**Important Data/Facts:** (3 bullet points)\n" "**Conclusion:** (1-2 sentences)\n\n" f"Document:\n{full_text[:4500]}" ) return call_llm(provider, keys, system, user_msg, []) # ── Suggested questions ─────────────────────────────────────────────────────── def get_suggestions(provider, keys, full_text): system = 'Output ONLY a Python list of 4 strings. No markdown, no explanation, no preamble.' user_msg = f'4 insightful questions for this document. Format exactly: ["Q1?","Q2?","Q3?","Q4?"]\n\n{full_text[:2500]}' raw = call_llm(provider, keys, system, user_msg, []) try: match = re.search(r'\[.*?\]', raw, re.DOTALL) if match: qs = ast.literal_eval(match.group()) if isinstance(qs, list) and len(qs) >= 4: return qs[:4] except Exception: pass return [ "What is the main topic of this document?", "What are the key findings or conclusions?", "What data or evidence is presented?", "What are the recommendations or next steps?", ] # ── Chat export ─────────────────────────────────────────────────────────────── def build_export(history, doc_names): lines = [ "NEXUS AI — CHAT EXPORT", "=" * 50, f"Date: {datetime.now().strftime('%Y-%m-%d %H:%M')}", f"Documents: {', '.join(doc_names)}", "=" * 50, "", ] for i, turn in enumerate(history, 1): lines.append(f"Q{i}: {turn['q']}") lines.append(f"A{i}: {turn['a']}") if turn.get("citations"): cites = ", ".join( f"p.{c['page']} in '{c['doc']}' ({int(c['score']*100)}%)" for c in turn["citations"] ) lines.append(f"Sources: {cites}") lines.append("") return "\n".join(lines) # ── Session state ───────────────────────────────────────────────────────────── _defaults = { "history": [], "all_chunks": [], "all_embeddings": None, "docs": {}, # doc_name -> meta "summaries": {}, # doc_name -> summary text "suggestions": [], "prefill": "", "provider": "Groq", "doc_filter": None, # None = all docs } for k, v in _defaults.items(): if k not in st.session_state: st.session_state[k] = v # ── API keys (env or session) ───────────────────────────────────────────────── _env_groq = os.environ.get("GROQ_API_KEY", "") _env_gemini = os.environ.get("GEMINI_API_KEY", "") if "groq_key" not in st.session_state: st.session_state.groq_key = _env_groq if "gemini_key" not in st.session_state: st.session_state.gemini_key = _env_gemini def keys(): return {"groq": st.session_state.groq_key, "gemini": st.session_state.gemini_key} def active_key_ok(): p = st.session_state.provider k = keys() return bool(k["groq"] if p == "Groq" else k["gemini"]) # ═══════════════════════════════════════════════════════════════════════════════ # SIDEBAR # ═══════════════════════════════════════════════════════════════════════════════ with st.sidebar: # ── Logo ────────────────────────────────────────────────────────────────── st.markdown("""