File size: 9,147 Bytes
e468671
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
"""RAG-Engine: Interactive Streamlit Showcase.

Demonstrates how a Retrieval-Augmented Generation (RAG) system works step-by-step:
1. Ingestion & Document Chunking
2. Dense Vector Embeddings & Similarity Search
3. Sparse BM25 Keyword Search
4. Hybrid Retrieval with Reciprocal Rank Fusion (RRF)
5. Context-Augmented Prompting & Answer Generation
"""

import sys
from pathlib import Path

ROOT = Path(__file__).resolve().parent
sys.path.insert(0, str(ROOT / "src"))
sys.path.insert(0, str(ROOT))

import streamlit as st
from config.settings import get_settings
from rag_engine.pipeline import RAGPipeline

st.set_page_config(
    page_title="RAG Engine Showcase",
    page_icon="🧠",
    layout="wide",
)


@st.cache_resource
def get_pipeline():
    """Initialize and cache the RAGPipeline instance."""
    pipeline = RAGPipeline()
    pipeline.load_demo_corpus()
    return pipeline


def main():
    settings = get_settings()
    pipeline = get_pipeline()

    # Sidebar Navigation & System Status
    st.sidebar.title("🧠 RAG Engine")
    st.sidebar.caption("Domain-Agnostic Retrieval-Augmented Generation")

    stats = pipeline.get_stats()
    st.sidebar.markdown("---")
    st.sidebar.markdown(f"**Knowledge Base Stats:**")
    st.sidebar.markdown(f"- πŸ“„ Documents: `{stats['document_count']}`")
    st.sidebar.markdown(f"- 🧩 Chunks: `{stats['chunk_count']}`")
    st.sidebar.markdown(f"- ⚑ Vector Backend: `{'HashEmbeddings (Offline)' if settings.use_hash_embeddings else settings.embedding_model}`")
    st.sidebar.markdown(f"- πŸ€– LLM Mode: `{'OpenAI (' + settings.user_llm_model + ')' if settings.has_llm_key() else 'Extractive Fallback (Offline)'}`")
    st.sidebar.markdown("---")

    page = st.sidebar.radio(
        "Navigation",
        ["πŸ’¬ Ask Question", "πŸ”¬ Pipeline Inspector", "πŸ“š Knowledge Base"],
    )

    if page == "πŸ’¬ Ask Question":
        render_ask_page(pipeline, settings)
    elif page == "πŸ”¬ Pipeline Inspector":
        render_inspector_page(pipeline)
    elif page == "πŸ“š Knowledge Base":
        render_knowledge_base_page(pipeline)


def render_ask_page(pipeline, settings):
    st.header("πŸ’¬ Ask Questions Grounded in Knowledge")
    st.write(
        "Enter a question to retrieve relevant passages from the knowledge base and generate an answer."
    )

    # Sample query buttons for quick demonstration
    st.markdown("**Try a sample question:**")
    cols = st.columns(3)
    sample_q = None
    if cols[0].button("Quantum: Cooling temperature?"):
        sample_q = "What temperature must superconducting qubits be cooled down to?"
    if cols[1].button("JWST: Primary mirror materials?"):
        sample_q = "What is the primary mirror of the James Webb Space Telescope made of?"
    if cols[2].button("Energy: Long-duration storage?"):
        sample_q = "What technologies are used for long-duration energy storage exceeding 8 hours?"

    query = st.text_input(
        "Your Question:",
        value=sample_q if sample_q else "",
        placeholder="e.g. How does quantum superposition enable qubits to solve complex problems?",
    )

    if st.button("Ask RAG Engine", type="primary") or query:
        if not query.strip():
            st.warning("Please enter a question.")
            return

        with st.spinner("Retrieving relevant context and generating answer..."):
            result = pipeline.query(query)

        st.subheader("πŸ’‘ Answer")
        st.markdown(result["answer"])

        # Metadata badges
        st.markdown("---")
        m_col1, m_col2, m_col3, m_col4 = st.columns(4)
        m_col1.metric("⏱ Latency", f"{result['latency_ms']} ms")
        m_col2.metric("🎯 Faithfulness", f"{result['evaluation']['faithfulness'] * 100:.1f}%")
        m_col3.metric("❓ Relevancy", f"{result['evaluation']['answer_relevancy'] * 100:.1f}%")
        m_col4.metric("πŸ“š Sources Cited", len(result["sources"]))

        if result["sources"]:
            st.markdown(f"**Referenced Documents:** {', '.join([f'`{s}`' for s in result['sources']])}")

        # Show retrieved passages in an expander
        with st.expander("πŸ“– View Retrieved Passages (Top Ranked Chunks)"):
            for idx, item in enumerate(result["retrieved_chunks"], start=1):
                chunk = item["chunk"]
                st.markdown(f"**Passage {idx}** | Source: *{chunk.title}* | Hybrid Score: `{item['score']}`")
                st.info(chunk.text)


def render_inspector_page(pipeline):
    st.header("πŸ”¬ RAG Pipeline Inspector")
    st.write(
        "See behind the scenes: watch how the query flows through **Vector Search**, **BM25 Keyword Search**, "
        "and **Reciprocal Rank Fusion (RRF)** before reaching the LLM."
    )

    query = st.text_input(
        "Test Query to Inspect:",
        value="superconducting circuits cooling dilution refrigerator",
    )

    if query.strip():
        result = pipeline.query(query)

        col1, col2 = st.columns(2)

        with col1:
            st.subheader("1️⃣ Dense Vector Search (Top Hits)")
            st.caption("Captures semantic similarity using vector embeddings.")
            dense_hits = result["dense_results"]
            if dense_hits:
                for idx, h in enumerate(dense_hits[:5], start=1):
                    chunk = h["chunk"]
                    st.markdown(f"**Rank {idx}** (Cosine Sim: `{h['score']}`) - *{chunk.title}*")
                    st.text(chunk.text[:200] + "...")
            else:
                st.info("No vector hits.")

        with col2:
            st.subheader("2️⃣ Sparse BM25 Search (Top Hits)")
            st.caption("Captures exact lexical matches and keyword occurrences.")
            bm25_hits = result["bm25_results"]
            if bm25_hits:
                for idx, h in enumerate(bm25_hits[:5], start=1):
                    chunk = h["chunk"]
                    st.markdown(f"**Rank {idx}** (BM25 Score: `{h['score']}`) - *{chunk.title}*")
                    st.text(chunk.text[:200] + "...")
            else:
                st.info("No BM25 hits.")

        st.subheader("3️⃣ Hybrid Reciprocal Rank Fusion (RRF) Merge")
        st.caption("Combines dense and sparse rankings using `RRF = sum(weight / (60 + rank))`")

        fused_items = result["retrieved_chunks"]
        for idx, item in enumerate(fused_items, start=1):
            chunk = item["chunk"]
            d_rank = item.get("dense_rank", "N/A")
            b_rank = item.get("bm25_rank", "N/A")
            st.markdown(
                f"**Final Rank {idx}** | Fused Score: `{item['score']}` | "
                f"Vector Rank: `{d_rank}` | BM25 Rank: `{b_rank}` | Source: *{chunk.title}*"
            )
            st.success(chunk.text)


def render_knowledge_base_page(pipeline):
    st.header("πŸ“š Knowledge Base Manager")
    st.write("Add documents to your RAG system via plain text, paste, or PDF upload.")

    tab1, tab2, tab3 = st.tabs(["πŸ“„ Existing Documents", "✍️ Add Text Document", "πŸ“Ž Upload PDF"])

    with tab1:
        st.subheader("Indexed Documents")
        if not pipeline.documents:
            st.info("No documents currently indexed. Click 'Reload Demo Corpus' below to populate.")
        else:
            for idx, doc in enumerate(pipeline.documents, start=1):
                with st.expander(f"{idx}. {doc.title} (ID: {doc.doc_id})"):
                    st.caption(f"Source: {doc.source} | Words: {len(doc.text.split())}")
                    st.text(doc.text[:500] + ("..." if len(doc.text) > 500 else ""))

        if st.button("πŸ”„ Reload Demo Corpus"):
            docs = pipeline.load_demo_corpus()
            st.success(f"Loaded and indexed {len(docs)} demo documents!")
            st.rerun()

    with tab2:
        st.subheader("Paste Document Content")
        new_title = st.text_input("Document Title", placeholder="e.g. Introduction to Renewable Energy")
        new_text = st.text_area("Document Text", placeholder="Paste the text of the article or notes here...", height=200)

        if st.button("Index Document", type="primary"):
            if not new_text.strip():
                st.warning("Please provide text content.")
            else:
                title = new_title.strip() or "Untitled Document"
                doc = pipeline.add_document(text=new_text, title=title)
                st.success(f"Successfully indexed document: '{doc.title}' into RAG knowledge base!")
                st.rerun()

    with tab3:
        st.subheader("Upload PDF Document")
        uploaded_file = st.file_uploader("Choose a PDF file", type=["pdf"])
        if uploaded_file is not None:
            if st.button("Process & Index PDF", type="primary"):
                import tempfile
                with tempfile.NamedTemporaryFile(delete=False, suffix=".pdf") as tmp:
                    tmp.write(uploaded_file.read())
                    tmp_path = Path(tmp.name)

                doc = pipeline.add_pdf(tmp_path)
                st.success(f"Extracted and indexed PDF: '{doc.title}' ({len(doc.text.split())} words)!")
                st.rerun()


if __name__ == "__main__":
    main()