"""
frontend/app.py
---------------
Streamlit UI for the Nepali Document RAG system.
Talks to the FastAPI backend at BACKEND_URL.
"""
import os
import json
import requests
import streamlit as st
# ── Config ────────────────────────────────────────────────────────────────────
BACKEND_URL = os.getenv("BACKEND_URL", "http://localhost:8080")
st.set_page_config(
page_title="Nepali Document Search",
page_icon="🔍",
layout="centered",
initial_sidebar_state="collapsed",
)
# ── CSS ─────────────────────────────────────────────────────
st.markdown(
"""
""",
unsafe_allow_html=True,
)
# ── Session State ──────────────────────────────────────────────────────────────
if "messages" not in st.session_state:
st.session_state.messages = []
# ── Sidebar ───────────────────────────────────────────────────────────────────
with st.sidebar:
st.markdown("
", unsafe_allow_html=True)
st.subheader("Configuration")
st.markdown("", unsafe_allow_html=True)
top_k_retrieval = st.number_input(
"Documents to retrieve",
min_value=5,
max_value=50,
value=20,
step=5,
help="Number of candidate documents fetched from the vector store.",
key="config_top_k_retrieval",
)
top_k_context = st.number_input(
"Documents to use in context",
min_value=1,
max_value=10,
value=5,
help="Top-ranked documents passed to the language model.",
key="config_top_k_context",
)
st.markdown("", unsafe_allow_html=True)
use_streaming = st.checkbox("Stream response", value=True, key="config_streaming")
st.markdown("---")
st.markdown(
"Nepali Document RAG by Anup Aryal
",
unsafe_allow_html=True,
)
# ── Page Header ───────────────────────────────────────────────────────────────
st.markdown("", unsafe_allow_html=True)
st.markdown(
"""
Retrieval-Augmented Generation
Nepali Document Search
Ask questions about your documents in Nepali or English
""",
unsafe_allow_html=True,
)
# ── Conversation History ──────────────────────────────────────────────────────
for msg in st.session_state.messages:
with st.chat_message(msg["role"]):
if msg["role"] == "user":
st.markdown(msg["content"])
else:
st.markdown(
f'{msg["content"]}
',
unsafe_allow_html=True,
)
if msg.get("metrics"):
m = msg["metrics"]
st.caption(
f"⏱ Retrieval {m['retrieval']} ms · Generation {m['generation']} ms · Total {m['total']} ms"
)
if msg.get("sources"):
with st.expander(
f"📄 {len(msg['sources'])} source(s) referenced", expanded=False
):
for i, src in enumerate(msg["sources"]):
st.markdown(
f"""
""",
unsafe_allow_html=True,
)
# ── Chat Input ────────────────────────────────────────────────────────────────
if prompt := st.chat_input("Ask a question about your documents…"):
with st.chat_message("user"):
st.markdown(prompt)
st.session_state.messages.append({"role": "user", "content": prompt})
payload = {
"query": prompt.strip(),
"top_k_retrieval": st.session_state.config_top_k_retrieval,
"top_k_context": st.session_state.config_top_k_context,
}
with st.chat_message("assistant"):
answer_placeholder = st.empty()
metrics_placeholder = st.empty()
sources_placeholder = st.empty()
full_answer = ""
sources = []
metrics_data = None
if st.session_state.config_streaming:
# ── Streaming mode ──
try:
with requests.post(
f"{BACKEND_URL}/query/stream",
json=payload,
stream=True,
timeout=120,
) as resp:
if resp.status_code != 200:
st.error(f"Backend error {resp.status_code}: {resp.text}")
else:
for line in resp.iter_lines():
if not line:
continue
text = line.decode("utf-8")
if not text.startswith("data: "):
continue
content = text[6:]
if content == "[DONE]":
break
elif content.startswith("[ERROR]"):
st.error(f"Error: {content[7:]}")
break
elif content.startswith("[SOURCES]"):
try:
sources = json.loads(content[9:])
except ValueError:
pass
else:
decoded_content = content.replace("\\n", "\n")
full_answer += decoded_content
answer_placeholder.markdown(
f'{full_answer}▌
',
unsafe_allow_html=True,
)
answer_placeholder.markdown(
f'{full_answer}
',
unsafe_allow_html=True,
)
except requests.exceptions.ConnectionError:
st.error(
"Unable to reach the backend service. Please check that it is running."
)
else:
# ── Non-streaming mode ──
with st.spinner("Searching and generating response…"):
try:
resp = requests.post(
f"{BACKEND_URL}/query", json=payload, timeout=120
)
if resp.status_code != 200:
st.error(
f"Backend error {resp.status_code}: {resp.json().get('detail', resp.text)}"
)
else:
data = resp.json()
full_answer = data["answer"]
sources = data.get("sources", [])
metrics_data = {
"retrieval": data["retrieval_time_ms"],
"generation": data["generation_time_ms"],
"total": data["total_time_ms"],
}
answer_placeholder.markdown(
f'{full_answer}
',
unsafe_allow_html=True,
)
except requests.exceptions.ConnectionError:
st.error(
"Unable to reach the backend service. Please check that it is running."
)
if metrics_data:
metrics_placeholder.caption(
f"⏱ Retrieval {metrics_data['retrieval']} ms · Generation {metrics_data['generation']} ms · Total {metrics_data['total']} ms"
)
if sources:
with sources_placeholder.expander(
f"📄 {len(sources)} source(s) referenced", expanded=False
):
for i, src in enumerate(sources):
st.markdown(
f"""
""",
unsafe_allow_html=True,
)
st.session_state.messages.append(
{
"role": "assistant",
"content": full_answer,
"sources": sources,
"metrics": metrics_data,
}
)