"""Professional Streamlit UI for the FastAPI Codebase Q&A RAG system.
Run locally:
streamlit run app.py
Required environment variable:
GROQ_API_KEY
On Hugging Face Spaces, add GROQ_API_KEY under:
Settings -> Variables and secrets
"""
import os
import sys
import time
from pathlib import Path
from textwrap import dedent
from typing import Iterator
from dotenv import load_dotenv
load_dotenv(override=True)
ROOT_DIR = Path(__file__).resolve().parent
sys.path.insert(0, str(ROOT_DIR / "src"))
import streamlit as st
from ask import SYSTEM_PROMPT, build_prompt
from retrieval import retrieve
# ---------------------------------------------------------------------
# Configuration
# ---------------------------------------------------------------------
LLM_MODEL = os.environ.get("GROQ_MODEL", "openai/gpt-oss-120b")
GITHUB_URL = "https://github.com/islam-mamedov/codebase-rag"
SPACE_URL = (
"https://huggingface.co/spaces/"
"islam-mamedov/fastapi-codebase-qa"
)
EXAMPLES = [
{
"title": "Custom status codes",
"question": "How do I return a custom status code from an endpoint?",
"icon": "↗",
},
{
"title": "Dependency injection",
"question": "How does FastAPI's dependency injection system work?",
"icon": "◫",
},
{
"title": "Find a class",
"question": "Where is the APIRouter class defined?",
"icon": "⌘",
},
{
"title": "Test refusal",
"question": "How do I connect FastAPI to MongoDB?",
"icon": "◇",
},
]
MODE_LABELS = {
"dense": "Dense retrieval",
"dense_rw": "Dense + query rewriting",
"hybrid": "Hybrid dense + BM25",
}
MODE_DESCRIPTIONS = {
"dense": (
"Recommended. Best evaluation result with "
"0.91 recall@5 and 0.71 MRR."
),
"dense_rw": (
"Uses an LLM to expand the search query before retrieval. "
"It matched dense recall but slightly reduced ranking quality."
),
"hybrid": (
"Combines dense retrieval with BM25 keyword search. "
"Included for comparison with the selected dense pipeline."
),
}
SOURCE_ICONS = {
"code": "⌘",
"doc": "▤",
"issue": "◉",
}
# ---------------------------------------------------------------------
# Page setup
# ---------------------------------------------------------------------
st.set_page_config(
page_title="FastAPI Codebase Q&A",
page_icon="⚡",
layout="wide",
initial_sidebar_state="expanded",
)
def render_html(content: str) -> None:
"""Render trusted static HTML without Markdown parsing."""
st.html(dedent(content).strip())
# ---------------------------------------------------------------------
# Styling
# ---------------------------------------------------------------------
render_html(
"""
"""
)
# ---------------------------------------------------------------------
# Index initialization
# ---------------------------------------------------------------------
@st.cache_resource(
show_spinner=(
"Preparing the search index. "
"The first startup can take approximately three minutes."
)
)
def ensure_index() -> bool:
"""Build the Chroma index when it does not already exist."""
import chromadb
index_path = ROOT_DIR / "data" / "chroma"
client = chromadb.PersistentClient(path=str(index_path))
try:
collection = client.get_collection("chunks")
if collection.count() > 0:
return True
except Exception:
pass
import index
index.main()
return True
ensure_index()
# ---------------------------------------------------------------------
# Sidebar
# ---------------------------------------------------------------------
with st.sidebar:
st.markdown("## ⚡ Codebase Q&A")
st.caption(
"Evaluation-driven retrieval over FastAPI source code, "
"documentation and resolved issues."
)
st.divider()
st.markdown("### Retrieval")
mode = st.selectbox(
"Search strategy",
options=["dense", "dense_rw", "hybrid"],
format_func=lambda value: MODE_LABELS[value],
help=(
"Dense retrieval achieved the strongest overall "
"evaluation result."
),
)
st.caption(MODE_DESCRIPTIONS[mode])
st.divider()
st.markdown("### Evaluation snapshot")
st.caption("42-question hand-labelled benchmark")
metric_col_1, metric_col_2 = st.columns(2)
metric_col_1.metric("Recall@5", "0.91")
metric_col_2.metric("MRR", "0.71")
metric_col_3, metric_col_4 = st.columns(2)
metric_col_3.metric("Correctness", "0.91")
metric_col_4.metric("Refusals", "7 / 7")
st.caption(
"Every push runs unit tests and fails CI when "
"recall@5 falls below 0.90."
)
st.divider()
st.markdown("### Project")
st.link_button(
"⌘ View source code",
GITHUB_URL,
use_container_width=True,
)
st.link_button(
"↗ Open live deployment",
SPACE_URL,
use_container_width=True,
)
st.divider()
if st.button(
"Clear conversation",
use_container_width=True,
):
st.session_state.messages = []
st.rerun()
# ---------------------------------------------------------------------
# Hero
# ---------------------------------------------------------------------
render_html(
"""
Live evaluation-driven RAG system
Understand FastAPI through its actual codebase.
Ask technical questions and receive answers grounded in FastAPI
source code, documentation and resolved GitHub issues. Each
response includes retrieved evidence, while unsupported questions
are refused instead of guessed.
1,352 indexed chunks
AST-aware code retrieval
Source-linked answers
CI evaluation gate
"""
)
# ---------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------
@st.cache_data(show_spinner=False)
def cached_retrieve(
question: str,
retrieval_mode: str,
) -> list[dict]:
"""Retrieve the top five chunks for a question."""
return retrieve(
question,
k=5,
mode=retrieval_mode,
)
def stream_answer(
question: str,
hits: list[dict],
) -> Iterator[str]:
"""Stream a grounded answer from Groq."""
from groq import Groq
api_key = os.environ.get("GROQ_API_KEY")
if not api_key:
st.error(
"The GROQ_API_KEY environment variable is not configured."
)
st.stop()
client = Groq(api_key=api_key)
try:
stream = client.chat.completions.create(
model=LLM_MODEL,
messages=[
{
"role": "system",
"content": SYSTEM_PROMPT,
},
{
"role": "user",
"content": build_prompt(question, hits),
},
],
temperature=0.1,
stream=True,
)
for chunk in stream:
delta = chunk.choices[0].delta.content
if delta:
yield delta
except Exception as error:
st.error(
"The answer service encountered an error. "
"Please try the question again."
)
yield (
"\n\nThe answer could not be generated because the "
"language-model request failed."
)
print(f"[Groq error] {error}")
def render_sources(hits: list[dict]) -> None:
"""Render retrieved evidence below an answer."""
render_html(
f"""
Retrieved evidence
{len(hits)} sources
"""
)
for index_number, hit in enumerate(hits, start=1):
metadata = hit.get("meta", {})
source_type = metadata.get("source_type", "doc")
source_icon = SOURCE_ICONS.get(source_type, "▤")
source_path = metadata.get("path", "Unknown source")
source_symbol = metadata.get("symbol", "")
source_url = metadata.get("url", "")
source_score = hit.get("score")
label = f"{source_icon} {index_number}. {source_path}"
if source_symbol:
label += f" · {source_symbol}"
with st.expander(label):
detail_col_1, detail_col_2 = st.columns([3, 1])
with detail_col_1:
st.caption(
f"Source type: {source_type.capitalize()}"
)
with detail_col_2:
if source_score is not None:
st.caption(
f"Similarity: {source_score:.3f}"
)
if source_url:
st.markdown(
f"[Open original source on GitHub ↗]"
f"({source_url})"
)
code_language = (
"python"
if source_type == "code"
else "text"
)
source_text = hit.get("text", "")
st.code(
source_text[:1600],
language=code_language,
wrap_lines=True,
)
# ---------------------------------------------------------------------
# Conversation state
# ---------------------------------------------------------------------
if "messages" not in st.session_state:
st.session_state.messages = []
# ---------------------------------------------------------------------
# Existing conversation
# ---------------------------------------------------------------------
for message in st.session_state.messages:
with st.chat_message(message["role"]):
st.markdown(message["content"])
if message.get("timing"):
st.caption(message["timing"])
if message.get("sources"):
render_sources(message["sources"])
# ---------------------------------------------------------------------
# Empty state
# ---------------------------------------------------------------------
pending_question = None
if not st.session_state.messages:
render_html(
"""
Start exploring
Ask a question or try an example
"""
)
render_html(
"""
The assistant searches the indexed FastAPI corpus before
answering. Questions about unrelated integrations should
produce an honest refusal.
"""
)
first_row = st.columns(2)
second_row = st.columns(2)
example_columns = [
first_row[0],
first_row[1],
second_row[0],
second_row[1],
]
for column, example in zip(
example_columns,
EXAMPLES,
):
with column:
button_label = (
f"{example['icon']} {example['title']}\n\n"
f"{example['question']}"
)
if st.button(
button_label,
key=f"example-{example['title']}",
use_container_width=True,
):
pending_question = example["question"]
# ---------------------------------------------------------------------
# Chat input
# ---------------------------------------------------------------------
typed_prompt = st.chat_input(
"Ask about FastAPI's code, documentation or behaviour..."
)
prompt = typed_prompt or pending_question
# ---------------------------------------------------------------------
# Generate response
# ---------------------------------------------------------------------
if prompt:
st.session_state.messages.append(
{
"role": "user",
"content": prompt,
}
)
with st.chat_message("user"):
st.markdown(prompt)
with st.chat_message("assistant"):
retrieval_started = time.perf_counter()
with st.spinner(
"Searching 1,352 code, documentation and issue chunks..."
):
hits = cached_retrieve(
prompt,
mode,
)
retrieval_seconds = (
time.perf_counter() - retrieval_started
)
generation_started = time.perf_counter()
answer = st.write_stream(
stream_answer(
prompt,
hits,
)
)
generation_seconds = (
time.perf_counter() - generation_started
)
timing = (
f"Retrieved in {retrieval_seconds:.2f}s"
f" · Generated in {generation_seconds:.1f}s"
f" · Strategy: {MODE_LABELS[mode]}"
)
st.caption(timing)
render_sources(hits)
st.session_state.messages.append(
{
"role": "assistant",
"content": answer,
"sources": hits,
"timing": timing,
}
)
# ---------------------------------------------------------------------
# Footer
# ---------------------------------------------------------------------
render_html(
"""
"""
)