Islam Mamedov commited on
Commit
e5e7d99
·
1 Parent(s): b100e4f

Ship chat UI, update project links, and add MIT license

Browse files
Files changed (3) hide show
  1. LICENSE +21 -0
  2. README.md +2 -2
  3. app.py +110 -42
LICENSE ADDED
@@ -0,0 +1,21 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Islam Mamedov
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
README.md CHANGED
@@ -15,7 +15,7 @@ codebase and get answers grounded in its actual source code, documentation,
15
  and GitHub issues — with citations linking to the exact lines, and honest
16
  refusals when the corpus doesn't contain the answer.
17
 
18
- **Live demo:** https://huggingface.co/spaces/Pace200413/fastapi-codebase-qa
19
 
20
  Every architectural decision in this project was made by measuring, not
21
  guessing: a hand-labeled 42-question eval set gates every change, and the
@@ -129,7 +129,7 @@ to FastAPI" from "answerable from FastAPI's corpus".
129
  ## Run it yourself
130
 
131
  ```bash
132
- git clone https://github.com/Pace200413/codebase-rag && cd codebase-rag
133
  python -m venv .venv && source .venv/bin/activate
134
  pip install -r requirements.txt PyGithub tree-sitter tree-sitter-python pytest
135
  echo "GROQ_API_KEY=gsk_..." > .env # free key: console.groq.com
 
15
  and GitHub issues — with citations linking to the exact lines, and honest
16
  refusals when the corpus doesn't contain the answer.
17
 
18
+ **Live demo:** https://huggingface.co/spaces/islam-mamedov/fastapi-codebase-qa
19
 
20
  Every architectural decision in this project was made by measuring, not
21
  guessing: a hand-labeled 42-question eval set gates every change, and the
 
129
  ## Run it yourself
130
 
131
  ```bash
132
+ git clone https://github.com/islam-mamedov/codebase-rag && cd codebase-rag
133
  python -m venv .venv && source .venv/bin/activate
134
  pip install -r requirements.txt PyGithub tree-sitter tree-sitter-python pytest
135
  echo "GROQ_API_KEY=gsk_..." > .env # free key: console.groq.com
app.py CHANGED
@@ -10,6 +10,7 @@ first boot (~3 min, cached afterwards).
10
 
11
  import os
12
  import sys
 
13
  from pathlib import Path
14
 
15
  from dotenv import load_dotenv
@@ -32,7 +33,24 @@ EXAMPLES = [
32
  ]
33
 
34
  st.set_page_config(page_title="FastAPI Codebase Q&A", page_icon="⚡",
35
- layout="wide")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
36
 
37
 
38
  @st.cache_resource(show_spinner="First boot: building the search index "
@@ -53,13 +71,9 @@ def ensure_index() -> bool:
53
 
54
  ensure_index()
55
 
56
- st.title("⚡ FastAPI Codebase Q&A")
57
- st.caption("Retrieval-augmented answers over FastAPI's source code, docs, "
58
- "and GitHub issues — every claim cited, honest refusals when "
59
- "the corpus doesn't know.")
60
-
61
  with st.sidebar:
62
- st.header("How it works")
63
  st.markdown(
64
  "1. Your question is embedded (`bge-small-en-v1.5`)\n"
65
  "2. Top-5 chunks retrieved from ~1,350 AST-aware chunks "
@@ -68,16 +82,30 @@ with st.sidebar:
68
  "4. Citations link to the exact GitHub lines"
69
  )
70
  mode = st.selectbox("Retrieval mode", ["dense", "dense_rw", "hybrid"],
71
- help="dense won the ablation; others shown for "
72
- "comparison")
73
  st.divider()
74
- st.markdown(
75
- "**Eval results (42-question set)**\n\n"
76
- "recall@5 **0.91** · MRR **0.71**\n\n"
77
- "faithful **0.89** · correct **0.91** · refusal **7/7**"
78
- )
79
- st.markdown("[Source & write-up]"
 
 
 
80
  "(https://github.com/Pace200413/codebase-rag)")
 
 
 
 
 
 
 
 
 
 
 
81
 
82
 
83
  @st.cache_data(show_spinner=False)
@@ -85,49 +113,89 @@ def cached_retrieve(question: str, mode: str) -> list[dict]:
85
  return retrieve(question, k=5, mode=mode)
86
 
87
 
88
- def generate(question: str, hits: list[dict]) -> str:
 
89
  from groq import Groq
90
  api_key = os.environ.get("GROQ_API_KEY")
91
  if not api_key:
92
  st.error("GROQ_API_KEY is not set.")
93
  st.stop()
94
  client = Groq(api_key=api_key)
95
- response = client.chat.completions.create(
96
  model=LLM_MODEL,
97
  messages=[
98
  {"role": "system", "content": SYSTEM_PROMPT},
99
  {"role": "user", "content": build_prompt(question, hits)},
100
  ],
101
  temperature=0.1,
 
102
  )
103
- return response.choices[0].message.content
104
-
105
-
106
- # --- example question buttons ---
107
- st.write("Try one:")
108
- cols = st.columns(len(EXAMPLES))
109
- for col, ex in zip(cols, EXAMPLES):
110
- if col.button(ex, use_container_width=True):
111
- st.session_state["question"] = ex
112
 
113
- question = st.text_input("Ask about FastAPI's codebase",
114
- key="question",
115
- placeholder="How do I handle file uploads?")
116
 
117
- if question:
118
- with st.spinner("Searching the codebase..."):
119
- hits = cached_retrieve(question, mode)
120
- with st.spinner("Writing the answer..."):
121
- answer = generate(question, hits)
122
-
123
- st.markdown(answer)
124
-
125
- st.divider()
126
- st.subheader("Sources")
127
  for i, h in enumerate(hits, 1):
128
  meta = h["meta"]
129
- label = (f"[{i}] {meta['source_type']} · {meta['path']}"
 
 
130
  + (f" · {meta['symbol']}" if meta["symbol"] else ""))
131
  with st.expander(label):
132
- st.markdown(f"[Open on GitHub]({meta['url']})")
133
- st.code(h["text"][:1500])
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
10
 
11
  import os
12
  import sys
13
+ import time
14
  from pathlib import Path
15
 
16
  from dotenv import load_dotenv
 
33
  ]
34
 
35
  st.set_page_config(page_title="FastAPI Codebase Q&A", page_icon="⚡",
36
+ layout="centered")
37
+
38
+ st.markdown("""
39
+ <style>
40
+ .stButton > button {
41
+ border-radius: 12px;
42
+ border: 1px solid rgba(128, 128, 128, 0.35);
43
+ font-size: 0.85rem;
44
+ transition: border-color 0.2s;
45
+ }
46
+ .stButton > button:hover { border-color: #f0b429; color: #f0b429; }
47
+ [data-testid="stMetricValue"] { font-size: 1.4rem; }
48
+ [data-testid="stExpander"] {
49
+ border-radius: 10px;
50
+ border: 1px solid rgba(128, 128, 128, 0.25);
51
+ }
52
+ </style>
53
+ """, unsafe_allow_html=True)
54
 
55
 
56
  @st.cache_resource(show_spinner="First boot: building the search index "
 
71
 
72
  ensure_index()
73
 
74
+ # ---------------- sidebar ----------------
 
 
 
 
75
  with st.sidebar:
76
+ st.header("⚙️ How it works")
77
  st.markdown(
78
  "1. Your question is embedded (`bge-small-en-v1.5`)\n"
79
  "2. Top-5 chunks retrieved from ~1,350 AST-aware chunks "
 
82
  "4. Citations link to the exact GitHub lines"
83
  )
84
  mode = st.selectbox("Retrieval mode", ["dense", "dense_rw", "hybrid"],
85
+ help="dense won a 7-configuration ablation; "
86
+ "others shown for comparison")
87
  st.divider()
88
+ st.subheader("📊 Eval (42-question benchmark)")
89
+ c1, c2 = st.columns(2)
90
+ c1.metric("recall@5", "0.91")
91
+ c2.metric("MRR", "0.71")
92
+ c3, c4 = st.columns(2)
93
+ c3.metric("correct", "0.91")
94
+ c4.metric("refusal", "7/7")
95
+ st.divider()
96
+ st.markdown("[💻 Source & write-up]"
97
  "(https://github.com/Pace200413/codebase-rag)")
98
+ if st.button("🗑 Clear conversation", use_container_width=True):
99
+ st.session_state.messages = []
100
+ st.rerun()
101
+
102
+ # ---------------- header ----------------
103
+ st.title("⚡ FastAPI Codebase Q&A")
104
+ st.caption("Ask anything about FastAPI's source code, docs, and GitHub "
105
+ "issues. Every claim is cited. When the corpus doesn't know, "
106
+ "it says so.")
107
+
108
+ # ---------------- helpers ----------------
109
 
110
 
111
  @st.cache_data(show_spinner=False)
 
113
  return retrieve(question, k=5, mode=mode)
114
 
115
 
116
+ def stream_answer(question: str, hits: list[dict]):
117
+ """Yield the answer token by token (looks alive, no behavior change)."""
118
  from groq import Groq
119
  api_key = os.environ.get("GROQ_API_KEY")
120
  if not api_key:
121
  st.error("GROQ_API_KEY is not set.")
122
  st.stop()
123
  client = Groq(api_key=api_key)
124
+ stream = client.chat.completions.create(
125
  model=LLM_MODEL,
126
  messages=[
127
  {"role": "system", "content": SYSTEM_PROMPT},
128
  {"role": "user", "content": build_prompt(question, hits)},
129
  ],
130
  temperature=0.1,
131
+ stream=True,
132
  )
133
+ for chunk in stream:
134
+ delta = chunk.choices[0].delta.content
135
+ if delta:
136
+ yield delta
 
 
 
 
 
137
 
 
 
 
138
 
139
+ def render_sources(hits: list[dict]) -> None:
140
+ st.markdown("##### 📎 Sources")
 
 
 
 
 
 
 
 
141
  for i, h in enumerate(hits, 1):
142
  meta = h["meta"]
143
+ icon = {"code": "🧩", "doc": "📄", "issue": "🐛"}.get(
144
+ meta["source_type"], "📄")
145
+ label = (f"{icon} [{i}] {meta['path']}"
146
  + (f" · {meta['symbol']}" if meta["symbol"] else ""))
147
  with st.expander(label):
148
+ st.markdown(f"[Open on GitHub ↗]({meta['url']})")
149
+ st.code(h["text"][:1200])
150
+
151
+
152
+ # ---------------- conversation state ----------------
153
+ if "messages" not in st.session_state:
154
+ st.session_state.messages = []
155
+
156
+ # render history
157
+ for msg in st.session_state.messages:
158
+ with st.chat_message(msg["role"]):
159
+ st.markdown(msg["content"])
160
+ if msg.get("sources"):
161
+ render_sources(msg["sources"])
162
+ if msg.get("timing"):
163
+ st.caption(msg["timing"])
164
+
165
+ # example buttons (only before the first question, to keep the video clean)
166
+ pending = None
167
+ if not st.session_state.messages:
168
+ st.write("**Try one:**")
169
+ cols = st.columns(2)
170
+ for i, ex in enumerate(EXAMPLES):
171
+ if cols[i % 2].button(ex, use_container_width=True):
172
+ pending = ex
173
+
174
+ prompt = st.chat_input("Ask about FastAPI's codebase...") or pending
175
+
176
+ if prompt:
177
+ st.session_state.messages.append({"role": "user", "content": prompt})
178
+ with st.chat_message("user"):
179
+ st.markdown(prompt)
180
+
181
+ with st.chat_message("assistant"):
182
+ t0 = time.perf_counter()
183
+ with st.spinner("Searching 1,352 chunks..."):
184
+ hits = cached_retrieve(prompt, mode)
185
+ t_retrieve = time.perf_counter() - t0
186
+
187
+ t1 = time.perf_counter()
188
+ answer = st.write_stream(stream_answer(prompt, hits))
189
+ t_answer = time.perf_counter() - t1
190
+
191
+ timing = (f"retrieved in {t_retrieve:.2f}s · "
192
+ f"answered in {t_answer:.1f}s · mode: {mode}")
193
+ st.caption(timing)
194
+ render_sources(hits)
195
+
196
+ st.session_state.messages.append({
197
+ "role": "assistant",
198
+ "content": answer,
199
+ "sources": hits,
200
+ "timing": timing,
201
+ })