Hebaelsayed commited on
Commit
3ebdd9a
Β·
verified Β·
1 Parent(s): f41b68e

Update src/streamlit_app.py

Browse files
Files changed (1) hide show
  1. src/streamlit_app.py +504 -560
src/streamlit_app.py CHANGED
@@ -1,681 +1,625 @@
1
  import streamlit as st
2
  import os
3
  import time
 
 
 
 
 
4
  from qdrant_client import QdrantClient
5
  from qdrant_client.models import Distance, VectorParams, PointStruct
6
  from sentence_transformers import SentenceTransformer
7
- import PyPDF2
8
- import io
9
 
10
  # ============================================================================
11
- # CONFIGURATION
12
  # ============================================================================
13
 
14
  st.set_page_config(
15
- page_title="Math AI - Phase 2.5: Database + PDF",
16
- page_icon="πŸ—„οΈ",
17
- layout="wide"
 
18
  )
19
 
20
  COLLECTION_NAME = "math_knowledge_base"
21
 
22
  # ============================================================================
23
- # CACHED FUNCTIONS
24
  # ============================================================================
25
 
26
- @st.cache_resource(show_spinner="πŸ”Œ Connecting to Qdrant...")
27
- def get_qdrant_client():
28
- qdrant_url = os.getenv("QDRANT_URL")
29
- qdrant_api_key = os.getenv("QDRANT_API_KEY")
 
 
 
30
 
31
- if not qdrant_url or not qdrant_api_key:
32
- return None
 
33
 
34
- return QdrantClient(url=qdrant_url, api_key=qdrant_api_key)
35
 
36
- @st.cache_resource(show_spinner="πŸ€– Loading embedding model (30-60s first time)...")
37
- def get_embedding_model():
 
 
 
 
38
  try:
39
- model = SentenceTransformer('sentence-transformers/all-MiniLM-L6-v2')
40
- return model
 
 
 
41
  except Exception as e:
42
- st.error(f"Failed to load model: {e}")
43
  return None
44
 
45
- def get_vector_count_reliable(client, collection_name):
 
 
 
 
 
 
 
 
 
 
 
46
  try:
47
  count = 0
48
  offset = None
49
- max_iterations = 1000
50
-
51
- for _ in range(max_iterations):
52
- result = client.scroll(
53
- collection_name=collection_name,
54
  limit=100,
55
  offset=offset,
56
  with_payload=False,
57
  with_vectors=False
58
  )
59
-
60
- if result is None or result[0] is None or len(result[0]) == 0:
61
  break
62
-
63
  count += len(result[0])
64
  offset = result[1]
65
-
66
  if offset is None:
67
  break
68
-
69
  return count
70
  except:
71
  return 0
72
 
73
- def check_collection_exists(client, collection_name):
74
- try:
75
- collections = client.get_collections().collections
76
- return any(c.name == collection_name for c in collections)
77
- except:
78
- return False
79
-
80
- def extract_text_from_pdf(pdf_file):
81
- """Extract text from PDF file"""
82
- try:
83
- pdf_reader = PyPDF2.PdfReader(pdf_file)
84
- text = ""
85
-
86
- for page_num, page in enumerate(pdf_reader.pages):
87
- page_text = page.extract_text()
88
- text += f"\n\n--- Page {page_num + 1} ---\n\n{page_text}"
89
-
90
- return text
91
- except Exception as e:
92
- st.error(f"PDF extraction error: {str(e)}")
93
- return None
94
-
95
  # ============================================================================
96
- # SESSION STATE
97
  # ============================================================================
98
 
99
- if 'db_created' not in st.session_state:
100
- st.session_state.db_created = False
101
-
102
- if 'embedder_ready' not in st.session_state:
103
- st.session_state.embedder_ready = False
104
-
105
- if 'show_step' not in st.session_state:
106
- st.session_state.show_step = 'all'
107
 
108
  # ============================================================================
109
- # MAIN APP
110
  # ============================================================================
111
 
112
- st.title("πŸ—„οΈ Phase 2.5: Database Setup + PDF Upload")
113
 
114
- client = get_qdrant_client()
115
- embedder = get_embedding_model()
 
 
 
116
 
117
- # ============================================================================
118
- # SIDEBAR
119
- # ============================================================================
120
 
121
- with st.sidebar:
122
- st.header("⚑ Quick Navigation")
123
-
124
- if st.button("πŸ“‹ Show All Steps", use_container_width=True):
125
- st.session_state.show_step = 'all'
126
-
127
- if st.button("πŸš€ Skip to Upload", use_container_width=True):
128
- st.session_state.show_step = 'upload'
129
-
130
- if st.button("πŸ” Skip to Search", use_container_width=True):
131
- st.session_state.show_step = 'search'
132
-
133
- st.markdown("---")
134
- st.subheader("πŸ“Š System Status")
135
 
136
- if client and check_collection_exists(client, COLLECTION_NAME):
137
- st.success("βœ… Database Ready")
138
- st.session_state.db_created = True
139
- else:
140
- st.warning("⚠️ Database Not Ready")
141
-
142
- if embedder:
143
- st.success("βœ… Model Loaded")
144
- st.session_state.embedder_ready = True
145
- else:
146
- st.warning("⚠️ Model Not Loaded")
147
-
148
- if client and st.session_state.db_created:
149
- count = get_vector_count_reliable(client, COLLECTION_NAME)
150
- st.metric("Vectors in DB", f"{count:,}")
151
-
152
- show_all = st.session_state.show_step == 'all'
153
- show_upload = st.session_state.show_step in ['all', 'upload']
154
- show_search = st.session_state.show_step in ['all', 'search']
155
 
156
  # ============================================================================
157
- # STEP 1-2: Quick Status
158
  # ============================================================================
159
 
160
- if show_all:
161
- st.header("Step 1-2: System Check")
162
 
163
- col1, col2, col3 = st.columns(3)
 
164
 
165
- with col1:
166
- st.metric("Claude API", "βœ…" if os.getenv("ANTHROPIC_API_KEY") else "❌")
 
167
 
168
- with col2:
169
- st.metric("Qdrant", "βœ… Connected" if client else "❌")
170
 
171
- with col3:
172
- st.metric("Embedder", "βœ… Cached" if embedder else "❌")
 
 
 
173
 
174
- if not client:
175
- st.error("⚠️ Check Qdrant secrets!")
176
- st.stop()
177
 
178
- st.markdown("---")
179
-
180
- # ============================================================================
181
- # STEP 3: Collection Management
182
- # ============================================================================
183
-
184
- if show_all:
185
- st.header("πŸ—οΈ Step 3: Database Collection")
186
 
187
- if st.session_state.db_created:
188
- st.success(f"βœ… Collection '{COLLECTION_NAME}' ready!")
189
-
 
 
 
 
 
 
 
 
 
 
190
  col1, col2 = st.columns(2)
 
191
  with col1:
192
- if st.button("πŸ”„ Recreate Collection"):
193
- try:
194
- client.delete_collection(COLLECTION_NAME)
195
- st.session_state.db_created = False
196
- st.rerun()
197
- except Exception as e:
198
- st.error(f"Error: {e}")
199
 
200
  with col2:
201
- if st.button("ℹ️ Collection Info"):
202
- count = get_vector_count_reliable(client, COLLECTION_NAME)
203
- st.json({"name": COLLECTION_NAME, "vectors": count, "status": "Ready"})
 
 
 
204
 
205
- else:
206
- if st.button("πŸ—οΈ CREATE COLLECTION", type="primary"):
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
207
  try:
208
- client.create_collection(
209
  collection_name=COLLECTION_NAME,
210
- vectors_config=VectorParams(size=384, distance=Distance.COSINE)
 
211
  )
212
- st.success(f"πŸŽ‰ Created: {COLLECTION_NAME}")
213
- st.session_state.db_created = True
214
- st.rerun()
215
  except Exception as e:
216
- st.error(f"❌ Failed: {str(e)}")
217
-
218
- st.markdown("---")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
219
 
220
- # ============================================================================
221
- # STEP 4: Embedding Model
222
- # ============================================================================
223
 
224
- if show_all:
225
- st.header("πŸ€– Step 4: Embedding Model")
226
-
227
- if embedder:
228
- st.success("βœ… Model loaded and cached!")
229
- st.session_state.embedder_ready = True
230
- else:
231
- st.warning("⚠️ Model loading failed. Refresh page.")
232
-
233
- st.markdown("---")
234
 
235
- # ============================================================================
236
- # STEP 5A: Upload Custom Text
237
- # ============================================================================
238
 
239
- if show_upload:
240
- st.header("πŸ“ Step 5A: Upload Custom Notes")
241
-
242
- if not st.session_state.db_created or not st.session_state.embedder_ready:
243
- st.error("⚠️ Complete Steps 3 & 4 first")
244
- else:
245
-
246
- # Choose upload method
247
- upload_method = st.radio(
248
- "Upload method:",
249
- ["πŸ“ Paste Text", "πŸ“„ Upload PDF File"],
250
- horizontal=True
251
- )
252
-
253
- if upload_method == "πŸ“ Paste Text":
254
- with st.expander("✍️ Paste text", expanded=True):
255
-
256
- custom_text = st.text_area(
257
- "Math notes:",
258
- value="""Linear Equations: ax + b = 0, solution is x = -b/a
259
 
260
- Quadratic Equations: axΒ² + bx + c = 0
261
- Solution: x = (-b ± √(b²-4ac)) / 2a
262
 
263
- Pythagorean Theorem: aΒ² + bΒ² = cΒ²
264
 
265
- Derivatives:
266
- d/dx(xⁿ) = nxⁿ⁻¹
267
- d/dx(sin x) = cos x""",
268
- height=200
269
- )
270
-
271
- source_name = st.text_input("Source name:", value="math_notes.txt")
272
-
273
- if st.button("πŸš€ UPLOAD TEXT", type="primary"):
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
274
 
275
- if not custom_text.strip():
276
- st.error("Please enter text!")
277
- else:
278
- try:
279
- progress = st.progress(0)
280
- status = st.empty()
281
-
282
- status.text("πŸ“„ Chunking text...")
283
- progress.progress(0.2)
284
-
285
- words = custom_text.split()
286
- chunks = []
287
- chunk_size = 50
288
-
289
- for i in range(0, len(words), 40):
290
- chunk = ' '.join(words[i:i + chunk_size])
291
- if chunk.strip():
292
- chunks.append(chunk)
293
-
294
- st.write(f"βœ… Created {len(chunks)} chunks")
295
-
296
- status.text("πŸ”’ Generating embeddings...")
297
- progress.progress(0.5)
298
-
299
- embeddings = embedder.encode(chunks, show_progress_bar=False)
300
- st.write(f"βœ… Generated {len(embeddings)} embeddings")
301
-
302
- status.text("☁️ Uploading...")
303
- progress.progress(0.8)
304
-
305
- points = []
306
- for idx, (chunk, embedding) in enumerate(zip(chunks, embeddings)):
307
- points.append(PointStruct(
308
- id=abs(hash(f"{source_name}_{idx}_{custom_text[:50]}_{time.time()}")) % (2**63),
309
- vector=embedding.tolist(),
310
- payload={
311
- "content": chunk,
312
- "source_name": source_name,
313
- "source_type": "custom_notes",
314
- "chunk_index": idx
315
- }
316
- ))
317
-
318
- client.upsert(collection_name=COLLECTION_NAME, points=points)
319
-
320
- progress.progress(1.0)
321
- status.empty()
322
-
323
- st.success(f"πŸŽ‰ Uploaded {len(points)} vectors!")
324
-
325
- count = get_vector_count_reliable(client, COLLECTION_NAME)
326
- st.info(f"πŸ“Š **Total vectors: {count:,}**")
327
-
328
- except Exception as e:
329
- st.error(f"❌ Failed: {str(e)}")
330
- st.exception(e)
331
-
332
- else: # PDF Upload
333
- with st.expander("πŸ“„ Upload PDF", expanded=True):
334
-
335
- st.info("πŸŽ‰ **NEW!** Upload your math PDFs directly")
336
-
337
- uploaded_file = st.file_uploader(
338
- "Choose PDF file:",
339
- type=['pdf'],
340
- help="Upload a PDF with math content"
341
- )
342
-
343
- if uploaded_file:
344
- st.write(f"πŸ“„ File: {uploaded_file.name} ({uploaded_file.size / 1024:.1f} KB)")
345
 
346
- source_name = st.text_input(
347
- "Source name:",
348
- value=uploaded_file.name.replace('.pdf', '')
 
 
 
349
  )
350
 
351
- if st.button("πŸš€ UPLOAD PDF", type="primary"):
352
-
353
- try:
354
- progress = st.progress(0)
355
- status = st.empty()
356
-
357
- # Extract text
358
- status.text("πŸ“– Extracting text from PDF...")
359
- progress.progress(0.1)
360
-
361
- extracted_text = extract_text_from_pdf(uploaded_file)
362
-
363
- if not extracted_text:
364
- st.error("❌ Failed to extract text from PDF")
365
- st.stop()
366
-
367
- st.write(f"βœ… Extracted {len(extracted_text)} characters")
368
-
369
- # Show preview
370
- with st.expander("πŸ‘οΈ Preview extracted text"):
371
- st.text(extracted_text[:500] + "..." if len(extracted_text) > 500 else extracted_text)
372
-
373
- # Chunk
374
- status.text("πŸ“„ Chunking text...")
375
- progress.progress(0.3)
376
-
377
- words = extracted_text.split()
378
- chunks = []
379
- chunk_size = 100 # Larger chunks for PDFs
380
- overlap = 20
381
-
382
- for i in range(0, len(words), chunk_size - overlap):
383
- chunk = ' '.join(words[i:i + chunk_size])
384
- if chunk.strip():
385
- chunks.append(chunk)
386
-
387
- st.write(f"βœ… Created {len(chunks)} chunks")
388
-
389
- # Embed
390
- status.text("πŸ”’ Generating embeddings...")
391
- progress.progress(0.5)
392
-
393
- embeddings = []
394
- for idx, chunk in enumerate(chunks):
395
- embedding = embedder.encode(chunk)
396
- embeddings.append(embedding)
397
-
398
- if idx % 20 == 0:
399
- progress.progress(0.5 + (0.3 * idx / len(chunks)))
400
-
401
- st.write(f"βœ… Generated {len(embeddings)} embeddings")
402
-
403
- # Upload
404
- status.text("☁️ Uploading to database...")
405
- progress.progress(0.9)
406
-
407
- points = []
408
- for idx, (chunk, embedding) in enumerate(zip(chunks, embeddings)):
409
- points.append(PointStruct(
410
- id=abs(hash(f"pdf_{source_name}_{idx}_{time.time()}")) % (2**63),
411
- vector=embedding.tolist(),
412
- payload={
413
- "content": chunk,
414
- "source_name": source_name,
415
- "source_type": "pdf_upload",
416
- "chunk_index": idx,
417
- "file_name": uploaded_file.name
418
- }
419
- ))
420
-
421
- client.upsert(collection_name=COLLECTION_NAME, points=points)
422
-
423
- progress.progress(1.0)
424
- status.empty()
425
-
426
- st.success(f"πŸŽ‰ Uploaded {len(points)} vectors from PDF!")
427
- st.balloons()
428
-
429
- count = get_vector_count_reliable(client, COLLECTION_NAME)
430
- st.info(f"πŸ“Š **Total vectors: {count:,}**")
431
-
432
- except Exception as e:
433
- st.error(f"❌ Upload failed: {str(e)}")
434
- st.exception(e)
435
-
436
- st.markdown("---")
437
 
438
  # ============================================================================
439
- # STEP 5B: Load Public Datasets (FIXED - No DeepMind)
440
  # ============================================================================
441
 
442
- if show_upload:
443
- st.header("πŸ“š Step 5B: Load Public Datasets")
444
 
445
- if not st.session_state.db_created or not st.session_state.embedder_ready:
446
- st.error("⚠️ Complete Steps 3 & 4 first")
447
- else:
448
- with st.expander("πŸ“Š Load from Hugging Face", expanded=False):
449
-
450
- dataset_choice = st.selectbox(
451
- "Dataset:",
452
- [
453
- "GSM8K - Grade School Math (8.5K problems)",
454
- "MATH - Competition Math (12.5K problems) ✨",
455
- "MathQA - Math Word Problems (37K problems) πŸ†•",
456
- "CAMEL-AI Math - GPT-4 Generated (50K problems)",
457
- "RACE - Reading Comprehension (28K passages)"
458
- ]
459
- )
460
-
461
- # INCREASED LIMIT FROM 500 TO 2000!
462
- sample_size = st.slider("Items to load:", 10, 2000, 50)
463
-
464
- st.warning(f"⚠️ Loading {sample_size} items. Large numbers take 5-15 minutes!")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
465
 
466
- if st.button("πŸ“₯ LOAD DATASET", type="primary"):
467
-
468
- try:
469
- from datasets import load_dataset
470
-
471
- progress = st.progress(0)
472
- status = st.empty()
473
-
474
- # GSM8K
475
- if "GSM8K" in dataset_choice:
476
- status.text("πŸ“₯ Downloading GSM8K...")
477
- progress.progress(0.1)
478
-
479
- dataset = load_dataset("openai/gsm8k", "main", split="train", trust_remote_code=True)
480
- dataset_name = "GSM8K"
481
-
482
- texts = []
483
- for i in range(min(sample_size, len(dataset))):
484
- item = dataset[i]
485
- text = f"Problem: {item['question']}\n\nSolution: {item['answer']}"
486
- texts.append(text)
487
-
488
- # MATH
489
- elif "MATH" in dataset_choice and "Competition" in dataset_choice:
490
- status.text("πŸ“₯ Downloading MATH...")
491
- progress.progress(0.1)
492
-
493
- dataset = None
494
- dataset_name = "MATH"
495
-
496
- # Try multiple sources
497
- for source in ["lighteval/MATH", "DigitalLearningGmbH/MATH-lighteval", "EleutherAI/hendrycks_math"]:
498
- try:
499
- dataset = load_dataset(source, split="train", trust_remote_code=True)
500
- st.success(f"βœ… Using {source}")
501
- break
502
- except:
503
- continue
504
-
505
- if dataset is None:
506
- st.error("❌ All MATH sources failed")
507
- st.stop()
508
-
509
- texts = []
510
- for i in range(min(sample_size, len(dataset))):
511
- item = dataset[i]
512
- problem = item.get('problem', item.get('question', ''))
513
- solution = item.get('solution', item.get('answer', ''))
514
- problem_type = item.get('type', item.get('level', 'general'))
515
- text = f"Problem ({problem_type}): {problem}\n\nSolution: {solution}"
516
- texts.append(text)
517
 
518
- # MathQA (REPLACES DEEPMIND)
519
- elif "MathQA" in dataset_choice:
520
- status.text("πŸ“₯ Downloading MathQA...")
521
- progress.progress(0.1)
 
 
522
 
523
- st.info("πŸ†• MathQA: 37K math word problems with detailed solutions")
524
 
525
- dataset = load_dataset("allenai/math_qa", split="train", trust_remote_code=True)
526
- dataset_name = "MathQA"
 
527
 
528
- texts = []
529
- for i in range(min(sample_size, len(dataset))):
530
- item = dataset[i]
531
- text = f"Problem: {item['Problem']}\n\nRationale: {item['Rationale']}\n\nAnswer: {item['correct']}"
532
- texts.append(text)
533
-
534
- # CAMEL-AI
535
- elif "CAMEL" in dataset_choice:
536
- status.text("πŸ“₯ Downloading CAMEL-AI...")
537
- progress.progress(0.1)
538
 
539
- dataset = load_dataset("camel-ai/math", split="train", trust_remote_code=True)
540
- dataset_name = "CAMEL-Math"
 
 
 
 
 
 
 
 
 
 
 
541
 
542
- texts = []
543
- for i in range(min(sample_size, len(dataset))):
544
- item = dataset[i]
545
- text = f"Problem: {item['message']}"
546
- texts.append(text)
547
 
548
- # RACE
549
- else:
550
- status.text("πŸ“₯ Downloading RACE...")
551
- progress.progress(0.1)
552
-
553
- dataset = load_dataset("ehovy/race", "all", split="train", trust_remote_code=True)
554
- dataset_name = "RACE"
555
-
556
- texts = []
557
- for i in range(min(sample_size, len(dataset))):
558
- item = dataset[i]
559
- text = f"Article: {item['article'][:500]}\n\nQuestion: {item['question']}\n\nAnswer: {item['answer']}"
560
- texts.append(text)
 
 
 
 
 
561
 
562
- # Common processing
563
- st.write(f"βœ… Loaded {len(texts)} items from {dataset_name}")
564
- progress.progress(0.3)
 
565
 
566
- status.text("πŸ”’ Generating embeddings...")
567
- embeddings = []
 
 
568
 
569
- for idx, text in enumerate(texts):
570
- embedding = embedder.encode(text)
571
- embeddings.append(embedding)
572
-
573
- if idx % 50 == 0:
574
- progress.progress(0.3 + (0.5 * idx / len(texts)))
575
- status.text(f"πŸ”’ Embedding {idx+1}/{len(texts)}")
576
 
577
- st.write(f"βœ… Generated {len(embeddings)} embeddings")
578
- progress.progress(0.8)
579
 
580
- status.text("☁️ Uploading...")
 
581
 
582
  points = []
583
- for idx, (text, embedding) in enumerate(zip(texts, embeddings)):
584
- content = text[:2000] if len(text) > 2000 else text
585
-
586
  points.append(PointStruct(
587
- id=abs(hash(f"{dataset_name}_{idx}_{time.time()}")) % (2**63),
588
- vector=embedding.tolist(),
589
  payload={
590
- "content": content,
591
- "source_name": dataset_name,
592
  "source_type": "public_dataset",
593
- "dataset": dataset_name,
594
- "index": idx
595
  }
596
  ))
597
 
598
- client.upsert(collection_name=COLLECTION_NAME, points=points)
599
-
600
- progress.progress(1.0)
601
- status.empty()
602
-
603
- st.success(f"πŸŽ‰ Uploaded {len(points)} vectors from {dataset_name}!")
604
-
605
- count = get_vector_count_reliable(client, COLLECTION_NAME)
606
- st.info(f"πŸ“Š **Total vectors: {count:,}**")
607
-
608
- except ImportError:
609
- st.error("❌ Add 'datasets' to requirements.txt")
610
- except Exception as e:
611
- st.error(f"❌ Failed: {str(e)}")
612
- st.exception(e)
613
 
614
- st.markdown("---")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
615
 
616
  # ============================================================================
617
- # STEP 6: Search
618
  # ============================================================================
619
 
620
- if show_search:
621
- st.header("πŸ” Step 6: Test Search")
622
 
623
- if not st.session_state.db_created or not st.session_state.embedder_ready:
624
- st.error("⚠️ Database and embedder must be ready")
625
- else:
626
- search_query = st.text_input(
627
- "Question:",
628
- placeholder="Solve xΒ² + 5x - 4 = 0"
629
- )
630
 
631
- col1, col2 = st.columns([3, 1])
632
- with col1:
633
- top_k = st.slider("Results:", 1, 10, 5)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
634
 
635
- with col2:
636
- st.metric("DB Vectors", get_vector_count_reliable(client, COLLECTION_NAME))
 
 
 
 
 
637
 
638
- if st.button("πŸ” SEARCH", type="primary") and search_query:
639
 
640
- try:
641
- with st.spinner("Searching..."):
642
-
643
- query_embedding = embedder.encode(search_query)
644
-
645
- results = client.search(
646
- collection_name=COLLECTION_NAME,
647
- query_vector=query_embedding.tolist(),
648
- limit=top_k
649
- )
650
-
651
- if results:
652
- st.success(f"βœ… Found {len(results)} results!")
653
-
654
- for i, result in enumerate(results, 1):
655
- similarity_pct = result.score * 100
656
-
657
- if similarity_pct > 50:
658
- color = "🟒"
659
- elif similarity_pct > 30:
660
- color = "🟑"
661
- else:
662
- color = "πŸ”΄"
663
-
664
- with st.expander(f"{color} Result {i} - {similarity_pct:.1f}% match", expanded=(i<=2)):
665
- st.info(result.payload['content'])
666
-
667
- col1, col2, col3 = st.columns(3)
668
- with col1:
669
- st.caption(f"**Source:** {result.payload['source_name']}")
670
- with col2:
671
- st.caption(f"**Type:** {result.payload['source_type']}")
672
- with col3:
673
- st.caption(f"**Score:** {result.score:.4f}")
674
- else:
675
- st.warning("No results found!")
676
-
677
- except Exception as e:
678
- st.error(f"❌ Search failed: {str(e)}")
679
 
680
- st.markdown("---")
681
- st.success("πŸŽ‰ Phase 2.5 Complete! You now have: Text, PDF upload, and 4 working datasets!")
 
 
1
  import streamlit as st
2
  import os
3
  import time
4
+ import base64
5
+ from io import BytesIO
6
+ from PIL import Image
7
+ import PyPDF2
8
+ from anthropic import Anthropic
9
  from qdrant_client import QdrantClient
10
  from qdrant_client.models import Distance, VectorParams, PointStruct
11
  from sentence_transformers import SentenceTransformer
 
 
12
 
13
  # ============================================================================
14
+ # COMPLETE MATH AI SYSTEM - ALL-IN-ONE HUGGING FACE SPACE
15
  # ============================================================================
16
 
17
  st.set_page_config(
18
+ page_title="Math AI System",
19
+ page_icon="πŸŽ“",
20
+ layout="wide",
21
+ initial_sidebar_state="expanded"
22
  )
23
 
24
  COLLECTION_NAME = "math_knowledge_base"
25
 
26
  # ============================================================================
27
+ # CACHED RESOURCES
28
  # ============================================================================
29
 
30
+ @st.cache_resource
31
+ def get_clients():
32
+ """Initialize all clients - cached"""
33
+ qdrant = QdrantClient(
34
+ url=os.getenv("QDRANT_URL"),
35
+ api_key=os.getenv("QDRANT_API_KEY")
36
+ )
37
 
38
+ claude = Anthropic(api_key=os.getenv("ANTHROPIC_API_KEY"))
39
+
40
+ embedder = SentenceTransformer('sentence-transformers/all-MiniLM-L6-v2')
41
 
42
+ return qdrant, claude, embedder
43
 
44
+ # ============================================================================
45
+ # HELPER FUNCTIONS
46
+ # ============================================================================
47
+
48
+ def extract_text_from_pdf(pdf_file):
49
+ """Extract text from typed PDF"""
50
  try:
51
+ pdf_reader = PyPDF2.PdfReader(pdf_file)
52
+ text = ""
53
+ for page_num, page in enumerate(pdf_reader.pages):
54
+ text += f"\n\n=== Page {page_num + 1} ===\n\n{page.extract_text()}"
55
+ return text
56
  except Exception as e:
 
57
  return None
58
 
59
+ def chunk_text(text, chunk_size=150, overlap=30):
60
+ """Split text into chunks"""
61
+ words = text.split()
62
+ chunks = []
63
+ for i in range(0, len(words), chunk_size - overlap):
64
+ chunk = ' '.join(words[i:i + chunk_size])
65
+ if chunk.strip():
66
+ chunks.append(chunk)
67
+ return chunks
68
+
69
+ def get_vector_count(qdrant):
70
+ """Get total vectors in database"""
71
  try:
72
  count = 0
73
  offset = None
74
+ for _ in range(1000):
75
+ result = qdrant.scroll(
76
+ collection_name=COLLECTION_NAME,
 
 
77
  limit=100,
78
  offset=offset,
79
  with_payload=False,
80
  with_vectors=False
81
  )
82
+ if not result or not result[0]:
 
83
  break
 
84
  count += len(result[0])
85
  offset = result[1]
 
86
  if offset is None:
87
  break
 
88
  return count
89
  except:
90
  return 0
91
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
92
  # ============================================================================
93
+ # MAIN APP
94
  # ============================================================================
95
 
96
+ # Initialize clients
97
+ try:
98
+ qdrant, claude, embedder = get_clients()
99
+ st.sidebar.success("βœ… System Ready")
100
+ except Exception as e:
101
+ st.error(f"❌ Initialization failed: {e}")
102
+ st.info("Add QDRANT_URL, QDRANT_API_KEY, and ANTHROPIC_API_KEY in Settings β†’ Secrets")
103
+ st.stop()
104
 
105
  # ============================================================================
106
+ # SIDEBAR: MODE SELECTION
107
  # ============================================================================
108
 
109
+ st.sidebar.title("πŸŽ“ Math AI System")
110
 
111
+ mode = st.sidebar.radio(
112
+ "Select Mode:",
113
+ ["πŸ” Search & Solve", "πŸ—οΈ Setup Database", "πŸ§ͺ Testing Dashboard"],
114
+ index=0
115
+ )
116
 
117
+ st.sidebar.markdown("---")
 
 
118
 
119
+ # Show database stats
120
+ try:
121
+ vector_count = get_vector_count(qdrant)
122
+ st.sidebar.metric("Vectors in DB", f"{vector_count:,}")
 
 
 
 
 
 
 
 
 
 
123
 
124
+ storage_mb = (vector_count * 384 * 4) / (1024 * 1024)
125
+ st.sidebar.metric("Storage Used", f"{storage_mb:.1f} MB")
126
+ except:
127
+ st.sidebar.warning("Database not accessible")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
128
 
129
  # ============================================================================
130
+ # MODE 1: SEARCH & SOLVE (Main Interface)
131
  # ============================================================================
132
 
133
+ if mode == "πŸ” Search & Solve":
 
134
 
135
+ st.title("πŸ” Math Problem Solver")
136
+ st.markdown("*Search your knowledge base and get detailed solutions*")
137
 
138
+ # ========================================================================
139
+ # INPUT: Problem Statement
140
+ # ========================================================================
141
 
142
+ st.header("πŸ“ Input Problem")
 
143
 
144
+ input_method = st.radio(
145
+ "How to input:",
146
+ ["✍️ Type Question", "πŸ“„ Upload Exam PDF"],
147
+ horizontal=True
148
+ )
149
 
150
+ problem = None
 
 
151
 
152
+ if input_method == "✍️ Type Question":
153
+ problem = st.text_area(
154
+ "Enter math problem:",
155
+ placeholder="Example: Find the gradient of the loss function L(w) = (1/2)||Xw - y||Β²",
156
+ height=150
157
+ )
 
 
158
 
159
+ else:
160
+ uploaded_exam = st.file_uploader("Upload exam PDF:", type=['pdf'])
161
+ if uploaded_exam:
162
+ exam_text = extract_text_from_pdf(uploaded_exam)
163
+ if exam_text:
164
+ st.text_area("Extracted text:", exam_text[:1000], height=200)
165
+ problem = st.text_input("Extract specific question or use full text")
166
+
167
+ # ========================================================================
168
+ # SETTINGS
169
+ # ========================================================================
170
+
171
+ with st.expander("βš™οΈ Advanced Settings"):
172
  col1, col2 = st.columns(2)
173
+
174
  with col1:
175
+ search_filter = st.multiselect(
176
+ "Search in:",
177
+ ["Books", "Exams", "Handwritten Solutions", "Public Datasets"],
178
+ default=["Books", "Exams", "Handwritten Solutions"]
179
+ )
 
 
180
 
181
  with col2:
182
+ top_k = st.slider("Retrieve top:", 3, 20, 5)
183
+ detail_level = st.select_slider(
184
+ "Detail level:",
185
+ ["Concise", "Standard", "Detailed", "Very Detailed"],
186
+ value="Detailed"
187
+ )
188
 
189
+ # ========================================================================
190
+ # SOLVE BUTTON
191
+ # ========================================================================
192
+
193
+ if st.button("πŸš€ SOLVE PROBLEM", type="primary") and problem:
194
+
195
+ with st.spinner("πŸ” Searching knowledge base..."):
196
+
197
+ # Generate query embedding
198
+ query_embedding = embedder.encode(problem)
199
+
200
+ # Create filter
201
+ filter_types = []
202
+ if "Books" in search_filter:
203
+ filter_types.append("book")
204
+ if "Exams" in search_filter:
205
+ filter_types.append("exam")
206
+ if "Handwritten Solutions" in search_filter:
207
+ filter_types.append("answer_handwritten")
208
+ if "Public Datasets" in search_filter:
209
+ filter_types.append("public_dataset")
210
+
211
+ # Search Qdrant
212
  try:
213
+ results = qdrant.search(
214
  collection_name=COLLECTION_NAME,
215
+ query_vector=query_embedding.tolist(),
216
+ limit=top_k
217
  )
 
 
 
218
  except Exception as e:
219
+ st.error(f"Search failed: {e}")
220
+ results = []
221
+
222
+ if not results:
223
+ st.warning("No relevant context found. Try loading more data in Setup mode.")
224
+
225
+ else:
226
+ st.success(f"βœ… Found {len(results)} relevant references!")
227
+
228
+ # Show retrieved context
229
+ with st.expander("πŸ“š Retrieved References"):
230
+ for i, result in enumerate(results, 1):
231
+ similarity = result.score * 100
232
+ st.markdown(f"**Reference {i}** ({similarity:.1f}% relevant)")
233
+ st.info(result.payload['content'][:300] + "...")
234
+ st.caption(f"Source: {result.payload.get('source_name', 'Unknown')}")
235
+ st.markdown("---")
236
+
237
+ # Generate solution with Claude
238
+ with st.spinner("πŸ€– Claude is generating solution..."):
239
+
240
+ # Prepare context
241
+ context = "\n\n".join([
242
+ f"[Reference {i+1} from {r.payload.get('source_name', 'Unknown')}]:\n{r.payload['content']}"
243
+ for i, r in enumerate(results)
244
+ ])
245
+
246
+ # Determine detail level
247
+ detail_instructions = {
248
+ "Concise": "Provide a brief solution focusing on key steps.",
249
+ "Standard": "Provide a clear solution with main steps explained.",
250
+ "Detailed": "Provide a comprehensive solution with detailed explanations.",
251
+ "Very Detailed": "Provide an exhaustive solution with all intermediate steps, intuitions, and alternative approaches."
252
+ }
253
+
254
+ # Create prompt
255
+ prompt = f"""You are an expert mathematics tutor specializing in machine learning mathematics.
256
 
257
+ PROBLEM TO SOLVE:
258
+ {problem}
 
259
 
260
+ REFERENCE MATERIALS (from student's books, exams, and notes):
261
+ {context}
 
 
 
 
 
 
 
 
262
 
263
+ TASK:
264
+ Solve this problem providing a complete, educational solution.
 
265
 
266
+ {detail_instructions[detail_level]}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
267
 
268
+ FORMAT YOUR RESPONSE EXACTLY LIKE THIS:
 
269
 
270
+ ## SOLUTION
271
 
272
+ [Provide step-by-step solution here with clear mathematical notation]
273
+
274
+ ## REASONING & APPROACH
275
+
276
+ [Explain WHY you chose this approach, what concepts are involved, and how the references helped]
277
+
278
+ ## REFERENCES USED
279
+
280
+ [List which references you used and HOW each contributed to the solution. Be specific - mention what information came from which source]
281
+
282
+ ## VERIFICATION
283
+
284
+ [If applicable, verify the solution or discuss how to check if it's correct]
285
+
286
+ IMPORTANT:
287
+ - Use proper mathematical notation (LaTeX if needed: ∫, βˆ‘, βˆ‚, etc.)
288
+ - Reference the student's materials when explaining concepts
289
+ - Make it educational - help them understand, not just get an answer"""
290
+
291
+ try:
292
+ message = claude.messages.create(
293
+ model="claude-sonnet-4-20250514",
294
+ max_tokens=4000,
295
+ messages=[{"role": "user", "content": prompt}]
296
+ )
297
 
298
+ solution = message.content[0].text
299
+
300
+ # Display solution
301
+ st.markdown("---")
302
+ st.markdown(solution)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
303
 
304
+ # Download option
305
+ st.download_button(
306
+ "πŸ“₯ Download Solution",
307
+ solution,
308
+ file_name=f"solution_{int(time.time())}.md",
309
+ mime="text/markdown"
310
  )
311
 
312
+ # API usage
313
+ with st.expander("πŸ“Š API Usage"):
314
+ st.json({
315
+ "model": "claude-sonnet-4-20250514",
316
+ "input_tokens": message.usage.input_tokens,
317
+ "output_tokens": message.usage.output_tokens,
318
+ "cost_estimate": f"${(message.usage.input_tokens * 0.000003 + message.usage.output_tokens * 0.000015):.4f}"
319
+ })
320
+
321
+ except Exception as e:
322
+ st.error(f"Claude error: {e}")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
323
 
324
  # ============================================================================
325
+ # MODE 2: SETUP DATABASE (One-Time Processing)
326
  # ============================================================================
327
 
328
+ elif mode == "πŸ—οΈ Setup Database":
 
329
 
330
+ st.title("πŸ—οΈ Database Setup")
331
+ st.markdown("*Process and upload your documents (run once)*")
332
+
333
+ st.warning("""
334
+ ⚠️ **IMPORTANT LIMITATION**:
335
+
336
+ Hugging Face Spaces cannot directly access Google Drive files.
337
+
338
+ **Recommended Solution:**
339
+ 1. Use **Google Colab** for one-time processing (cloud, free)
340
+ 2. Use **this HF Space** for daily searching/solving
341
+
342
+ **Alternative (Manual)**:
343
+ - Download PDFs from Google Drive
344
+ - Upload them here one by one
345
+ """)
346
+
347
+ # ========================================================================
348
+ # CREATE COLLECTION
349
+ # ========================================================================
350
+
351
+ st.header("Step 1: Create Database Collection")
352
+
353
+ try:
354
+ collections = qdrant.get_collections().collections
355
+ exists = any(c.name == COLLECTION_NAME for c in collections)
356
+
357
+ if exists:
358
+ st.success(f"βœ… Collection '{COLLECTION_NAME}' exists")
359
+ else:
360
+ if st.button("πŸ—οΈ Create Collection"):
361
+ qdrant.create_collection(
362
+ collection_name=COLLECTION_NAME,
363
+ vectors_config=VectorParams(size=384, distance=Distance.COSINE)
364
+ )
365
+ st.success("βœ… Created!")
366
+ st.rerun()
367
+ except Exception as e:
368
+ st.error(f"Error: {e}")
369
+
370
+ st.markdown("---")
371
+
372
+ # ========================================================================
373
+ # UPLOAD OPTIONS
374
+ # ========================================================================
375
+
376
+ st.header("Step 2: Upload Documents")
377
+
378
+ tab1, tab2, tab3 = st.tabs(["πŸ“š Upload PDFs", "πŸ“Š Load Public Datasets", "πŸ–ŠοΈ Process Handwritten (Colab)"])
379
+
380
+ with tab1:
381
+ st.info("Upload your books and typed exams here")
382
+
383
+ uploaded_files = st.file_uploader(
384
+ "Choose PDF files:",
385
+ type=['pdf'],
386
+ accept_multiple_files=True
387
+ )
388
+
389
+ doc_type = st.selectbox("Document type:", ["Book", "Exam", "Other"])
390
+
391
+ if uploaded_files and st.button("Process & Upload PDFs"):
392
 
393
+ for uploaded_file in uploaded_files:
394
+ with st.expander(f"Processing {uploaded_file.name}"):
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
395
 
396
+ try:
397
+ # Extract
398
+ text = extract_text_from_pdf(uploaded_file)
399
+ if not text:
400
+ st.error("Failed to extract text")
401
+ continue
402
 
403
+ st.write(f"βœ… Extracted {len(text):,} chars")
404
 
405
+ # Chunk
406
+ chunks = chunk_text(text)
407
+ st.write(f"βœ… Created {len(chunks)} chunks")
408
 
409
+ # Embed
410
+ embeddings = embedder.encode(chunks, show_progress_bar=False)
 
 
 
 
 
 
 
 
411
 
412
+ # Upload
413
+ points = []
414
+ for i, (chunk, emb) in enumerate(zip(chunks, embeddings)):
415
+ points.append(PointStruct(
416
+ id=abs(hash(f"{uploaded_file.name}_{i}_{time.time()}")) % (2**63),
417
+ vector=emb.tolist(),
418
+ payload={
419
+ "content": chunk,
420
+ "source_name": uploaded_file.name,
421
+ "source_type": doc_type.lower(),
422
+ "chunk_index": i
423
+ }
424
+ ))
425
 
426
+ qdrant.upsert(collection_name=COLLECTION_NAME, points=points)
427
+ st.success(f"βœ… Uploaded {len(points)} vectors!")
 
 
 
428
 
429
+ except Exception as e:
430
+ st.error(f"Error: {e}")
431
+
432
+ with tab2:
433
+ st.info("Load pre-built math datasets")
434
+
435
+ dataset_choice = st.selectbox(
436
+ "Choose dataset:",
437
+ ["GSM8K", "MATH", "MathQA"]
438
+ )
439
+
440
+ sample_size = st.slider("Number of samples:", 10, 1000, 100)
441
+
442
+ if st.button("Load Dataset"):
443
+ try:
444
+ from datasets import load_dataset
445
+
446
+ with st.spinner(f"Loading {dataset_choice}..."):
447
 
448
+ if dataset_choice == "GSM8K":
449
+ dataset = load_dataset("openai/gsm8k", "main", split="train", trust_remote_code=True)
450
+ texts = [f"Problem: {dataset[i]['question']}\n\nSolution: {dataset[i]['answer']}"
451
+ for i in range(min(sample_size, len(dataset)))]
452
 
453
+ elif dataset_choice == "MATH":
454
+ dataset = load_dataset("lighteval/MATH", split="train", trust_remote_code=True)
455
+ texts = [f"Problem: {dataset[i].get('problem', '')}\n\nSolution: {dataset[i].get('solution', '')}"
456
+ for i in range(min(sample_size, len(dataset)))]
457
 
458
+ else: # MathQA
459
+ dataset = load_dataset("allenai/math_qa", split="train", trust_remote_code=True)
460
+ texts = [f"Problem: {dataset[i]['Problem']}\n\nAnswer: {dataset[i]['correct']}"
461
+ for i in range(min(sample_size, len(dataset)))]
 
 
 
462
 
463
+ st.write(f"βœ… Loaded {len(texts)} problems")
 
464
 
465
+ # Embed & upload
466
+ embeddings = embedder.encode(texts, show_progress_bar=True)
467
 
468
  points = []
469
+ for i, (text, emb) in enumerate(zip(texts, embeddings)):
 
 
470
  points.append(PointStruct(
471
+ id=abs(hash(f"{dataset_choice}_{i}_{time.time()}")) % (2**63),
472
+ vector=emb.tolist(),
473
  payload={
474
+ "content": text[:2000],
475
+ "source_name": dataset_choice,
476
  "source_type": "public_dataset",
477
+ "index": i
 
478
  }
479
  ))
480
 
481
+ qdrant.upsert(collection_name=COLLECTION_NAME, points=points)
482
+ st.success(f"βœ… Uploaded {len(points)} vectors!")
483
+ st.balloons()
484
+
485
+ except Exception as e:
486
+ st.error(f"Error: {e}")
 
 
 
 
 
 
 
 
 
487
 
488
+ with tab3:
489
+ st.warning("**Handwritten OCR requires Google Colab** (HF Spaces limitation)")
490
+
491
+ st.markdown("""
492
+ ### Why Colab for Handwritten Notes?
493
+
494
+ 1. **File Access**: Need direct Google Drive access
495
+ 2. **Processing Power**: OCR is compute-intensive
496
+ 3. **Image Processing**: Requires additional libraries
497
+
498
+ ### Steps:
499
+
500
+ 1. **Click button below** to open ready-to-use Colab notebook
501
+ 2. **Run the notebook** (processes handwritten PDFs with AI OCR)
502
+ 3. **Vectors auto-upload** to your Qdrant database
503
+ 4. **Come back here** to search!
504
+
505
+ The notebook handles:
506
+ - βœ… Google Drive connection
507
+ - βœ… Italian cursive handwriting OCR (Claude Vision)
508
+ - βœ… Context from books/exams
509
+ - βœ… Direct upload to Qdrant
510
+ """)
511
+
512
+ colab_code_url = "https://colab.research.google.com/drive/your-notebook-id"
513
+
514
+ st.link_button(
515
+ "πŸ““ Open Google Colab Notebook",
516
+ colab_code_url,
517
+ use_container_width=True
518
+ )
519
+
520
+ st.info("""
521
+ **What the Colab notebook will do:**
522
+ - Connect to your Google Drive (one click)
523
+ - Read PDFs from Math_AI_Documents/answers/
524
+ - Use Claude Vision to OCR handwritten Italian cursive
525
+ - Upload directly to this same Qdrant database
526
+ - Takes ~30-60 minutes, costs ~$0.60
527
+ """)
528
 
529
  # ============================================================================
530
+ # MODE 3: TESTING DASHBOARD
531
  # ============================================================================
532
 
533
+ elif mode == "πŸ§ͺ Testing Dashboard":
 
534
 
535
+ st.title("πŸ§ͺ Testing Dashboard")
536
+ st.markdown("*Evaluate system performance*")
537
+
538
+ tab1, tab2, tab3 = st.tabs(["πŸ“Š Database Stats", "🎯 Accuracy Tests", "πŸ“ˆ Performance"])
539
+
540
+ with tab1:
541
+ st.header("Database Statistics")
542
 
543
+ try:
544
+ # Get sample
545
+ sample = qdrant.scroll(
546
+ collection_name=COLLECTION_NAME,
547
+ limit=1000,
548
+ with_payload=True,
549
+ with_vectors=False
550
+ )
551
+
552
+ if sample and sample[0]:
553
+ # Count by type
554
+ types = {}
555
+ sources = set()
556
+
557
+ for point in sample[0]:
558
+ src_type = point.payload.get('source_type', 'unknown')
559
+ types[src_type] = types.get(src_type, 0) + 1
560
+ sources.add(point.payload.get('source_name', 'Unknown'))
561
+
562
+ # Display
563
+ col1, col2, col3 = st.columns(3)
564
+
565
+ with col1:
566
+ st.metric("Total Vectors", get_vector_count(qdrant))
567
+
568
+ with col2:
569
+ st.metric("Unique Sources", len(sources))
570
+
571
+ with col3:
572
+ st.metric("Document Types", len(types))
573
+
574
+ # Breakdown
575
+ st.subheader("Breakdown by Type")
576
+ for doc_type, count in sorted(types.items()):
577
+ st.progress(count / sum(types.values()), text=f"{doc_type}: {count}")
578
+
579
+ # Sources
580
+ st.subheader("Sources")
581
+ for src in sorted(sources)[:20]:
582
+ st.caption(f"β€’ {src}")
583
 
584
+ except Exception as e:
585
+ st.error(f"Error: {e}")
586
+
587
+ with tab2:
588
+ st.header("Test Search Accuracy")
589
+
590
+ test_query = st.text_input("Test query:", placeholder="gradient descent")
591
 
592
+ if st.button("Run Test Search") and test_query:
593
 
594
+ query_emb = embedder.encode(test_query)
595
+
596
+ results = qdrant.search(
597
+ collection_name=COLLECTION_NAME,
598
+ query_vector=query_emb.tolist(),
599
+ limit=5
600
+ )
601
+
602
+ st.write(f"**Found {len(results)} results:**")
603
+
604
+ for i, r in enumerate(results, 1):
605
+ similarity = r.score * 100
606
+
607
+ quality = "🟒 Excellent" if similarity > 70 else "🟑 Good" if similarity > 50 else "πŸ”΄ Fair"
608
+
609
+ st.markdown(f"**{i}. {quality}** ({similarity:.1f}%)")
610
+ st.text(r.payload['content'][:200] + "...")
611
+ st.caption(f"Source: {r.payload.get('source_name')}")
612
+ st.markdown("---")
613
+
614
+ with tab3:
615
+ st.header("Performance Metrics")
616
+
617
+ st.info("Coming soon: Response time, token usage, cost tracking")
618
+
619
+ # ============================================================================
620
+ # FOOTER
621
+ # ============================================================================
 
 
 
 
 
 
 
 
 
 
 
622
 
623
+ st.sidebar.markdown("---")
624
+ st.sidebar.caption("πŸŽ“ Math AI System v1.0")
625
+ st.sidebar.caption("Powered by Claude + Qdrant")