"""
ui/app.py
Streamlit chat interface for the SmartRAG pipeline.
Features:
- Chat history with session state
- Source document display
- Performance metrics display
- Document ingestion sidebar
- Evaluation dashboard with ablation study results
Run: streamlit run ui/app.py
"""
import json
import time
from pathlib import Path
import pandas as pd
import requests
import streamlit as st
import plotly.express as px
import plotly.graph_objects as go
# ─── Page Config ─────────────────────────────────────────────────
st.set_page_config(
page_title="SmartRAG",
page_icon="🧠",
layout="wide",
initial_sidebar_state="expanded",
)
API_BASE = "http://localhost:8000"
# ─── Custom CSS ───────────────────────────────────────────────────
st.markdown("""
""", unsafe_allow_html=True)
# ─── Session State ────────────────────────────────────────────────
if "messages" not in st.session_state:
st.session_state.messages = []
if "total_queries" not in st.session_state:
st.session_state.total_queries = 0
if "avg_latency" not in st.session_state:
st.session_state.avg_latency = 0.0
# ─── Header ───────────────────────────────────────────────────────
col1, col2 = st.columns([4, 1])
with col1:
st.title("🧠 SmartRAG")
st.caption("Fine-tuned LLM + Retrieval-Augmented Generation")
# ─── Tabs ─────────────────────────────────────────────────────────
tab_chat, tab_eval = st.tabs(["💬 Chat", "📊 Evaluation"])
# ─── Sidebar ──────────────────────────────────────────────────────
with st.sidebar:
st.markdown('
', unsafe_allow_html=True)
top_k = st.slider("Chunks to retrieve (top-k)", 1, 10, 4)
st.divider()
# Health check
st.header("📡 System Status")
try:
resp = requests.get(f"{API_BASE}/health", timeout=3)
health = resp.json()
st.success("API Online" if health["status"] == "healthy" else "API Degraded")
st.write(f"Model: {'✅' if health['model_loaded'] else '❌'}")
st.write(f"VectorStore: {'✅' if health['vectorstore_loaded'] else '❌'}")
except Exception:
st.error("API Offline — start with: `uvicorn api.app:app`")
st.divider()
# Document ingestion
st.header("📄 Add Documents")
uploaded = st.file_uploader("Upload .txt file", type=["txt"])
if uploaded and st.button("Ingest Document"):
text = uploaded.read().decode("utf-8")
with st.spinner("Ingesting..."):
try:
resp = requests.post(
f"{API_BASE}/ingest",
json={"texts": [text], "metadata": [{"filename": uploaded.name}]},
)
st.success(f"✅ Ingested: {uploaded.name}")
except Exception as e:
st.error(f"Ingestion failed: {e}")
if st.button("🗑️ Clear Chat"):
st.session_state.messages = []
st.rerun()
# ─── Chat Tab ─────────────────────────────────────────────────────
with tab_chat:
st.markdown('', unsafe_allow_html=True)
# Chat Interface
for msg in st.session_state.messages:
with st.chat_message(msg["role"]):
st.write(msg["content"])
if msg.get("sources"):
with st.expander(f"📎 {len(msg['sources'])} source(s) used"):
for src in msg["sources"]:
st.markdown(f'
{src}
', unsafe_allow_html=True)
if msg.get("latency_ms"):
st.caption(f"⚡ {msg['latency_ms']:.0f}ms · {msg.get('num_chunks', 0)} chunks retrieved")
# Input
if prompt := st.chat_input("Ask anything about your documents..."):
# Show user message
st.session_state.messages.append({"role": "user", "content": prompt})
with st.chat_message("user"):
st.write(prompt)
# Get response
with st.chat_message("assistant"):
with st.spinner("Thinking..."):
try:
resp = requests.post(
f"{API_BASE}/query",
json={"question": prompt, "top_k": top_k},
timeout=60,
)
data = resp.json()
answer = data.get("answer", "No answer returned.")
sources = data.get("sources", [])
latency = data.get("latency_ms", 0)
num_chunks = data.get("num_chunks_retrieved", 0)
st.write(answer)
if sources:
with st.expander(f"📎 {len(sources)} source(s) used"):
for src in sources:
st.markdown(f'
{src}
', unsafe_allow_html=True)
st.caption(f"⚡ {latency:.0f}ms · {num_chunks} chunks retrieved")
# Update session state
st.session_state.messages.append({
"role": "assistant",
"content": answer,
"sources": sources,
"latency_ms": latency,
"num_chunks": num_chunks,
})
st.session_state.total_queries += 1
# Update running avg latency
n = st.session_state.total_queries
st.session_state.avg_latency = (
(st.session_state.avg_latency * (n - 1) + latency) / n
)
except requests.exceptions.ConnectionError:
st.error("❌ Cannot connect to API. Run: `uvicorn api.app:app --port 8000`")
except Exception as e:
st.error(f"Error: {e}")
# Session Stats
st.divider()
col1, col2, col3 = st.columns([2, 2, 1])
with col1:
st.metric("Total Queries", st.session_state.total_queries)
with col2:
st.metric("Avg Latency", f"{st.session_state.avg_latency:.0f} ms")
with col3:
if st.button("🗑️ Clear Chat"):
st.session_state.messages = []
st.rerun()
st.markdown('
', unsafe_allow_html=True)
# ─── Evaluation Tab ───────────────────────────────────────────────
with tab_eval:
st.markdown('', unsafe_allow_html=True)
st.header("🔬 Ablation Study Results")
st.caption("Comparing different pipeline configurations")
# Load evaluation data
try:
# Load summary
with open(Path(__file__).parent.parent / "artifacts" / "eval_results" / "ablation_summary.json", "r") as f:
summary = json.load(f)
# Load detailed results
df = pd.read_csv(Path(__file__).parent.parent / "artifacts" / "eval_results" / "ablation_results.csv")
# System names mapping
system_names = {
"A_base_dense": "Base Model + Dense RAG",
"B_finetuned_dense": "Fine-Tuned + Dense RAG",
"C_finetuned_hybrid": "Fine-Tuned + Hybrid Search",
"D_full_pipeline": "Fine-Tuned + Hybrid + Reranking"
}
# Metrics overview
st.subheader("📈 Performance Metrics")
cols = st.columns(4)
for i, (key, data) in enumerate(summary.items()):
with cols[i]:
st.markdown(f"""
{system_names.get(key, key)}
{data['avg_faithfulness']:.2f}
Faithfulness
{data['avg_keyword_coverage']:.2f}
Keyword Coverage
{data['avg_latency_ms']:.0f}ms
Avg Latency
{data['failure_rate']:.1%}
Failure Rate
""", unsafe_allow_html=True)
st.divider()
# Charts
st.subheader("📊 Comparative Analysis")
# Faithfulness chart
fig_faith = px.bar(
x=[system_names[k] for k in summary.keys()],
y=[v['avg_faithfulness'] for v in summary.values()],
title="Faithfulness Score by Pipeline",
labels={'x': 'Pipeline', 'y': 'Faithfulness'},
color=[v['avg_faithfulness'] for v in summary.values()],
color_continuous_scale='Viridis'
)
fig_faith.update_layout(
plot_bgcolor='#0e1117',
paper_bgcolor='#0e1117',
font_color='#ffffff'
)
st.plotly_chart(fig_faith, use_container_width=True)
# Latency chart
fig_lat = px.bar(
x=[system_names[k] for k in summary.keys()],
y=[v['avg_latency_ms'] for v in summary.values()],
title="Average Latency by Pipeline",
labels={'x': 'Pipeline', 'y': 'Latency (ms)'},
color=[v['avg_latency_ms'] for v in summary.values()],
color_continuous_scale='Plasma'
)
fig_lat.update_layout(
plot_bgcolor='#0e1117',
paper_bgcolor='#0e1117',
font_color='#ffffff'
)
st.plotly_chart(fig_lat, use_container_width=True)
# Detailed results table
st.subheader("📋 Detailed Results")
st.dataframe(df, use_container_width=True)
# Failure analysis
st.subheader("❌ Failure Analysis")
failure_df = df[df['failure'] != 'none']
if not failure_df.empty:
st.dataframe(failure_df[['system', 'question', 'failure']], use_container_width=True)
else:
st.success("No failures detected in the evaluation set!")
except FileNotFoundError:
st.warning("Evaluation results not found. Run the ablation study first: `python -m evaluation.ablation`")
except Exception as e:
st.error(f"Error loading evaluation data: {e}")
st.markdown('
', unsafe_allow_html=True)