zakerytclarke commited on
Commit
cbd2e30
·
verified ·
1 Parent(s): 3e65f9e

Update src/streamlit_app.py

Browse files
Files changed (1) hide show
  1. src/streamlit_app.py +305 -141
src/streamlit_app.py CHANGED
@@ -3,7 +3,7 @@ import os
3
  import re
4
  import time
5
  import warnings
6
- from typing import List, Dict
7
 
8
  import requests
9
  import streamlit as st
@@ -14,73 +14,30 @@ from teapotai import TeapotAI
14
 
15
 
16
  # -----------------------
17
- # Silence noisy warnings (optional)
18
  # -----------------------
19
  warnings.filterwarnings("ignore", message="pkg_resources is deprecated as an API.*")
20
  warnings.filterwarnings("ignore", message='Field name "schema" in "TeapotTool" shadows.*')
21
 
22
 
23
  # -----------------------
24
- # Branding / Theme
25
  # -----------------------
26
  TEAPOT_LOGO_GIF = "https://teapotai.com/assets/logo.gif"
27
 
28
- TEA_BG = "#fbf7ef" # warm off-white
29
- TEA_PANEL = "#fffaf2" # slightly brighter
30
- TEA_TEXT = "#1f2937" # slate-ish
31
- TEA_MUTED = "#6b7280" # gray
32
- TEA_ACCENT = "#c0841d" # warm amber
33
- TEA_BORDER = "rgba(31, 41, 55, 0.10)"
34
-
35
- st.set_page_config(
36
- page_title="TeapotAI Chat",
37
- page_icon="🫖",
38
- layout="centered",
39
- )
40
-
41
- CUSTOM_CSS = f"""
42
- <style>
43
- .stApp {{
44
- background: {TEA_BG};
45
- color: {TEA_TEXT};
46
- }}
47
-
48
- section[data-testid="stSidebar"] {{
49
- background: {TEA_PANEL};
50
- border-right: 1px solid {TEA_BORDER};
51
- }}
52
-
53
- div[data-testid="stChatMessage"] {{
54
- border-radius: 16px;
55
- padding: 8px 10px;
56
- }}
57
-
58
- .stTextInput > div > div, .stTextArea > div > div {{
59
- border-radius: 12px !important;
60
- }}
61
-
62
- .stButton button {{
63
- border-radius: 12px;
64
- border: 1px solid {TEA_BORDER};
65
- }}
66
-
67
- a {{
68
- color: {TEA_ACCENT} !important;
69
- }}
70
- </style>
71
- """
72
- st.markdown(CUSTOM_CSS, unsafe_allow_html=True)
73
 
74
 
75
  # -----------------------
76
- # Config
77
  # -----------------------
78
- BRAVE_ENDPOINT = "https://api.search.brave.com/res/v1/web/search"
79
- TOP_K = 3
80
- TIMEOUT_SECS = 15
81
-
82
  MODEL_TINY = "teapotai/tinyteapot"
83
- MODEL_LLM = "teapotai/teapotllm"
 
84
 
85
  DEFAULT_SYSTEM_PROMPT = (
86
  "You are Teapot, an open-source AI assistant optimized for low-end devices, "
@@ -95,39 +52,173 @@ DEFAULT_DOCUMENTS = [
95
  ]
96
 
97
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
98
  # -----------------------
99
  # Helpers
100
  # -----------------------
101
  def st_image_full_width(img_url: str):
102
- """
103
- HF Spaces sometimes pins an older streamlit build where st.image doesn't accept
104
- use_container_width. Fall back to use_column_width.
105
- """
106
  try:
107
  st.image(img_url, use_container_width=True)
108
  except TypeError:
109
  st.image(img_url, use_column_width=True)
110
 
111
 
112
- def get_brave_key() -> str:
113
- # HF Spaces typically provides secrets as env vars.
114
  return os.getenv("BRAVE_API_KEY") or (st.secrets.get("BRAVE_API_KEY") if hasattr(st, "secrets") else None)
115
 
116
 
117
  def brave_search_snippets(query: str, top_k: int = 3) -> List[Dict[str, str]]:
118
  brave_api_key = get_brave_key()
119
  if not brave_api_key:
120
- raise RuntimeError("Missing BRAVE_API_KEY (set Space secret or env var).")
121
 
122
  headers = {"Accept": "application/json", "X-Subscription-Token": brave_api_key}
123
  params = {"q": query, "count": top_k}
124
 
125
- resp = requests.get(
126
- BRAVE_ENDPOINT,
127
- headers=headers,
128
- params=params,
129
- timeout=TIMEOUT_SECS,
130
- )
131
  resp.raise_for_status()
132
  data = resp.json()
133
 
@@ -144,9 +235,7 @@ def brave_search_snippets(query: str, top_k: int = 3) -> List[Dict[str, str]]:
144
 
145
 
146
  def format_context_from_results(results: List[Dict[str, str]]) -> str:
147
- """
148
- Stable formatting + strip <strong> tags as requested.
149
- """
150
  if not results:
151
  return ""
152
 
@@ -159,18 +248,11 @@ def format_context_from_results(results: List[Dict[str, str]]) -> str:
159
  title = title.replace("<strong>", "").replace("</strong>", "")
160
  snippet = snippet.replace("<strong>", "").replace("</strong>", "")
161
 
162
- blocks.append(
163
- f"[{i}] {title}\n"
164
- f"URL: {url}\n"
165
- f"Snippet: {snippet}"
166
- )
167
  return "\n\n".join(blocks)
168
 
169
 
170
- def typewriter_render(text: str, container, speed_chars_per_sec: float = 350.0):
171
- """
172
- TeapotAI.query returns a full string; mimic streaming with a typewriter effect.
173
- """
174
  if not text:
175
  container.markdown("")
176
  return
@@ -183,13 +265,10 @@ def typewriter_render(text: str, container, speed_chars_per_sec: float = 350.0):
183
 
184
 
185
  # -----------------------
186
- # Model / TeapotAI loader
187
  # -----------------------
188
  @st.cache_resource
189
  def load_teapot_ai(model_name: str) -> TeapotAI:
190
- """
191
- Cached per model_name. TinyTeapot is loaded on startup via an explicit call.
192
- """
193
  tokenizer = AutoTokenizer.from_pretrained(model_name)
194
  model = AutoModelForSeq2SeqLM.from_pretrained(model_name)
195
 
@@ -205,81 +284,166 @@ def load_teapot_ai(model_name: str) -> TeapotAI:
205
 
206
 
207
  # -----------------------
208
- # UI Header
209
  # -----------------------
210
- col1, col2 = st.columns([1, 3], vertical_alignment="center")
211
- with col1:
212
- st_image_full_width(TEAPOT_LOGO_GIF)
213
- with col2:
214
- st.markdown("## TeapotAI Chat")
215
- st.caption("Brave Search (top 3 snippets) → context → TeapotAI.query()")
 
 
216
 
217
- with st.sidebar:
218
- st.header("Settings")
219
 
220
- model_choice = st.radio(
221
- "Model",
222
- options=[MODEL_TINY, MODEL_LLM],
223
- index=0,
224
- help="TinyTeapot loads by default. Switching loads the other model (cached).",
225
- )
226
 
227
- system_prompt = st.text_area("System prompt", value=DEFAULT_SYSTEM_PROMPT, height=150)
228
- show_sources = st.checkbox("Show sources/context", value=True)
229
- typing_effect = st.checkbox("Typing effect", value=True)
 
 
 
 
 
 
230
 
 
 
231
 
232
- # Requirement: load tiny model on startup regardless of selection
233
- _ = load_teapot_ai(MODEL_TINY)
 
 
 
 
 
 
 
234
 
235
- # Load selected model (cached after first load)
236
- teapot_ai = load_teapot_ai(model_choice)
 
237
 
238
 
239
  # -----------------------
240
- # Chat state
241
  # -----------------------
242
- if "messages" not in st.session_state:
243
- st.session_state.messages = [] # [{"role": "user"/"assistant", "content": str}]
 
 
 
 
 
 
244
 
245
- for m in st.session_state.messages:
246
- with st.chat_message(m["role"]):
247
- st.markdown(m["content"])
248
 
249
- question = st.chat_input("Ask a question… (Brave will fetch top 3 snippets)")
250
-
251
- if question:
252
- st.session_state.messages.append({"role": "user", "content": question})
253
- with st.chat_message("user"):
254
- st.markdown(question)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
255
 
256
- # Brave search context
257
- try:
258
- results = brave_search_snippets(question, top_k=TOP_K)
259
- context = format_context_from_results(results)
260
- except Exception:
261
- results = []
262
- context = ""
263
 
264
- # TeapotAI query (context includes Brave results)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
265
  answer = teapot_ai.query(
266
- query=question,
267
  context=context,
268
  system_prompt=system_prompt,
269
  )
270
 
271
- with st.chat_message("assistant"):
272
- if show_sources:
273
- with st.expander("Sources / Context used", expanded=False):
274
- if context.strip():
275
- st.code(context)
276
- else:
277
- st.write("(No search context returned.)")
278
-
279
- placeholder = st.empty()
280
- if typing_effect:
281
- typewriter_render(answer, placeholder, speed_chars_per_sec=350.0)
282
- else:
283
- placeholder.markdown(answer)
284
-
285
- st.session_state.messages.append({"role": "assistant", "content": answer})
 
3
  import re
4
  import time
5
  import warnings
6
+ from typing import List, Dict, Any, Optional
7
 
8
  import requests
9
  import streamlit as st
 
14
 
15
 
16
  # -----------------------
17
+ # Warnings (optional)
18
  # -----------------------
19
  warnings.filterwarnings("ignore", message="pkg_resources is deprecated as an API.*")
20
  warnings.filterwarnings("ignore", message='Field name "schema" in "TeapotTool" shadows.*')
21
 
22
 
23
  # -----------------------
24
+ # Brand / Assets
25
  # -----------------------
26
  TEAPOT_LOGO_GIF = "https://teapotai.com/assets/logo.gif"
27
 
28
+ SUGGESTED_QUERIES = [
29
+ "Who are you?",
30
+ "Tell me about teapotllm",
31
+ "What is the weather like in NYC today?",
32
+ ]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
33
 
34
 
35
  # -----------------------
36
+ # Models
37
  # -----------------------
 
 
 
 
38
  MODEL_TINY = "teapotai/tinyteapot"
39
+ MODEL_LLM = "teapotai/teapotllm" # if you keep the toggle elsewhere later
40
+ DEFAULT_MODEL = MODEL_TINY
41
 
42
  DEFAULT_SYSTEM_PROMPT = (
43
  "You are Teapot, an open-source AI assistant optimized for low-end devices, "
 
52
  ]
53
 
54
 
55
+ # -----------------------
56
+ # Web search (Brave) config
57
+ # -----------------------
58
+ BRAVE_ENDPOINT = "https://api.search.brave.com/res/v1/web/search"
59
+ TOP_K = 3
60
+ TIMEOUT_SECS = 15
61
+
62
+
63
+ # -----------------------
64
+ # Streamlit page config
65
+ # -----------------------
66
+ st.set_page_config(page_title="TeapotAI Chat", page_icon="🫖", layout="centered")
67
+
68
+
69
+ # -----------------------
70
+ # Theme (light/dark) via CSS variables
71
+ # -----------------------
72
+ LIGHT_THEME = dict(
73
+ bg="#fbf7ef",
74
+ panel="#fffaf2",
75
+ text="#111827",
76
+ muted="#6b7280",
77
+ accent="#c0841d",
78
+ border="rgba(17, 24, 39, 0.12)",
79
+ bubble_user="#eef2ff",
80
+ bubble_asst="#ffffff",
81
+ code_bg="#0b1220",
82
+ )
83
+
84
+ DARK_THEME = dict(
85
+ bg="#0b0f19",
86
+ panel="#0f1626",
87
+ text="#e5e7eb",
88
+ muted="#9ca3af",
89
+ accent="#f59e0b",
90
+ border="rgba(229, 231, 235, 0.12)",
91
+ bubble_user="#111827",
92
+ bubble_asst="#0f172a",
93
+ code_bg="#0b1220",
94
+ )
95
+
96
+
97
+ def inject_css(theme: dict):
98
+ css = f"""
99
+ <style>
100
+ :root {{
101
+ --bg: {theme["bg"]};
102
+ --panel: {theme["panel"]};
103
+ --text: {theme["text"]};
104
+ --muted: {theme["muted"]};
105
+ --accent: {theme["accent"]};
106
+ --border: {theme["border"]};
107
+ --bubble_user: {theme["bubble_user"]};
108
+ --bubble_asst: {theme["bubble_asst"]};
109
+ --code_bg: {theme["code_bg"]};
110
+ }}
111
+
112
+ .stApp {{
113
+ background: var(--bg);
114
+ color: var(--text);
115
+ }}
116
+
117
+ section[data-testid="stSidebar"] {{
118
+ background: var(--panel);
119
+ border-right: 1px solid var(--border);
120
+ }}
121
+
122
+ /* Header title */
123
+ h1, h2, h3, p, span, label {{
124
+ color: var(--text) !important;
125
+ }}
126
+
127
+ a {{
128
+ color: var(--accent) !important;
129
+ text-decoration: none;
130
+ }}
131
+ a:hover {{ text-decoration: underline; }}
132
+
133
+ /* Chat message containers */
134
+ div[data-testid="stChatMessage"] {{
135
+ border-radius: 18px;
136
+ padding: 10px 12px;
137
+ }}
138
+
139
+ /* We color bubbles using a wrapper class we add via markdown */
140
+ .bubble-user {{
141
+ background: var(--bubble_user);
142
+ border: 1px solid var(--border);
143
+ border-radius: 16px;
144
+ padding: 10px 12px;
145
+ }}
146
+ .bubble-asst {{
147
+ background: var(--bubble_asst);
148
+ border: 1px solid var(--border);
149
+ border-radius: 16px;
150
+ padding: 10px 12px;
151
+ }}
152
+
153
+ /* Inputs and buttons */
154
+ .stTextArea textarea {{
155
+ border-radius: 12px !important;
156
+ }}
157
+ .stButton button {{
158
+ border-radius: 999px !important;
159
+ border: 1px solid var(--border) !important;
160
+ }}
161
+
162
+ /* Make popover trigger button look like an icon */
163
+ button[kind="secondary"] {{
164
+ border-radius: 999px !important;
165
+ }}
166
+
167
+ /* Code blocks */
168
+ code, pre {{
169
+ background: var(--code_bg) !important;
170
+ }}
171
+
172
+ /* Suggested chips */
173
+ .suggest-row {{
174
+ display: flex;
175
+ gap: 10px;
176
+ flex-wrap: wrap;
177
+ margin-top: 6px;
178
+ }}
179
+ .suggest-chip {{
180
+ display: inline-block;
181
+ padding: 10px 12px;
182
+ border: 1px solid var(--border);
183
+ border-radius: 999px;
184
+ background: var(--panel);
185
+ color: var(--text);
186
+ cursor: pointer;
187
+ user-select: none;
188
+ }}
189
+ .suggest-chip:hover {{
190
+ border-color: var(--accent);
191
+ }}
192
+ </style>
193
+ """
194
+ st.markdown(css, unsafe_allow_html=True)
195
+
196
+
197
  # -----------------------
198
  # Helpers
199
  # -----------------------
200
  def st_image_full_width(img_url: str):
201
+ # Streamlit API varies across HF images
 
 
 
202
  try:
203
  st.image(img_url, use_container_width=True)
204
  except TypeError:
205
  st.image(img_url, use_column_width=True)
206
 
207
 
208
+ def get_brave_key() -> Optional[str]:
209
+ # HF Spaces secrets are usually env vars; also allow Streamlit secrets
210
  return os.getenv("BRAVE_API_KEY") or (st.secrets.get("BRAVE_API_KEY") if hasattr(st, "secrets") else None)
211
 
212
 
213
  def brave_search_snippets(query: str, top_k: int = 3) -> List[Dict[str, str]]:
214
  brave_api_key = get_brave_key()
215
  if not brave_api_key:
216
+ raise RuntimeError("Missing BRAVE_API_KEY (set Space secret / env var).")
217
 
218
  headers = {"Accept": "application/json", "X-Subscription-Token": brave_api_key}
219
  params = {"q": query, "count": top_k}
220
 
221
+ resp = requests.get(BRAVE_ENDPOINT, headers=headers, params=params, timeout=TIMEOUT_SECS)
 
 
 
 
 
222
  resp.raise_for_status()
223
  data = resp.json()
224
 
 
235
 
236
 
237
  def format_context_from_results(results: List[Dict[str, str]]) -> str:
238
+ # Stable formatting + strip <strong> tags exactly as requested
 
 
239
  if not results:
240
  return ""
241
 
 
248
  title = title.replace("<strong>", "").replace("</strong>", "")
249
  snippet = snippet.replace("<strong>", "").replace("</strong>", "")
250
 
251
+ blocks.append(f"[{i}] {title}\nURL: {url}\nSnippet: {snippet}")
 
 
 
 
252
  return "\n\n".join(blocks)
253
 
254
 
255
+ def typewriter_render(text: str, container, speed_chars_per_sec: float = 450.0):
 
 
 
256
  if not text:
257
  container.markdown("")
258
  return
 
265
 
266
 
267
  # -----------------------
268
+ # Model / TeapotAI loader (cached)
269
  # -----------------------
270
  @st.cache_resource
271
  def load_teapot_ai(model_name: str) -> TeapotAI:
 
 
 
272
  tokenizer = AutoTokenizer.from_pretrained(model_name)
273
  model = AutoModelForSeq2SeqLM.from_pretrained(model_name)
274
 
 
284
 
285
 
286
  # -----------------------
287
+ # Session state init
288
  # -----------------------
289
+ if "theme" not in st.session_state:
290
+ st.session_state.theme = "Light"
291
+ if "messages" not in st.session_state:
292
+ # Each message can optionally include sources/context metadata
293
+ # {"role": "assistant", "content": "...", "sources": [...], "context": "..."}
294
+ st.session_state.messages = []
295
+ if "pending_query" not in st.session_state:
296
+ st.session_state.pending_query = None
297
 
298
+ # Inject theme CSS
299
+ inject_css(LIGHT_THEME if st.session_state.theme == "Light" else DARK_THEME)
300
 
 
 
 
 
 
 
301
 
302
+ # -----------------------
303
+ # Sidebar (ONLY configurable: theme toggle, system prompt, web search checkbox)
304
+ # -----------------------
305
+ with st.sidebar:
306
+ st.markdown("### Settings")
307
+ st.session_state.theme = st.radio("Theme", ["Light", "Dark"], horizontal=True, index=0 if st.session_state.theme == "Light" else 1)
308
+
309
+ system_prompt = st.text_area("System prompt", value=DEFAULT_SYSTEM_PROMPT, height=160)
310
+ use_web_search = st.checkbox("Use web search", value=True)
311
 
312
+ # Re-inject after theme changes (Streamlit reruns)
313
+ inject_css(LIGHT_THEME if st.session_state.theme == "Light" else DARK_THEME)
314
 
315
+ # -----------------------
316
+ # Header
317
+ # -----------------------
318
+ c1, c2 = st.columns([1, 4], vertical_alignment="center")
319
+ with c1:
320
+ st_image_full_width(TEAPOT_LOGO_GIF)
321
+ with c2:
322
+ st.markdown("## TeapotAI Chat")
323
+ st.caption("A lightweight, grounded chat experience.")
324
 
325
+ # Load tiny model on startup (your earlier requirement) and use it (no model toggle in UI now per your request)
326
+ _ = load_teapot_ai(DEFAULT_MODEL)
327
+ teapot_ai = load_teapot_ai(DEFAULT_MODEL)
328
 
329
 
330
  # -----------------------
331
+ # Suggested searches (shown only when chat is empty)
332
  # -----------------------
333
+ if len(st.session_state.messages) == 0 and st.session_state.pending_query is None:
334
+ st.markdown("#### Try one of these")
335
+ cols = st.columns(3)
336
+ for i, q in enumerate(SUGGESTED_QUERIES):
337
+ with cols[i]:
338
+ if st.button(q, key=f"suggest_{i}"):
339
+ st.session_state.pending_query = q
340
+ st.rerun()
341
 
 
 
 
342
 
343
+ # -----------------------
344
+ # Render chat history
345
+ # -----------------------
346
+ for idx, m in enumerate(st.session_state.messages):
347
+ if m["role"] == "user":
348
+ with st.chat_message("user"):
349
+ st.markdown(f'<div class="bubble-user">{m["content"]}</div>', unsafe_allow_html=True)
350
+ else:
351
+ with st.chat_message("assistant"):
352
+ st.markdown(f'<div class="bubble-asst">{m["content"]}</div>', unsafe_allow_html=True)
353
+
354
+ # Info icon → expands sources/context + links
355
+ sources = m.get("sources") or []
356
+ context = m.get("context") or ""
357
+
358
+ if sources or context:
359
+ # Prefer popover if available, else expander
360
+ try:
361
+ with st.popover("ℹ️", use_container_width=False):
362
+ st.markdown("**Sources**")
363
+ if sources:
364
+ for j, s in enumerate(sources, start=1):
365
+ title = s.get("title", "").strip() or f"Result {j}"
366
+ url = s.get("url", "").strip()
367
+ snippet = s.get("snippet", "").strip()
368
+ if url:
369
+ st.markdown(f"- [{title}]({url})")
370
+ else:
371
+ st.markdown(f"- {title}")
372
+ if snippet:
373
+ st.caption(snippet)
374
+ else:
375
+ st.caption("(No sources returned.)")
376
+
377
+ st.markdown("**Full context**")
378
+ if context.strip():
379
+ st.code(context)
380
+ else:
381
+ st.caption("(Empty context.)")
382
+ except Exception:
383
+ with st.expander("ℹ️ Sources / Context"):
384
+ st.markdown("**Sources**")
385
+ if sources:
386
+ for j, s in enumerate(sources, start=1):
387
+ title = s.get("title", "").strip() or f"Result {j}"
388
+ url = s.get("url", "").strip()
389
+ snippet = s.get("snippet", "").strip()
390
+ if url:
391
+ st.markdown(f"- [{title}]({url})")
392
+ else:
393
+ st.markdown(f"- {title}")
394
+ if snippet:
395
+ st.caption(snippet)
396
+ else:
397
+ st.caption("(No sources returned.)")
398
+
399
+ st.markdown("**Full context**")
400
+ if context.strip():
401
+ st.code(context)
402
+ else:
403
+ st.caption("(Empty context.)")
404
 
 
 
 
 
 
 
 
405
 
406
+ # -----------------------
407
+ # Input handling
408
+ # -----------------------
409
+ user_input = st.chat_input("Ask a question…")
410
+
411
+ if st.session_state.pending_query and not user_input:
412
+ user_input = st.session_state.pending_query
413
+ st.session_state.pending_query = None
414
+
415
+ if user_input:
416
+ # Add user message
417
+ st.session_state.messages.append({"role": "user", "content": user_input})
418
+
419
+ # Build context (optional web search)
420
+ results: List[Dict[str, str]] = []
421
+ context = ""
422
+
423
+ if use_web_search:
424
+ try:
425
+ results = brave_search_snippets(user_input, top_k=TOP_K)
426
+ context = format_context_from_results(results)
427
+ except Exception:
428
+ # If web search fails, keep context empty so system prompt can enforce refusal behavior
429
+ results = []
430
+ context = ""
431
+
432
+ # Query TeapotAI
433
  answer = teapot_ai.query(
434
+ query=user_input,
435
  context=context,
436
  system_prompt=system_prompt,
437
  )
438
 
439
+ # Append assistant message with metadata for info popover
440
+ st.session_state.messages.append(
441
+ {
442
+ "role": "assistant",
443
+ "content": answer,
444
+ "sources": results,
445
+ "context": context,
446
+ }
447
+ )
448
+
449
+ st.rerun()