CoolFace
Apppublic

srenosh/rag-knowledge-base

sourceHugging Faceupdated 7mo agoView on Hugging Face
0likes
app.py454 linesDownload Raw Back to root
1"""2ResolveAI — Enterprise RAG Knowledge Base3Hugging Face Spaces deployment.4"""5 6import os7import streamlit as st8from pathlib import Path9 10# ── Page config ─────────────────────────────────────────────────────────────11st.set_page_config(12    page_title="ResolveAI — Document Intelligence",13    page_icon="🔵",14    layout="wide",15    initial_sidebar_state="expanded",16    menu_items={17        'About': "ResolveAI — Intelligent document processing for federal and enterprise clients."18    }19)20 21# ── Custom CSS ───────────────────────────────────────────────────────────────22st.markdown("""23<style>24/* ── Base ── */25html, body, [class*="css"] {26    font-family: -apple-system, BlinkMacSystemFont, 'Inter', 'Segoe UI', sans-serif;27}28 29/* Hide Streamlit chrome */30#MainMenu, footer, header { visibility: hidden; }31.stDeployButton { display: none; }32[data-testid="stToolbar"] { display: none; }33 34/* ── App background ── */35.stApp {36    background-color: #0a0a0a;37}38 39/* ── Sidebar ── */40[data-testid="stSidebar"] {41    background-color: #0f0f0f;42    border-right: 1px solid rgba(255,255,255,0.06);43}44[data-testid="stSidebar"] * {45    color: rgba(255,255,255,0.75) !important;46}47 48/* ── Sidebar section headers ── */49.sidebar-label {50    font-size: 10px;51    font-weight: 600;52    letter-spacing: 0.18em;53    text-transform: uppercase;54    color: rgba(255,255,255,0.25) !important;55    margin-bottom: 10px;56    margin-top: 4px;57}58 59/* ── File uploader ── */60[data-testid="stFileUploader"] {61    border: 1px dashed rgba(255,255,255,0.1) !important;62    border-radius: 12px !important;63    background: rgba(255,255,255,0.02) !important;64    padding: 8px !important;65}66[data-testid="stFileUploader"]:hover {67    border-color: rgba(96,165,250,0.35) !important;68}69 70/* ── Buttons ── */71.stButton > button {72    background: rgba(255,255,255,0.04) !important;73    border: 1px solid rgba(255,255,255,0.1) !important;74    border-radius: 8px !important;75    color: rgba(255,255,255,0.7) !important;76    font-size: 13px !important;77    font-weight: 500 !important;78    transition: all 0.2s !important;79    height: 40px !important;80}81.stButton > button:hover {82    background: rgba(255,255,255,0.08) !important;83    border-color: rgba(255,255,255,0.2) !important;84    color: white !important;85}86 87/* Primary ingest button */88.stButton.primary > button,89button[kind="primary"] {90    background: #2563eb !important;91    border-color: transparent !important;92    color: white !important;93}94button[kind="primary"]:hover {95    background: #1d4ed8 !important;96}97 98/* ── Chat input ── */99[data-testid="stChatInput"] {100    border-top: 1px solid rgba(255,255,255,0.06) !important;101    background: #0a0a0a !important;102}103[data-testid="stChatInput"] textarea {104    background: rgba(255,255,255,0.04) !important;105    border: 1px solid rgba(255,255,255,0.1) !important;106    border-radius: 12px !important;107    color: white !important;108    font-size: 14px !important;109}110[data-testid="stChatInput"] textarea:focus {111    border-color: rgba(96,165,250,0.5) !important;112    box-shadow: 0 0 0 3px rgba(59,130,246,0.1) !important;113}114 115/* ── Chat messages ── */116[data-testid="stChatMessage"] {117    background: transparent !important;118    border: none !important;119    padding: 8px 0 !important;120}121 122/* ── User message bubble ── */123[data-testid="stChatMessage"][data-type="human"] .stMarkdown p {124    background: rgba(37,99,235,0.12) !important;125    border: 1px solid rgba(37,99,235,0.2) !important;126    border-radius: 12px 12px 4px 12px !important;127    padding: 12px 16px !important;128    display: inline-block !important;129    max-width: 85% !important;130    float: right !important;131    color: rgba(255,255,255,0.85) !important;132    font-size: 14px !important;133    line-height: 1.6 !important;134}135 136/* ── Assistant message ── */137[data-testid="stChatMessage"][data-type="ai"] .stMarkdown {138    color: rgba(255,255,255,0.75) !important;139    font-size: 14px !important;140    line-height: 1.75 !important;141}142 143/* ── Expander (sources) ── */144[data-testid="stExpander"] {145    background: rgba(255,255,255,0.02) !important;146    border: 1px solid rgba(255,255,255,0.06) !important;147    border-radius: 10px !important;148}149[data-testid="stExpander"] summary {150    font-size: 12px !important;151    color: rgba(255,255,255,0.35) !important;152}153 154/* ── Info / success / error ── */155[data-testid="stAlert"] {156    border-radius: 10px !important;157    font-size: 13px !important;158}159 160/* ── Divider ── */161hr {162    border-color: rgba(255,255,255,0.06) !important;163    margin: 16px 0 !important;164}165 166/* ── Scrollbar ── */167::-webkit-scrollbar { width: 4px; }168::-webkit-scrollbar-track { background: transparent; }169::-webkit-scrollbar-thumb { background: rgba(255,255,255,0.1); border-radius: 4px; }170 171/* ── Doc badge ── */172.doc-badge {173    display: inline-flex;174    align-items: center;175    gap: 6px;176    padding: 5px 12px;177    background: rgba(255,255,255,0.03);178    border: 1px solid rgba(255,255,255,0.08);179    border-radius: 100px;180    font-size: 12px;181    color: rgba(255,255,255,0.45);182    margin-bottom: 4px;183    width: 100%;184}185</style>186""", unsafe_allow_html=True)187 188# ── Lazy imports ─────────────────────────────────────────────────────────────189from llama_index.core import VectorStoreIndex, SimpleDirectoryReader, StorageContext190from llama_index.vector_stores.chroma import ChromaVectorStore191from llama_index.embeddings.openai import OpenAIEmbedding192from llama_index.llms.openai import OpenAI193from llama_index.core import Settings194import chromadb195 196# ── Paths ─────────────────────────────────────────────────────────────────────197UPLOADS_DIR = Path("data/uploads")198VECTORSTORE_DIR = Path("data/vectorstore")199UPLOADS_DIR.mkdir(parents=True, exist_ok=True)200VECTORSTORE_DIR.mkdir(parents=True, exist_ok=True)201 202OPENAI_API_KEY = os.environ.get("OPENAI_API_KEY", "")203 204# ── ChromaDB ──────────────────────────────────────────────────────────────────205@st.cache_resource206def get_chroma_client():207    return chromadb.PersistentClient(path=str(VECTORSTORE_DIR))208 209def get_collection():210    return get_chroma_client().get_or_create_collection("knowledge_base")211 212# ── Ingest ────────────────────────────────────────────────────────────────────213def ingest_file(file_path: str) -> dict:214    Settings.embed_model = OpenAIEmbedding(model="text-embedding-3-small", api_key=OPENAI_API_KEY)215    collection = get_collection()216    vector_store = ChromaVectorStore(chroma_collection=collection)217    storage_context = StorageContext.from_defaults(vector_store=vector_store)218    docs = SimpleDirectoryReader(input_files=[file_path]).load_data()219    if not docs:220        return {"status": "error", "message": "Could not parse document."}221    VectorStoreIndex.from_documents(docs, storage_context=storage_context)222    return {"status": "success", "count": len(docs)}223 224def list_documents() -> list:225    try:226        results = get_collection().get(include=["metadatas"])227        names = {m["file_name"] for m in results.get("metadatas", []) if m and "file_name" in m}228        return sorted(names)229    except Exception:230        return []231 232def clear_all():233    client = get_chroma_client()234    client.delete_collection("knowledge_base")235    client.get_or_create_collection("knowledge_base")236    for f in UPLOADS_DIR.iterdir():237        if f.is_file():238            f.unlink()239 240# ── Query ─────────────────────────────────────────────────────────────────────241def ask(question: str) -> dict:242    try:243        Settings.embed_model = OpenAIEmbedding(model="text-embedding-3-small", api_key=OPENAI_API_KEY)244        Settings.llm = OpenAI(245            model="gpt-4o",246            api_key=OPENAI_API_KEY,247            system_prompt=(248                "You are an enterprise knowledge base assistant. "249                "Answer questions strictly based on the provided documents. "250                "If the answer is not in the documents, say so clearly. "251                "Be concise, accurate, and professional. Always cite source documents."252            )253        )254        vector_store = ChromaVectorStore(chroma_collection=get_collection())255        index = VectorStoreIndex.from_vector_store(vector_store)256        engine = index.as_query_engine(similarity_top_k=5, response_mode="compact")257        response = engine.query(question)258 259        sources = []260        if hasattr(response, "source_nodes"):261            seen = set()262            for node in response.source_nodes:263                fname = node.metadata.get("file_name", "Unknown")264                if fname not in seen:265                    sources.append({266                        "file": fname,267                        "score": round(node.score, 3) if node.score else None,268                        "snippet": node.text[:220].strip().replace("\n", " ") + "…"269                    })270                    seen.add(fname)271 272        return {"status": "success", "answer": str(response), "sources": sources}273    except Exception as e:274        return {"status": "error", "answer": f"Error: {str(e)}", "sources": []}275 276# ── Session state ─────────────────────────────────────────────────────────────277if "messages" not in st.session_state:278    st.session_state.messages = []279 280# ═══════════════════════════════════════════════════════════281# SIDEBAR282# ═══════════════════════════════════════════════════════════283with st.sidebar:284    # Brand285    st.markdown("""286    <div style="padding: 4px 0 20px; border-bottom: 1px solid rgba(255,255,255,0.06); margin-bottom: 20px;">287        <div style="font-size: 17px; font-weight: 600; letter-spacing: -0.3px; color: white;">ResolveAI</div>288        <div style="font-size: 11px; color: rgba(255,255,255,0.25); margin-top: 2px; letter-spacing: 0.05em;">Document Intelligence</div>289    </div>290    """, unsafe_allow_html=True)291 292    if not OPENAI_API_KEY:293        st.error("OPENAI_API_KEY not configured.", icon="⚠️")294 295    # Upload296    st.markdown('<div class="sidebar-label">Upload Documents</div>', unsafe_allow_html=True)297    uploaded_files = st.file_uploader(298        "PDF, DOCX, TXT, MD",299        type=["pdf", "docx", "txt", "md"],300        accept_multiple_files=True,301        label_visibility="collapsed",302    )303 304    if uploaded_files:305        if st.button("Ingest Documents", use_container_width=True, type="primary"):306            for uf in uploaded_files:307                dest = UPLOADS_DIR / uf.name308                dest.write_bytes(uf.getvalue())309                with st.spinner(f"Processing {uf.name}…"):310                    result = ingest_file(str(dest))311                if result["status"] == "success":312                    st.success(f"{uf.name} added", icon="✅")313                else:314                    st.error(f"{uf.name}: {result['message']}", icon="❌")315            st.rerun()316 317    st.markdown("<div style='height:16px'></div>", unsafe_allow_html=True)318 319    # Knowledge base320    docs = list_documents()321    st.markdown('<div class="sidebar-label">Knowledge Base</div>', unsafe_allow_html=True)322 323    if docs:324        for doc in docs:325            st.markdown(f'<div class="doc-badge">📄 {doc}</div>', unsafe_allow_html=True)326    else:327        st.markdown(328            '<div style="font-size:13px; color:rgba(255,255,255,0.2); padding: 8px 0;">No documents yet.</div>',329            unsafe_allow_html=True330        )331 332    st.markdown("<div style='height:16px'></div>", unsafe_allow_html=True)333 334    # Actions335    st.markdown('<div class="sidebar-label">Actions</div>', unsafe_allow_html=True)336    col1, col2 = st.columns(2)337    with col1:338        if st.button("Clear Chat", use_container_width=True):339            st.session_state.messages = []340            st.rerun()341    with col2:342        if st.button("Clear All", use_container_width=True):343            clear_all()344            st.session_state.messages = []345            st.rerun()346 347    # Footer348    st.markdown("""349    <div style="position: absolute; bottom: 24px; left: 24px; right: 24px;350                font-size: 11px; color: rgba(255,255,255,0.15); line-height: 1.6;">351        LlamaIndex · GPT-4o · ChromaDB<br>352        Built by <a href="https://linkedin.com/in/renosh-sunny-a69048222"353            style="color:rgba(96,165,250,0.5); text-decoration:none;">Renosh Sunny</a>354    </div>355    """, unsafe_allow_html=True)356 357 358# ═══════════════════════════════════════════════════════════359# MAIN PANEL360# ═══════════════════════════════════════════════════════════361 362# Header363st.markdown("""364<div style="padding: 48px 0 32px; border-bottom: 1px solid rgba(255,255,255,0.05); margin-bottom: 32px;">365    <div style="display:inline-flex; align-items:center; gap:8px; margin-bottom:14px;366                padding: 4px 14px; border-radius:100px;367                border: 1px solid rgba(255,255,255,0.08);368                font-size:11px; color:rgba(255,255,255,0.3); letter-spacing:0.18em; text-transform:uppercase;">369        Enterprise · RAG · AI370    </div>371    <h1 style="font-size: clamp(26px, 4vw, 38px); font-weight: 600; letter-spacing: -1px;372               color: white; margin: 0 0 10px; line-height: 1.1;">373        Document Intelligence374    </h1>375    <p style="font-size: 15px; color: rgba(255,255,255,0.35); font-weight: 300;376              margin: 0; max-width: 520px; line-height: 1.65;">377        Upload any document. Ask questions in plain English.378        The AI answers with cited sources — drawing only from your content.379    </p>380</div>381""", unsafe_allow_html=True)382 383# How to use — visible until the user starts chatting384if not st.session_state.messages:385    with st.expander("📖 How to use this app", expanded=True):386        col1, col2, col3, col4 = st.columns(4)387        with col1:388            st.markdown("#### 1️⃣ Upload")389            st.markdown("Click **Browse files** in the sidebar. Supports **PDF, DOCX, TXT, and Markdown**. You can upload multiple files at once.")390        with col2:391            st.markdown("#### 2️⃣ Ingest")392            st.markdown("Click **Ingest Documents**. The AI reads and indexes your content — takes a few seconds. Files appear under **Knowledge Base** when ready.")393        with col3:394            st.markdown("#### 3️⃣ Ask")395            st.markdown("Type any question in plain English in the chat box below. The AI answers using **only your documents**.")396        with col4:397            st.markdown("#### 4️⃣ Review Sources")398            st.markdown("Every answer has a **📎 Sources** dropdown. Click it to see which file and passage the answer came from.")399 400        st.divider()401        st.markdown("**💡 Example questions to try:**")402        st.markdown("""403- *What are the main topics covered in this document?*404- *Summarize the key findings*405- *What are the requirements for [topic]?*406- *What happens if [condition] occurs?*407- *List all deadlines or dates mentioned*408        """)409        st.caption("⚠️ This AI only answers from documents you upload — it will not search the internet. If the answer isn't in your documents, it will tell you.")410 411    if not docs:412        st.info("👈 Start by uploading a document in the sidebar.")413 414# Chat history415for msg in st.session_state.messages:416    with st.chat_message(msg["role"]):417        st.markdown(msg["content"])418        if msg.get("sources"):419            with st.expander(f"Sources · {len(msg['sources'])} document(s)"):420                for src in msg["sources"]:421                    score_str = f"  ·  Relevance: `{src['score']}`" if src.get("score") else ""422                    st.markdown(f"**{src['file']}**{score_str}")423                    st.caption(src["snippet"])424                    st.divider()425 426# Chat input427if prompt := st.chat_input("Ask anything about your documents…"):428    if not docs:429        st.warning("Upload and ingest at least one document first.", icon="⚠️")430    else:431        st.session_state.messages.append({"role": "user", "content": prompt})432        with st.chat_message("user"):433            st.markdown(prompt)434 435        with st.chat_message("assistant"):436            with st.spinner("Searching knowledge base…"):437                result = ask(prompt)438                answer = result.get("answer", "No answer returned.")439                sources = result.get("sources", [])440                st.markdown(answer)441                if sources:442                    with st.expander(f"Sources · {len(sources)} document(s)"):443                        for src in sources:444                            score_str = f"  ·  Relevance: `{src['score']}`" if src.get("score") else ""445                            st.markdown(f"**{src['file']}**{score_str}")446                            st.caption(src["snippet"])447                            st.divider()448 449        st.session_state.messages.append({450            "role": "assistant",451            "content": answer,452            "sources": sources,453        })454