Faraz618 commited on
Commit
8d4686d
Β·
verified Β·
1 Parent(s): 3ded79f

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +104 -126
app.py CHANGED
@@ -1,12 +1,8 @@
1
  """
2
- app.py β€” Enterprise RAG System β€” Gradio UI entry point.
3
 
4
- This is the file Hugging Face Spaces looks for first.
5
- It wires together all modules and builds the Gradio interface.
6
-
7
- Architecture: stateful single-user session using Gradio's gr.State()
8
- In production multi-user deployment, each session would have isolated
9
- vector store state (Redis or per-session FAISS index in memory).
10
  """
11
 
12
  import os
@@ -21,138 +17,129 @@ from src.generation import generate_answer
21
  from src.evaluation import run_evaluation
22
  from src.observability import trace_rag_query, get_observability_status
23
  from src.metrics import record_query_metrics, get_metrics_summary
24
- from src.utils import format_retrieved_chunks, count_tokens_estimate
25
 
26
  logging.basicConfig(level=logging.INFO)
27
  logger = logging.getLogger("enterprise-rag.app")
28
 
 
29
  # ─────────────────────────────────────────────────────────────────────────────
30
- # DOCUMENT PROCESSING
31
  # ─────────────────────────────────────────────────────────────────────────────
32
 
33
  def process_pdf(pdf_file, chunk_size: int, chunk_overlap: int):
34
  """
35
  Full ingestion pipeline: PDF β†’ text β†’ chunks β†’ embeddings β†’ FAISS index.
36
 
37
- Returns:
38
- status_message: shown in the UI
39
- chunks_state: stored in gr.State for use during queries
40
- index_state: FAISS index stored in gr.State
41
  """
42
  if pdf_file is None:
43
- return "⚠️ Please upload a PDF file.", None, None, "No document loaded."
 
 
 
 
 
44
 
45
  try:
46
- # Read file bytes
47
- with open(pdf_file.name, "rb") as f:
 
 
48
  file_bytes = f.read()
49
 
50
- # Size validation
51
  valid, size_msg = validate_pdf(file_bytes)
52
  if not valid:
53
- return f"❌ {size_msg}", None, None, "No document loaded."
54
 
55
- # Step 1: Extract text
56
  extraction = extract_text_from_pdf(file_bytes)
57
  if not extraction["success"]:
58
- return f"❌ {extraction['error']}", None, None, "Extraction failed."
59
 
60
  doc_text = extraction["text"]
61
  page_count = extraction["page_count"]
62
 
63
- # Step 2: Chunk
64
  chunks = chunk_text(doc_text, int(chunk_size), int(chunk_overlap))
65
  if not chunks:
66
- return "❌ No chunks created. Document may be too short.", None, None, ""
67
 
68
  stats = chunk_statistics(chunks)
69
  chunk_texts = [c["text"] for c in chunks]
70
 
71
- # Step 3: Embed
72
  embeddings = embed_texts(chunk_texts)
73
 
74
- # Step 4: Build FAISS index
75
  faiss_index = build_faiss_index(embeddings)
76
 
77
  status = (
78
  f"βœ… Document processed successfully!\n\n"
79
  f"πŸ“„ Pages: {page_count}\n"
80
  f"πŸ“¦ Chunks: {stats['count']}\n"
81
- f"πŸ“Š Avg chunk size: {stats['avg_tokens']} tokens\n"
82
  f"πŸ”’ Total tokens: {stats['total_tokens']}\n\n"
83
  f"Ready to answer questions."
84
  )
85
 
86
  doc_info = (
87
- f"**Document loaded:** {os.path.basename(pdf_file.name)}\n"
88
  f"Pages: {page_count} | Chunks: {stats['count']} | "
89
  f"Avg chunk: {stats['avg_tokens']} tokens"
90
  )
91
 
92
- logger.info(f"PDF processed: {stats['count']} chunks, {stats['total_tokens']} total tokens")
 
 
 
93
  return status, chunks, faiss_index, doc_info
94
 
95
  except Exception as e:
96
  logger.error(f"PDF processing failed: {e}")
97
- return f"❌ Processing error: {str(e)}", None, None, "Error"
98
 
99
 
100
- # ───────���─────────────────────────────────────────────────────────────────────
101
- # QUERY PIPELINE
102
- # ─────────────────────────────────────────────────────────────────────────────
103
-
104
- def answer_question(
105
- query: str,
106
- chunks_state,
107
- index_state,
108
- top_k: int,
109
- ):
110
  """
111
- Full RAG query pipeline: query β†’ embed β†’ retrieve β†’ generate β†’ evaluate β†’ trace.
112
-
113
- Returns:
114
- answer_text: shown in the center panel
115
- chunks_display: retrieved chunks for the right panel
116
- metrics_display: latency/token metrics for the right panel
117
- eval_display: evaluation scores for the right panel
118
- obs_display: observability status
119
  """
120
- empty_right = ("", "", "", "")
121
-
122
  if not query or not query.strip():
123
- return "⚠️ Please enter a question.", *empty_right
124
 
125
  if chunks_state is None or index_state is None:
126
  return (
127
  "⚠️ No document loaded. Please upload a PDF first.",
128
- *empty_right,
129
  )
130
 
 
131
  chunk_texts = [
132
- c["text"] if isinstance(c, dict) else c for c in chunks_state
 
133
  ]
134
 
135
  try:
136
- # Step 1: Retrieve relevant chunks
137
  retrieval = retrieve_relevant_chunks(
138
  query=query,
139
  chunks=chunk_texts,
140
  faiss_index=index_state,
141
  top_k=int(top_k),
142
  )
143
-
144
  retrieved_chunks = retrieval["retrieved_chunks"]
145
  scores = retrieval["scores"]
146
  is_relevant = retrieval["is_relevant"]
147
 
148
- # Step 2: Generate grounded answer
149
  generation = generate_answer(
150
  query=query,
151
  context_chunks=retrieved_chunks,
152
  scores=scores,
153
  is_relevant=is_relevant,
154
  )
155
-
156
  answer = generation["answer"]
157
  prompt_tokens = generation["prompt_tokens"]
158
  response_tokens = generation["response_tokens"]
@@ -160,7 +147,7 @@ def answer_question(
160
  model_used = generation.get("model_used", "unknown")
161
  fallback_used = generation["fallback_used"]
162
 
163
- # Step 3: Evaluate
164
  eval_scores = run_evaluation(
165
  query=query,
166
  answer=answer,
@@ -168,7 +155,7 @@ def answer_question(
168
  retrieval_scores=scores,
169
  )
170
 
171
- # Step 4: Record metrics
172
  record_query_metrics(
173
  retrieval_latency_ms=retrieval["retrieval_latency_ms"],
174
  generation_latency_ms=gen_latency,
@@ -178,7 +165,7 @@ def answer_question(
178
  fallback_used=fallback_used,
179
  )
180
 
181
- # Step 5: Trace to Langfuse + local log
182
  trace_rag_query(
183
  query=query,
184
  answer=answer,
@@ -193,35 +180,30 @@ def answer_question(
193
  fallback_used=fallback_used,
194
  )
195
 
196
- # ── Format outputs for Gradio panels ──────────────────────────────
197
 
198
- # Retrieved chunks panel
199
- retrieval_warning = f"\n\n⚠️ {retrieval['warning']}" if retrieval.get("warning") else ""
200
- chunks_display = (
201
- format_retrieved_chunks(retrieved_chunks, scores) + retrieval_warning
202
- )
203
 
204
- # Metrics panel
205
  metrics_display = get_metrics_summary()
206
 
207
- # Evaluation panel
208
- eval_display = f"""**πŸ§ͺ Answer Quality Scores**
209
- - Faithfulness: `{eval_scores['faithfulness']:.3f}` *(grounded in context)*
210
- - Answer Relevance: `{eval_scores['answer_relevance']:.3f}` *(answers the question)*
211
- - Context Precision: `{eval_scores['context_precision']:.3f}` *(retrieval quality)*
212
- - **Overall: `{eval_scores['overall']:.3f}`** β€” {eval_scores['quality_label']}
213
-
214
- {eval_scores.get('note', '')}"""
215
 
216
- # Observability panel
217
  obs_display = (
218
  f"{get_observability_status()}\n\n"
219
- f"**Last trace:**\n"
220
- f"β€’ Model: `{model_used}`\n"
221
- f"β€’ Retrieval: `{retrieval['retrieval_latency_ms']:.0f}ms`\n"
222
- f"β€’ Generation: `{gen_latency:.0f}ms`\n"
223
- f"β€’ Tokens: `{prompt_tokens + response_tokens}`\n"
224
- f"β€’ Fallback: `{'Yes' if fallback_used else 'No'}`"
225
  )
226
 
227
  return answer, chunks_display, metrics_display, eval_display, obs_display
@@ -232,65 +214,64 @@ def answer_question(
232
 
233
 
234
  # ─────────────────────────────────────────────────────────────────────────────
235
- # GRADIO UI
236
  # ─────────────────────────────────────────────────────────────────────────────
237
 
238
- CSS = """
239
- .panel-header { font-size: 13px; font-weight: 600; color: #666; }
240
- .answer-box textarea { font-size: 15px !important; line-height: 1.7 !important; }
241
- footer { display: none !important; }
242
- """
243
-
244
  with gr.Blocks(
245
  title="Enterprise RAG System",
246
  theme=gr.themes.Soft(primary_hue="blue"),
247
- css=CSS,
248
  ) as demo:
249
 
250
- # ── Session state (per-user, isolated) ──────────────────────────────────
251
  chunks_state = gr.State(None)
252
  index_state = gr.State(None)
253
 
254
- # ── Header ───────────────────────────────────────────────────────────────
255
  gr.Markdown(
256
- """# 🏒 Enterprise Knowledge Retrieval System
257
- **RAG pipeline with evaluation & observability** | Mistral-7B Β· FAISS Β· Sentence-Transformers
258
- """,
259
  )
260
 
261
  with gr.Row():
262
 
263
- # ── LEFT PANEL: Document Upload ───────────────────────────────────
264
- with gr.Column(scale=1, min_width=280):
265
- gr.Markdown("### πŸ“ Document Upload", elem_classes="panel-header")
266
 
267
  pdf_input = gr.File(
268
  label="Upload PDF",
269
  file_types=[".pdf"],
270
- type="filepath",
271
  )
272
 
273
  with gr.Accordion("βš™οΈ Chunking Settings", open=False):
274
  chunk_size_slider = gr.Slider(
275
- minimum=128, maximum=1024, value=512, step=64,
 
 
 
276
  label="Chunk Size (tokens)",
277
- info="Larger = more context per chunk, but noisier embeddings",
278
  )
279
  chunk_overlap_slider = gr.Slider(
280
- minimum=0, maximum=256, value=64, step=32,
 
 
 
281
  label="Chunk Overlap (tokens)",
282
- info="Overlap prevents answer loss at chunk boundaries",
283
  )
284
  top_k_slider = gr.Slider(
285
- minimum=1, maximum=10, value=5, step=1,
 
 
 
286
  label="Top-K Retrieval",
287
- info="Number of chunks to retrieve per query",
288
  )
289
 
290
  process_btn = gr.Button("πŸ“₯ Process Document", variant="primary")
291
 
292
  doc_status = gr.Textbox(
293
- label="Processing Status",
294
  lines=6,
295
  interactive=False,
296
  value="No document loaded.",
@@ -298,9 +279,9 @@ with gr.Blocks(
298
 
299
  doc_info_md = gr.Markdown("")
300
 
301
- # ── CENTER PANEL: Query & Answer ─────────────────────────────────
302
- with gr.Column(scale=2, min_width=400):
303
- gr.Markdown("### πŸ’¬ Query Interface", elem_classes="panel-header")
304
 
305
  query_input = gr.Textbox(
306
  label="Your Question",
@@ -311,46 +292,44 @@ with gr.Blocks(
311
  ask_btn = gr.Button("πŸ” Get Answer", variant="primary", size="lg")
312
 
313
  answer_output = gr.Markdown(
314
- label="AI Answer",
315
- value="*Upload a document and ask a question to get started.*",
316
- elem_classes="answer-box",
317
  )
318
 
319
  gr.Markdown(
320
- """---
321
- **Sample Questions** (after uploading a relevant document):
322
- - *What are the main topics covered in this document?*
323
- - *Summarize the key findings.*
324
- - *What does the document say about [specific topic]?*
325
- """
326
  )
327
 
328
- # ── RIGHT PANEL: Retrieval + Metrics + Evaluation ─────────────────
329
- with gr.Column(scale=1, min_width=300):
330
- gr.Markdown("### πŸ“Š Observability Panel", elem_classes="panel-header")
331
 
332
  with gr.Tabs():
333
- with gr.Tab("πŸ“„ Retrieved Chunks"):
334
  chunks_output = gr.Markdown(
335
- value="*Retrieved context will appear here after a query.*"
336
  )
337
 
338
  with gr.Tab("πŸ“ˆ Metrics"):
339
  metrics_output = gr.Markdown(
340
- value="*Metrics will appear after the first query.*"
341
  )
342
 
343
  with gr.Tab("πŸ§ͺ Evaluation"):
344
  eval_output = gr.Markdown(
345
- value="*Evaluation scores will appear after a query.*"
346
  )
347
 
348
  with gr.Tab("πŸ”­ Traces"):
349
  obs_output = gr.Markdown(
350
- value=f"{get_observability_status()}"
351
  )
352
 
353
- # ── Event Handlers ───────────────────────────────────────────────────────
354
 
355
  process_btn.click(
356
  fn=process_pdf,
@@ -364,7 +343,6 @@ with gr.Blocks(
364
  outputs=[answer_output, chunks_output, metrics_output, eval_output, obs_output],
365
  )
366
 
367
- # Allow pressing Enter in the query box
368
  query_input.submit(
369
  fn=answer_question,
370
  inputs=[query_input, chunks_state, index_state, top_k_slider],
 
1
  """
2
+ app.py β€” Enterprise RAG System β€” Gradio 5 entry point.
3
 
4
+ Wires together all pipeline modules and renders the three-panel UI.
5
+ Uses gr.State() for per-session document isolation.
 
 
 
 
6
  """
7
 
8
  import os
 
17
  from src.evaluation import run_evaluation
18
  from src.observability import trace_rag_query, get_observability_status
19
  from src.metrics import record_query_metrics, get_metrics_summary
20
+ from src.utils import format_retrieved_chunks
21
 
22
  logging.basicConfig(level=logging.INFO)
23
  logger = logging.getLogger("enterprise-rag.app")
24
 
25
+
26
  # ─────────────────────────────────────────────────────────────────────────────
27
+ # PIPELINE FUNCTIONS
28
  # ─────────────────────────────────────────────────────────────────────────────
29
 
30
  def process_pdf(pdf_file, chunk_size: int, chunk_overlap: int):
31
  """
32
  Full ingestion pipeline: PDF β†’ text β†’ chunks β†’ embeddings β†’ FAISS index.
33
 
34
+ Gradio 5 passes uploaded files as a file path string directly.
 
 
 
35
  """
36
  if pdf_file is None:
37
+ return (
38
+ "⚠️ Please upload a PDF file first.",
39
+ None,
40
+ None,
41
+ "",
42
+ )
43
 
44
  try:
45
+ # Gradio 5: pdf_file is a filepath string
46
+ file_path = pdf_file if isinstance(pdf_file, str) else pdf_file.name
47
+
48
+ with open(file_path, "rb") as f:
49
  file_bytes = f.read()
50
 
51
+ # Validate size
52
  valid, size_msg = validate_pdf(file_bytes)
53
  if not valid:
54
+ return f"❌ {size_msg}", None, None, ""
55
 
56
+ # Step 1 β€” Extract text
57
  extraction = extract_text_from_pdf(file_bytes)
58
  if not extraction["success"]:
59
+ return f"❌ {extraction['error']}", None, None, ""
60
 
61
  doc_text = extraction["text"]
62
  page_count = extraction["page_count"]
63
 
64
+ # Step 2 β€” Chunk
65
  chunks = chunk_text(doc_text, int(chunk_size), int(chunk_overlap))
66
  if not chunks:
67
+ return "❌ No chunks created. Document may be too short or empty.", None, None, ""
68
 
69
  stats = chunk_statistics(chunks)
70
  chunk_texts = [c["text"] for c in chunks]
71
 
72
+ # Step 3 β€” Embed
73
  embeddings = embed_texts(chunk_texts)
74
 
75
+ # Step 4 β€” Build FAISS index
76
  faiss_index = build_faiss_index(embeddings)
77
 
78
  status = (
79
  f"βœ… Document processed successfully!\n\n"
80
  f"πŸ“„ Pages: {page_count}\n"
81
  f"πŸ“¦ Chunks: {stats['count']}\n"
82
+ f"πŸ“Š Avg chunk: {stats['avg_tokens']} tokens\n"
83
  f"πŸ”’ Total tokens: {stats['total_tokens']}\n\n"
84
  f"Ready to answer questions."
85
  )
86
 
87
  doc_info = (
88
+ f"**Loaded:** `{os.path.basename(file_path)}` \n"
89
  f"Pages: {page_count} | Chunks: {stats['count']} | "
90
  f"Avg chunk: {stats['avg_tokens']} tokens"
91
  )
92
 
93
+ logger.info(
94
+ f"PDF ready: {stats['count']} chunks, "
95
+ f"{stats['total_tokens']} total tokens"
96
+ )
97
  return status, chunks, faiss_index, doc_info
98
 
99
  except Exception as e:
100
  logger.error(f"PDF processing failed: {e}")
101
+ return f"❌ Error: {str(e)}", None, None, ""
102
 
103
 
104
+ def answer_question(query: str, chunks_state, index_state, top_k: int):
 
 
 
 
 
 
 
 
 
105
  """
106
+ Full RAG query pipeline:
107
+ embed query β†’ retrieve β†’ generate β†’ evaluate β†’ trace β†’ display.
 
 
 
 
 
 
108
  """
 
 
109
  if not query or not query.strip():
110
+ return "⚠️ Please enter a question.", "", "", "", ""
111
 
112
  if chunks_state is None or index_state is None:
113
  return (
114
  "⚠️ No document loaded. Please upload a PDF first.",
115
+ "", "", "", "",
116
  )
117
 
118
+ # Normalize chunks to plain text strings
119
  chunk_texts = [
120
+ c["text"] if isinstance(c, dict) else c
121
+ for c in chunks_state
122
  ]
123
 
124
  try:
125
+ # Step 1 β€” Retrieve
126
  retrieval = retrieve_relevant_chunks(
127
  query=query,
128
  chunks=chunk_texts,
129
  faiss_index=index_state,
130
  top_k=int(top_k),
131
  )
 
132
  retrieved_chunks = retrieval["retrieved_chunks"]
133
  scores = retrieval["scores"]
134
  is_relevant = retrieval["is_relevant"]
135
 
136
+ # Step 2 β€” Generate
137
  generation = generate_answer(
138
  query=query,
139
  context_chunks=retrieved_chunks,
140
  scores=scores,
141
  is_relevant=is_relevant,
142
  )
 
143
  answer = generation["answer"]
144
  prompt_tokens = generation["prompt_tokens"]
145
  response_tokens = generation["response_tokens"]
 
147
  model_used = generation.get("model_used", "unknown")
148
  fallback_used = generation["fallback_used"]
149
 
150
+ # Step 3 β€” Evaluate
151
  eval_scores = run_evaluation(
152
  query=query,
153
  answer=answer,
 
155
  retrieval_scores=scores,
156
  )
157
 
158
+ # Step 4 β€” Record metrics
159
  record_query_metrics(
160
  retrieval_latency_ms=retrieval["retrieval_latency_ms"],
161
  generation_latency_ms=gen_latency,
 
165
  fallback_used=fallback_used,
166
  )
167
 
168
+ # Step 5 β€” Trace
169
  trace_rag_query(
170
  query=query,
171
  answer=answer,
 
180
  fallback_used=fallback_used,
181
  )
182
 
183
+ # ── Format right-panel outputs ─────────────────────────────────────
184
 
185
+ warning_text = f"\n\n⚠️ {retrieval['warning']}" if retrieval.get("warning") else ""
186
+ chunks_display = format_retrieved_chunks(retrieved_chunks, scores) + warning_text
 
 
 
187
 
 
188
  metrics_display = get_metrics_summary()
189
 
190
+ eval_display = (
191
+ f"**Answer Quality Scores**\n\n"
192
+ f"- Faithfulness: `{eval_scores['faithfulness']:.3f}` β€” grounded in context\n"
193
+ f"- Answer Relevance: `{eval_scores['answer_relevance']:.3f}` β€” answers the question\n"
194
+ f"- Context Precision: `{eval_scores['context_precision']:.3f}` β€” retrieval quality\n"
195
+ f"- **Overall: `{eval_scores['overall']:.3f}`** {eval_scores['quality_label']}\n\n"
196
+ f"{eval_scores.get('note', '')}"
197
+ )
198
 
 
199
  obs_display = (
200
  f"{get_observability_status()}\n\n"
201
+ f"**Last trace**\n"
202
+ f"- Model: `{model_used}`\n"
203
+ f"- Retrieval: `{retrieval['retrieval_latency_ms']:.0f}ms`\n"
204
+ f"- Generation: `{gen_latency:.0f}ms`\n"
205
+ f"- Total tokens: `{prompt_tokens + response_tokens}`\n"
206
+ f"- Fallback used: `{'Yes' if fallback_used else 'No'}`"
207
  )
208
 
209
  return answer, chunks_display, metrics_display, eval_display, obs_display
 
214
 
215
 
216
  # ─────────────────────────────────────────────────────────────────────────────
217
+ # GRADIO 5 UI
218
  # ─────────────────────────────────────────────────────────────────────────────
219
 
 
 
 
 
 
 
220
  with gr.Blocks(
221
  title="Enterprise RAG System",
222
  theme=gr.themes.Soft(primary_hue="blue"),
 
223
  ) as demo:
224
 
225
+ # Per-session state β€” each user gets isolated chunks and FAISS index
226
  chunks_state = gr.State(None)
227
  index_state = gr.State(None)
228
 
 
229
  gr.Markdown(
230
+ "# 🏒 Enterprise Knowledge Retrieval System\n"
231
+ "**RAG pipeline Β· Groq LLM Β· FAISS Β· Evaluation Β· Observability**"
 
232
  )
233
 
234
  with gr.Row():
235
 
236
+ # ── LEFT: Document Upload ─────────────────────────────────────────
237
+ with gr.Column(scale=1, min_width=260):
238
+ gr.Markdown("### πŸ“ Document Upload")
239
 
240
  pdf_input = gr.File(
241
  label="Upload PDF",
242
  file_types=[".pdf"],
 
243
  )
244
 
245
  with gr.Accordion("βš™οΈ Chunking Settings", open=False):
246
  chunk_size_slider = gr.Slider(
247
+ minimum=128,
248
+ maximum=1024,
249
+ value=512,
250
+ step=64,
251
  label="Chunk Size (tokens)",
252
+ info="Larger = more context per chunk",
253
  )
254
  chunk_overlap_slider = gr.Slider(
255
+ minimum=0,
256
+ maximum=256,
257
+ value=64,
258
+ step=32,
259
  label="Chunk Overlap (tokens)",
260
+ info="Prevents answer loss at boundaries",
261
  )
262
  top_k_slider = gr.Slider(
263
+ minimum=1,
264
+ maximum=10,
265
+ value=5,
266
+ step=1,
267
  label="Top-K Retrieval",
268
+ info="Chunks returned per query",
269
  )
270
 
271
  process_btn = gr.Button("πŸ“₯ Process Document", variant="primary")
272
 
273
  doc_status = gr.Textbox(
274
+ label="Status",
275
  lines=6,
276
  interactive=False,
277
  value="No document loaded.",
 
279
 
280
  doc_info_md = gr.Markdown("")
281
 
282
+ # ── CENTER: Query & Answer ────────────────────────────────────────
283
+ with gr.Column(scale=2, min_width=380):
284
+ gr.Markdown("### πŸ’¬ Ask Questions")
285
 
286
  query_input = gr.Textbox(
287
  label="Your Question",
 
292
  ask_btn = gr.Button("πŸ” Get Answer", variant="primary", size="lg")
293
 
294
  answer_output = gr.Markdown(
295
+ value="*Upload a document and ask a question to get started.*"
 
 
296
  )
297
 
298
  gr.Markdown(
299
+ "---\n"
300
+ "**Example questions after uploading:**\n"
301
+ "- What are the main topics covered?\n"
302
+ "- Summarize the key findings.\n"
303
+ "- What risks or challenges are mentioned?\n"
304
+ "- What are the specific numbers or statistics?"
305
  )
306
 
307
+ # ── RIGHT: Observability Panel ────────────────────────────────────
308
+ with gr.Column(scale=1, min_width=280):
309
+ gr.Markdown("### πŸ“Š Observability")
310
 
311
  with gr.Tabs():
312
+ with gr.Tab("πŸ“„ Chunks"):
313
  chunks_output = gr.Markdown(
314
+ value="*Retrieved context appears here after a query.*"
315
  )
316
 
317
  with gr.Tab("πŸ“ˆ Metrics"):
318
  metrics_output = gr.Markdown(
319
+ value="*Metrics appear after the first query.*"
320
  )
321
 
322
  with gr.Tab("πŸ§ͺ Evaluation"):
323
  eval_output = gr.Markdown(
324
+ value="*Evaluation scores appear after a query.*"
325
  )
326
 
327
  with gr.Tab("πŸ”­ Traces"):
328
  obs_output = gr.Markdown(
329
+ value=get_observability_status()
330
  )
331
 
332
+ # ── Event handlers ────────────────────────────────────────────────────
333
 
334
  process_btn.click(
335
  fn=process_pdf,
 
343
  outputs=[answer_output, chunks_output, metrics_output, eval_output, obs_output],
344
  )
345
 
 
346
  query_input.submit(
347
  fn=answer_question,
348
  inputs=[query_input, chunks_state, index_state, top_k_slider],