absiitr commited on
Commit
637d303
Β·
verified Β·
1 Parent(s): cc7258d

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +232 -178
app.py CHANGED
@@ -9,115 +9,191 @@ from langchain_text_splitters import RecursiveCharacterTextSplitter
9
  from langchain_community.embeddings import HuggingFaceEmbeddings
10
  from langchain_community.vectorstores import Chroma
11
  import torch
12
- import warnings
13
-
14
- # Suppress warnings that clutter the Streamlit interface
15
- warnings.filterwarnings("ignore")
16
- logging.basicConfig(level=logging.INFO)
17
 
18
  # ---------------- CONFIGURATION ----------------
 
19
 
20
- # Load API key from Hugging Face secrets
21
  GROQ_API_KEY = st.secrets.get("GROQ_API_KEY", os.environ.get("GROQ_API_KEY"))
22
  GROQ_MODEL = "llama-3.1-8b-instant"
23
 
24
- # Initialize Groq client
25
  client = None
26
  if GROQ_API_KEY:
27
  try:
28
  client = Groq(api_key=GROQ_API_KEY)
29
- # 1. REMOVED the st.success message here
30
- logging.info("βœ… Groq client initialized successfully.")
31
  except Exception as e:
32
- st.error(f"❌ Failed to initialize Groq client: {e}")
33
  client = None
34
  else:
 
35
  st.warning("⚠️ GROQ_API_KEY not found. Please add it to Hugging Face secrets.")
36
 
37
  # ---------------- STREAMLIT UI SETUP ----------------
38
  st.set_page_config(page_title="PDF Assistant", page_icon="πŸ“˜", layout="wide")
39
 
40
- # ---------------- CSS (Modified to fix scrolling) ----------------
41
- st.markdown("""
 
42
  <style>
43
- /* ======================================================= */
44
- /* !!! NEW CSS FOR SCROLL FIX !!! */
45
- /* ======================================================= */
46
- /* Target the main content area and allow vertical scrolling */
47
- .main {
 
 
 
 
 
 
48
  overflow-y: auto;
 
 
49
  }
50
 
51
- /* Target the sidebar container and prevent scrolling */
52
- .st-emotion-cache-1ldf2b0, .st-emotion-cache-1c5c56c { /* Added common classes */
53
- position: fixed !important; /* Keep it fixed */
54
- overflow-y: hidden !important; /* Prevent scrolling inside the sidebar */
55
  }
56
 
57
- /* Target the overall body element to ensure full height and hide redundant scrolls */
58
- body {
59
- overflow: hidden; /* Hide the body scrollbar */
 
 
 
 
 
 
 
 
 
 
 
 
 
60
  }
61
- /* ======================================================= */
62
 
63
- :root {
64
- --primary-color: #1e3a8a;
65
- --background-color: #0e1117;
66
- --secondary-background-color: #1a1d29;
67
- --text-color: #f0f2f6;
68
  }
69
 
 
70
  .chat-user {
71
  background: #2d3748;
72
- padding: 12px;
73
- border-radius: 10px 10px 2px 10px;
74
- margin: 6px 0 6px auto;
75
- max-width: 85%;
76
- text-align: right;
77
- color: var(--text-color);
 
78
  }
79
  .chat-bot {
80
- background: var(--primary-color);
81
- padding: 12px;
82
- border-radius: 10px 10px 10px 2px;
83
- margin: 6px auto 6px 0;
84
- max-width: 85%;
85
- text-align: left;
86
- color: #ffffff;
 
87
  }
88
-
89
  .sources {
90
- font-size: 0.8em;
91
- opacity: 0.7;
92
- margin-top: 10px;
93
- border-top: 1px solid rgba(255, 255, 255, 0.1);
94
- padding-top: 5px;
 
 
 
 
 
 
 
 
 
 
 
 
95
  }
96
 
97
- /* Hide the default send button used by the form */
98
- div[data-testid="stFormSubmitButton"] {
99
- display: none;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
100
  }
101
  </style>
102
- """, unsafe_allow_html=True)
 
 
103
 
104
  # ---------------- SESSION STATE ----------------
105
  if "chat" not in st.session_state:
106
  st.session_state.chat = []
107
-
108
  if "vectorstore" not in st.session_state:
109
  st.session_state.vectorstore = None
110
-
111
  if "retriever" not in st.session_state:
112
  st.session_state.retriever = None
113
-
114
  if "uploaded_file_name" not in st.session_state:
115
  st.session_state.uploaded_file_name = None
116
-
117
  if "uploader_key" not in st.session_state:
118
  st.session_state.uploader_key = 0
119
 
120
- # ---------------- FUNCTIONS ----------------
121
  def clear_chat_history():
122
  st.session_state.chat = []
123
 
@@ -132,57 +208,47 @@ def clear_memory():
132
  st.success("Memory cleared. Please upload a new PDF.")
133
 
134
  def process_pdf(uploaded_file):
135
- """Process uploaded PDF and create vectorstore."""
136
  try:
137
  with tempfile.NamedTemporaryFile(delete=False, suffix=".pdf") as tmp:
138
  tmp.write(uploaded_file.getvalue())
139
  path = tmp.name
140
-
141
  loader = PyPDFLoader(path)
142
  docs = loader.load()
143
-
144
- splitter = RecursiveCharacterTextSplitter(
145
- chunk_size=800,
146
- chunk_overlap=50
147
- )
148
  chunks = splitter.split_documents(docs)
149
-
150
  embeddings = HuggingFaceEmbeddings(
151
  model_name="sentence-transformers/all-MiniLM-L6-v2",
152
  model_kwargs={"device": "cpu"},
153
- encode_kwargs={"normalize_embeddings": True}
154
  )
155
-
156
  vectorstore = Chroma.from_documents(chunks, embeddings)
157
  retriever = vectorstore.as_retriever(search_kwargs={"k": 3})
158
-
159
  st.session_state.vectorstore = vectorstore
160
  st.session_state.retriever = retriever
161
-
162
  if os.path.exists(path):
163
  os.unlink(path)
164
-
165
  return len(chunks)
166
-
167
  except Exception as e:
168
- st.error(f"Error processing PDF: {str(e)}")
169
- # Ensure cleanup if an error occurs
170
- if 'path' in locals() and os.path.exists(path):
171
- os.unlink(path)
172
  return None
173
 
174
  def ask_question(question):
175
- """Retrieve and generate answer for the question."""
176
  if not client:
177
  return None, 0, "Groq client is not initialized. Check API key setup."
178
-
179
  if not st.session_state.retriever:
180
  return None, 0, "Upload PDF first to initialize the knowledge base."
181
-
182
  try:
183
  docs = st.session_state.retriever.invoke(question)
184
  context = "\n\n".join(d.page_content for d in docs)
185
-
186
  prompt = f"""
187
  You are a strict RAG Q&A assistant.
188
  Use ONLY the context provided. If the answer is not found, reply:
@@ -199,120 +265,108 @@ FINAL ANSWER:
199
  response = client.chat.completions.create(
200
  model=GROQ_MODEL,
201
  messages=[
202
- {"role": "system",
203
- "content": "Use only the PDF content. If answer not found, say: 'I cannot find this in the PDF.'"},
204
- {"role": "user", "content": prompt}
205
  ],
206
- temperature=0.0
207
  )
208
-
209
  answer = response.choices[0].message.content.strip()
210
  return answer, len(docs), None
211
-
212
  except APIError as e:
213
  return None, 0, f"Groq API Error: {str(e)}"
214
  except Exception as e:
 
215
  return None, 0, f"General error: {str(e)}"
216
 
217
- def handle_question_submit():
218
- """Logic for processing the question, triggered by Enter or button."""
219
- question = st.session_state.question_input
220
-
221
- # Check if the question is non-empty and the input is enabled
222
- if question and not (st.session_state.uploaded_file_name is None or client is None):
223
- # Add user query to chat history
224
- st.session_state.chat.append(("user", question))
225
-
226
- # Get answer
227
- with st.spinner("Thinking..."):
228
- answer, sources, error = ask_question(question)
229
-
230
- if answer:
231
- bot_message = f"{answer}<div class='sources'>Context Chunks Used: {sources}</div>"
232
- st.session_state.chat.append(("bot", bot_message))
233
- else:
234
- st.session_state.chat.append(("bot", f"πŸ”΄ **Error:** {error}"))
235
-
236
- # Clear the input box after submission
237
- st.session_state.question_input = ""
238
- st.rerun()
239
-
240
- # ---------------- UI COMPONENTS ----------------
241
-
242
- # 2. Title and Creator Info (Top Left)
243
- col1, col2 = st.columns([0.4, 0.6])
244
- with col1:
245
- st.title("πŸ“˜ PDF Assistant")
246
- st.markdown(
247
- '**Creator:** [Abhishek Saxena](https://www.linkedin.com/in/abhishek-iitr/ "Connect on LinkedIn")',
248
- unsafe_allow_html=True
249
- )
250
- with col2:
251
- pass
252
-
253
- # Sidebar Controls
254
  with st.sidebar:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
255
  st.header("Controls")
256
  st.button("πŸ—‘οΈ Clear Chat History", on_click=clear_chat_history, use_container_width=True)
257
  st.button("πŸ”₯ Clear PDF Memory", on_click=clear_memory, use_container_width=True)
258
-
259
  st.markdown("---")
 
260
  if st.session_state.uploaded_file_name:
261
- st.success(f"βœ… **Active PDF:**\n `{st.session_state.uploaded_file_name}`")
262
  else:
263
- st.warning("⬆️ Upload a PDF to start chatting!")
264
-
265
- # File Upload
266
- uploaded = st.file_uploader(
267
- "Upload your PDF",
268
- type=["pdf"],
269
- key=st.session_state.uploader_key
 
 
 
 
 
 
270
  )
271
 
272
- if uploaded and uploaded.name != st.session_state.uploaded_file_name:
273
- st.session_state.uploaded_file_name = None
274
- st.session_state.chat = []
275
-
276
- with st.spinner(f"Processing '{uploaded.name}'..."):
277
- chunks_count = process_pdf(uploaded)
278
-
279
- if chunks_count is not None:
280
- st.success(f"βœ… PDF processed successfully! {chunks_count} chunks created.")
281
- st.session_state.uploaded_file_name = uploaded.name
282
  else:
283
- st.error("❌ Failed to process PDF")
284
- st.session_state.uploaded_file_name = None
285
-
286
- st.rerun()
287
-
288
- # ---------------- CHAT DISPLAY ----------------
289
- st.markdown("---")
290
- st.markdown("## Chat History")
291
-
292
- # 3. Chat Input using st.form for Enter key submission
293
- with st.form(key='chat_form'):
294
- disabled_input = st.session_state.uploaded_file_name is None or client is None
295
-
296
- # Text input for the question
297
- question = st.text_input(
298
- "Ask a question about the loaded PDF:",
299
- key="question_input",
300
- disabled=disabled_input
301
- )
302
-
303
- # Hidden submit button to allow Enter key submission
304
- submitted = st.form_submit_button(
305
- label="Send",
306
- disabled=disabled_input,
307
- )
308
-
309
- if submitted:
310
- # This block executes when the user presses Enter (or the hidden button)
311
- handle_question_submit()
312
-
313
- # Display Chat History (after processing the form to ensure new messages show up)
314
- for role, msg in st.session_state.chat:
315
- if role == "user":
316
- st.markdown(f"<div class='chat-user'>{msg}</div>", unsafe_allow_html=True)
317
- else:
318
- st.markdown(f"<div class='chat-bot'>{msg}</div>", unsafe_allow_html=True)
 
 
 
 
9
  from langchain_community.embeddings import HuggingFaceEmbeddings
10
  from langchain_community.vectorstores import Chroma
11
  import torch
 
 
 
 
 
12
 
13
  # ---------------- CONFIGURATION ----------------
14
+ logging.basicConfig(level=logging.INFO)
15
 
16
+ # Load API key from Hugging Face secrets (or env)
17
  GROQ_API_KEY = st.secrets.get("GROQ_API_KEY", os.environ.get("GROQ_API_KEY"))
18
  GROQ_MODEL = "llama-3.1-8b-instant"
19
 
20
+ # Initialize Groq client silently (no top green message)
21
  client = None
22
  if GROQ_API_KEY:
23
  try:
24
  client = Groq(api_key=GROQ_API_KEY)
25
+ logging.info("Groq client initialized (silent).")
 
26
  except Exception as e:
27
+ logging.exception("Groq init failed.")
28
  client = None
29
  else:
30
+ # Keep a visible but non-green hint if key is missing
31
  st.warning("⚠️ GROQ_API_KEY not found. Please add it to Hugging Face secrets.")
32
 
33
  # ---------------- STREAMLIT UI SETUP ----------------
34
  st.set_page_config(page_title="PDF Assistant", page_icon="πŸ“˜", layout="wide")
35
 
36
+ # ---------------- CSS (layout + styling) ----------------
37
+ st.markdown(
38
+ """
39
  <style>
40
+ :root{
41
+ --primary:#1e3a8a;
42
+ --bg:#0e1117;
43
+ --bg2:#1a1d29;
44
+ --text:#f0f2f6;
45
+ }
46
+
47
+ /* Fixed sidebar */
48
+ section[data-testid="stSidebar"] {
49
+ position: fixed;
50
+ height: 100vh;
51
  overflow-y: auto;
52
+ padding-top: 0.5rem;
53
+ width: 300px;
54
  }
55
 
56
+ /* Main content offset to the right of sidebar */
57
+ .main {
58
+ margin-left: 320px;
59
+ padding-top: 16px;
60
  }
61
 
62
+ /* Header (title + creator) */
63
+ .header-left {
64
+ display: flex;
65
+ flex-direction: column;
66
+ align-items: flex-start;
67
+ gap: 4px;
68
+ margin-left: 8px;
69
+ }
70
+ .header-title {
71
+ font-size: 1.6rem;
72
+ font-weight: 600;
73
+ margin: 0;
74
+ }
75
+ .header-creator {
76
+ font-size: 0.9rem;
77
+ color: var(--text);
78
  }
 
79
 
80
+ /* Chat scroll area */
81
+ .chat-area {
82
+ height: calc(100vh - 200px);
83
+ overflow-y: auto;
84
+ padding: 1rem 2rem;
85
  }
86
 
87
+ /* Chat bubble styles */
88
  .chat-user {
89
  background: #2d3748;
90
+ padding: 12px 14px;
91
+ border-radius: 18px 18px 4px 18px;
92
+ margin: 12px 0 12px auto;
93
+ max-width: 75%;
94
+ color: var(--text);
95
+ line-height: 1.4;
96
+ word-break: break-word;
97
  }
98
  .chat-bot {
99
+ background: var(--primary);
100
+ padding: 12px 14px;
101
+ border-radius: 18px 18px 18px 4px;
102
+ margin: 12px auto 12px 0;
103
+ max-width: 75%;
104
+ color: white;
105
+ line-height: 1.4;
106
+ word-break: break-word;
107
  }
 
108
  .sources {
109
+ font-size: 0.8rem;
110
+ opacity: 0.75;
111
+ margin-top: 8px;
112
+ border-top: 1px solid rgba(255,255,255,0.08);
113
+ padding-top: 6px;
114
+ }
115
+
116
+ /* Sticky input bar at bottom of main area */
117
+ .input-bar {
118
+ position: sticky;
119
+ bottom: 0;
120
+ left: 320px;
121
+ right: 0;
122
+ background: var(--bg);
123
+ padding: 12px 20px;
124
+ border-top: 1px solid rgba(255,255,255,0.06);
125
+ z-index: 999;
126
  }
127
 
128
+ /* Form layout */
129
+ .input-row {
130
+ max-width: 980px;
131
+ margin: 0 auto;
132
+ display: flex;
133
+ gap: 8px;
134
+ align-items: center;
135
+ }
136
+ .text-input {
137
+ flex: 1;
138
+ background: transparent;
139
+ border: 1px solid rgba(255,255,255,0.06);
140
+ padding: 10px 14px;
141
+ border-radius: 999px;
142
+ color: var(--text);
143
+ outline: none;
144
+ font-size: 1rem;
145
+ }
146
+ .text-input::placeholder {
147
+ color: rgba(255,255,255,0.45);
148
+ }
149
+ .submit-arrow {
150
+ background: var(--primary);
151
+ color: white;
152
+ border: none;
153
+ height: 42px;
154
+ width: 42px;
155
+ border-radius: 50%;
156
+ display: inline-flex;
157
+ align-items: center;
158
+ justify-content: center;
159
+ font-weight: 700;
160
+ cursor: pointer;
161
+ }
162
+ .submit-arrow:disabled {
163
+ opacity: 0.45;
164
+ cursor: not-allowed;
165
+ }
166
+
167
+ /* Small responsive tweak */
168
+ @media (max-width: 768px) {
169
+ section[data-testid="stSidebar"] {
170
+ position: relative;
171
+ width: 100%;
172
+ height: auto;
173
+ }
174
+ .main {
175
+ margin-left: 0;
176
+ }
177
+ .input-bar { left: 0; }
178
  }
179
  </style>
180
+ """,
181
+ unsafe_allow_html=True,
182
+ )
183
 
184
  # ---------------- SESSION STATE ----------------
185
  if "chat" not in st.session_state:
186
  st.session_state.chat = []
 
187
  if "vectorstore" not in st.session_state:
188
  st.session_state.vectorstore = None
 
189
  if "retriever" not in st.session_state:
190
  st.session_state.retriever = None
 
191
  if "uploaded_file_name" not in st.session_state:
192
  st.session_state.uploaded_file_name = None
 
193
  if "uploader_key" not in st.session_state:
194
  st.session_state.uploader_key = 0
195
 
196
+ # ---------------- HELPERS / LOGIC ----------------
197
  def clear_chat_history():
198
  st.session_state.chat = []
199
 
 
208
  st.success("Memory cleared. Please upload a new PDF.")
209
 
210
  def process_pdf(uploaded_file):
 
211
  try:
212
  with tempfile.NamedTemporaryFile(delete=False, suffix=".pdf") as tmp:
213
  tmp.write(uploaded_file.getvalue())
214
  path = tmp.name
215
+
216
  loader = PyPDFLoader(path)
217
  docs = loader.load()
218
+
219
+ splitter = RecursiveCharacterTextSplitter(chunk_size=800, chunk_overlap=50)
 
 
 
220
  chunks = splitter.split_documents(docs)
221
+
222
  embeddings = HuggingFaceEmbeddings(
223
  model_name="sentence-transformers/all-MiniLM-L6-v2",
224
  model_kwargs={"device": "cpu"},
225
+ encode_kwargs={"normalize_embeddings": True},
226
  )
227
+
228
  vectorstore = Chroma.from_documents(chunks, embeddings)
229
  retriever = vectorstore.as_retriever(search_kwargs={"k": 3})
230
+
231
  st.session_state.vectorstore = vectorstore
232
  st.session_state.retriever = retriever
233
+
234
  if os.path.exists(path):
235
  os.unlink(path)
236
+
237
  return len(chunks)
 
238
  except Exception as e:
239
+ logging.exception("PDF processing error")
240
+ st.error(f"Error processing PDF: {e}")
 
 
241
  return None
242
 
243
  def ask_question(question):
 
244
  if not client:
245
  return None, 0, "Groq client is not initialized. Check API key setup."
 
246
  if not st.session_state.retriever:
247
  return None, 0, "Upload PDF first to initialize the knowledge base."
 
248
  try:
249
  docs = st.session_state.retriever.invoke(question)
250
  context = "\n\n".join(d.page_content for d in docs)
251
+
252
  prompt = f"""
253
  You are a strict RAG Q&A assistant.
254
  Use ONLY the context provided. If the answer is not found, reply:
 
265
  response = client.chat.completions.create(
266
  model=GROQ_MODEL,
267
  messages=[
268
+ {"role": "system", "content": "Use only the PDF content. If answer not found, say: 'I cannot find this in the PDF.'"},
269
+ {"role": "user", "content": prompt},
 
270
  ],
271
+ temperature=0.0,
272
  )
 
273
  answer = response.choices[0].message.content.strip()
274
  return answer, len(docs), None
 
275
  except APIError as e:
276
  return None, 0, f"Groq API Error: {str(e)}"
277
  except Exception as e:
278
+ logging.exception("Ask error")
279
  return None, 0, f"General error: {str(e)}"
280
 
281
+ # ---------------- SIDEBAR (Upload + Controls) ----------------
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
282
  with st.sidebar:
283
+ st.markdown("### Upload PDF", unsafe_allow_html=True)
284
+ uploaded = st.file_uploader("Choose a PDF file", type=["pdf"], key=st.session_state.uploader_key, label_visibility="collapsed")
285
+ if uploaded and uploaded.name != st.session_state.uploaded_file_name:
286
+ # reset chat and process
287
+ st.session_state.chat = []
288
+ st.session_state.uploaded_file_name = None
289
+ with st.spinner(f"Processing '{uploaded.name}'..."):
290
+ chunks_count = process_pdf(uploaded)
291
+ if chunks_count is not None:
292
+ st.success(f"βœ… PDF processed successfully! {chunks_count} chunks created.")
293
+ st.session_state.uploaded_file_name = uploaded.name
294
+ else:
295
+ st.error("❌ Failed to process PDF")
296
+ st.session_state.uploaded_file_name = None
297
+ st.experimental_rerun()
298
+
299
+ st.markdown("---")
300
  st.header("Controls")
301
  st.button("πŸ—‘οΈ Clear Chat History", on_click=clear_chat_history, use_container_width=True)
302
  st.button("πŸ”₯ Clear PDF Memory", on_click=clear_memory, use_container_width=True)
303
+
304
  st.markdown("---")
305
+ st.subheader("Status")
306
  if st.session_state.uploaded_file_name:
307
+ st.success(f"βœ… Active PDF:\n`{st.session_state.uploaded_file_name}`")
308
  else:
309
+ st.info("⬆️ Upload a PDF to start chatting!")
310
+
311
+ # ---------------- MAIN HEADER (Top-left Title + Creator) ----------------
312
+ # uses "main" margin via CSS
313
+ st.markdown('<div class="main">', unsafe_allow_html=True)
314
+ st.markdown(
315
+ """
316
+ <div class="header-left">
317
+ <div class="header-title">πŸ“˜ PDF Assistant</div>
318
+ <div class="header-creator">Created by <a href="https://www.linkedin.com/in/abhishek-iitr/" target="_blank" style="color: #9fc2ff;">Abhishek Saxena</a></div>
319
+ </div>
320
+ """,
321
+ unsafe_allow_html=True,
322
  )
323
 
324
+ # ---------------- CHAT AREA ----------------
325
+ st.markdown('<div class="chat-area">', unsafe_allow_html=True)
326
+
327
+ if not st.session_state.chat:
328
+ st.markdown('<div style="color:rgba(255,255,255,0.55); padding:20px 0;">Ask a question about your document to start the conversation.</div>', unsafe_allow_html=True)
329
+ else:
330
+ for role, msg in st.session_state.chat:
331
+ if role == "user":
332
+ st.markdown(f"<div class='chat-user'>{msg}</div>", unsafe_allow_html=True)
 
333
  else:
334
+ st.markdown(f"<div class='chat-bot'>{msg}</div>", unsafe_allow_html=True)
335
+
336
+ st.markdown("</div>", unsafe_allow_html=True) # close chat-area
337
+
338
+ # ---------------- INPUT BAR (Enter to submit + arrow button) ----------------
339
+ st.markdown('<div class="input-bar">', unsafe_allow_html=True)
340
+ st.markdown('<div class="input-row">', unsafe_allow_html=True)
341
+
342
+ # Build a form: pressing Enter in the input will submit the form.
343
+ with st.form(key="ask_form", clear_on_submit=True):
344
+ cols = st.columns([1, 0.12])
345
+ with cols[0]:
346
+ q = st.text_input(
347
+ "Type your question",
348
+ key="question_input",
349
+ placeholder="Ask anything about your PDF document...",
350
+ disabled=(st.session_state.uploaded_file_name is None or client is None),
351
+ label_visibility="collapsed",
352
+ )
353
+ with cols[1]:
354
+ # Arrow submit button (visible). Pressing Enter also triggers the form submit.
355
+ submit = st.form_submit_button("➀", help="Send (Enter or click)", disabled=(st.session_state.uploaded_file_name is None or client is None))
356
+
357
+ st.markdown("</div></div>", unsafe_allow_html=True) # close input-row and input-bar
358
+
359
+ if submit and q:
360
+ # Append user message
361
+ st.session_state.chat.append(("user", q))
362
+
363
+ with st.spinner("Thinking..."):
364
+ answer, sources, error = ask_question(q)
365
+ if answer:
366
+ bot_msg = f"{answer}<div class='sources'>Context Chunks Used: {sources}</div>"
367
+ st.session_state.chat.append(("bot", bot_msg))
368
+ else:
369
+ st.session_state.chat.append(("bot", f"πŸ”΄ **Error:** {error}"))
370
+
371
+ # Rerun so new messages show and form clears
372
+ st.experimental_rerun()