Islam Mamedov commited on
Commit
cc98906
·
1 Parent(s): 80a78e0

Day 8: Streamlit UI + dotenv config

Browse files
Files changed (5) hide show
  1. .gitignore +1 -0
  2. app.py +111 -0
  3. requirements.txt +5 -0
  4. src/ask.py +2 -6
  5. src/eval.py +2 -0
.gitignore CHANGED
@@ -4,3 +4,4 @@ data/issues/
4
  data/chroma/
5
  __pycache__/
6
  .pytest_cache/
 
 
4
  data/chroma/
5
  __pycache__/
6
  .pytest_cache/
7
+ .env
app.py ADDED
@@ -0,0 +1,111 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Streamlit UI for the FastAPI Codebase Q&A RAG system.
2
+
3
+ Run locally:
4
+ export GROQ_API_KEY=gsk_...
5
+ streamlit run app.py
6
+
7
+ On Hugging Face Spaces, set GROQ_API_KEY as a Space secret.
8
+ """
9
+
10
+ import os
11
+ import sys
12
+ from dotenv import load_dotenv
13
+ load_dotenv(override=True)
14
+ from pathlib import Path
15
+
16
+ sys.path.insert(0, str(Path(__file__).parent / "src"))
17
+
18
+ import streamlit as st
19
+
20
+ from ask import SYSTEM_PROMPT, build_prompt
21
+ from retrieval import retrieve
22
+
23
+ LLM_MODEL = os.environ.get("GROQ_MODEL", "openai/gpt-oss-120b")
24
+
25
+ EXAMPLES = [
26
+ "How do I return a custom status code from an endpoint?",
27
+ "How does dependency injection work?",
28
+ "Where is the APIRouter class defined?",
29
+ "How do I connect FastAPI to MongoDB?", # tests honest refusal
30
+ ]
31
+
32
+ st.set_page_config(page_title="FastAPI Codebase Q&A", page_icon="⚡",
33
+ layout="wide")
34
+ st.title("⚡ FastAPI Codebase Q&A")
35
+ st.caption("Retrieval-augmented answers over FastAPI's source code, docs, "
36
+ "and GitHub issues — every claim cited, honest refusals when "
37
+ "the corpus doesn't know.")
38
+
39
+ with st.sidebar:
40
+ st.header("How it works")
41
+ st.markdown(
42
+ "1. Your question is embedded (`bge-small-en-v1.5`)\n"
43
+ "2. Top-5 chunks retrieved from ~1,350 AST-aware chunks "
44
+ "(code, docs, issues)\n"
45
+ "3. An LLM answers **only** from those chunks\n"
46
+ "4. Citations link to the exact GitHub lines"
47
+ )
48
+ mode = st.selectbox("Retrieval mode", ["dense", "dense_rw", "hybrid"],
49
+ help="dense won the ablation; others shown for "
50
+ "comparison")
51
+ st.divider()
52
+ st.markdown(
53
+ "**Eval results (42-question set)**\n\n"
54
+ "recall@5 **0.91** · MRR **0.71**\n\n"
55
+ "faithful **0.89** · correct **0.91** · refusal **7/7**"
56
+ )
57
+ # TODO: replace with your repo URL
58
+ st.markdown("[Source & write-up](https://github.com/YOUR_USERNAME/codebase-rag)")
59
+
60
+
61
+ @st.cache_data(show_spinner=False)
62
+ def cached_retrieve(question: str, mode: str) -> list[dict]:
63
+ return retrieve(question, k=5, mode=mode)
64
+
65
+
66
+ def generate(question: str, hits: list[dict]) -> str:
67
+ from groq import Groq
68
+ api_key = os.environ.get("GROQ_API_KEY")
69
+ if not api_key:
70
+ st.error("GROQ_API_KEY is not set.")
71
+ st.stop()
72
+ client = Groq(api_key=api_key)
73
+ response = client.chat.completions.create(
74
+ model=LLM_MODEL,
75
+ messages=[
76
+ {"role": "system", "content": SYSTEM_PROMPT},
77
+ {"role": "user", "content": build_prompt(question, hits)},
78
+ ],
79
+ temperature=0.1,
80
+ )
81
+ return response.choices[0].message.content
82
+
83
+
84
+ # --- example question buttons ---
85
+ st.write("Try one:")
86
+ cols = st.columns(len(EXAMPLES))
87
+ for col, ex in zip(cols, EXAMPLES):
88
+ if col.button(ex, use_container_width=True):
89
+ st.session_state["question"] = ex
90
+
91
+ question = st.text_input("Ask about FastAPI's codebase",
92
+ key="question",
93
+ placeholder="How do I handle file uploads?")
94
+
95
+ if question:
96
+ with st.spinner("Searching the codebase..."):
97
+ hits = cached_retrieve(question, mode)
98
+ with st.spinner("Writing the answer..."):
99
+ answer = generate(question, hits)
100
+
101
+ st.markdown(answer)
102
+
103
+ st.divider()
104
+ st.subheader("Sources")
105
+ for i, h in enumerate(hits, 1):
106
+ meta = h["meta"]
107
+ label = (f"[{i}] {meta['source_type']} · {meta['path']}"
108
+ + (f" · {meta['symbol']}" if meta["symbol"] else ""))
109
+ with st.expander(label):
110
+ st.markdown(f"[Open on GitHub]({meta['url']})")
111
+ st.code(h["text"][:1500])
requirements.txt ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ streamlit
2
+ chromadb
3
+ sentence-transformers
4
+ groq
5
+ rank-bm25
src/ask.py CHANGED
@@ -18,8 +18,8 @@ import os
18
  import sys
19
 
20
  from retrieval import retrieve as retrieve_chunks
21
-
22
- REFUSAL_TEXT = "I couldn't find this in the indexed codebase."
23
  LLM_MODEL = os.environ.get("GROQ_MODEL", "openai/gpt-oss-120b")
24
  TOP_K = 5
25
 
@@ -74,10 +74,6 @@ def main() -> None:
74
  args = parser.parse_args()
75
 
76
  hits = retrieve_chunks(args.question, k=args.k, mode=args.mode)
77
- top_score = hits[0].get("score")
78
- if top_score is not None and top_score < SCORE_THRESHOLD:
79
- print(f"\n{REFUSAL_TEXT}")
80
- return
81
 
82
  if args.show_chunks:
83
  for i, h in enumerate(hits, 1):
 
18
  import sys
19
 
20
  from retrieval import retrieve as retrieve_chunks
21
+ from dotenv import load_dotenv
22
+ load_dotenv(override=True)
23
  LLM_MODEL = os.environ.get("GROQ_MODEL", "openai/gpt-oss-120b")
24
  TOP_K = 5
25
 
 
74
  args = parser.parse_args()
75
 
76
  hits = retrieve_chunks(args.question, k=args.k, mode=args.mode)
 
 
 
 
77
 
78
  if args.show_chunks:
79
  for i, h in enumerate(hits, 1):
src/eval.py CHANGED
@@ -23,6 +23,8 @@ import time
23
  from pathlib import Path
24
 
25
  from retrieval import retrieve
 
 
26
 
27
  DATA_DIR = Path("data")
28
  EVAL_SET = DATA_DIR / "eval_set.jsonl"
 
23
  from pathlib import Path
24
 
25
  from retrieval import retrieve
26
+ from dotenv import load_dotenv
27
+ load_dotenv(override=True)
28
 
29
  DATA_DIR = Path("data")
30
  EVAL_SET = DATA_DIR / "eval_set.jsonl"