NavyDevilDoc commited on
Commit
676a1c0
Β·
verified Β·
1 Parent(s): c0086e2

Update src/app.py

Browse files
Files changed (1) hide show
  1. src/app.py +118 -297
src/app.py CHANGED
@@ -13,7 +13,7 @@ from openai import OpenAI
13
  from datetime import datetime
14
  from test_integration import run_tests
15
  from core.QuizEngine import QuizEngine
16
- from core.PineconeManager import PineconeManager # FIXED: Added missing import
17
 
18
  # --- CONFIGURATION ---
19
  st.set_page_config(page_title="Navy AI Toolkit", page_icon="βš“", layout="wide")
@@ -35,6 +35,10 @@ if "quiz_state" not in st.session_state:
35
  "generated_question_text": ""
36
  }
37
 
 
 
 
 
38
  if "active_index" not in st.session_state:
39
  st.session_state.active_index = None
40
 
@@ -82,7 +86,6 @@ class OutlineProcessor:
82
  # --- HELPER FUNCTIONS ---
83
  def query_model_universal(messages, max_tokens, model_choice, user_key=None):
84
  """Unified router for both Chat and Tools."""
85
- # 1. OpenAI Path
86
  if "GPT-4o" in model_choice:
87
  key = user_key if user_key else OPENAI_KEY
88
  if not key: return "[Error: No OpenAI API Key]", None
@@ -96,8 +99,6 @@ def query_model_universal(messages, max_tokens, model_choice, user_key=None):
96
  return resp.choices[0].message.content, usage
97
  except Exception as e:
98
  return f"[OpenAI Error: {e}]", None
99
-
100
- # 2. Local Path
101
  else:
102
  model_map = {
103
  "Granite 4 (IBM)": "granite4:latest",
@@ -109,7 +110,6 @@ def query_model_universal(messages, max_tokens, model_choice, user_key=None):
109
 
110
  url = f"{API_URL_ROOT}/generate"
111
 
112
- # Flatten history for Ollama
113
  hist = ""
114
  sys_msg = "You are a helpful assistant."
115
  for m in messages:
@@ -128,18 +128,28 @@ def query_model_universal(messages, max_tokens, model_choice, user_key=None):
128
  return f"[Conn Error: {e}]", None
129
 
130
  def update_sidebar_metrics():
131
- # Helper to safely update metrics if placeholder exists
132
  if metric_placeholder:
133
  stats = tracker.get_daily_stats()
134
  u_stats = stats["users"].get(st.session_state.username, {"input":0, "output":0})
135
  metric_placeholder.metric("My Tokens Today", u_stats["input"] + u_stats["output"])
136
 
 
 
 
 
 
 
 
 
 
 
 
 
137
  # --- LOGIN ---
138
  if "authentication_status" not in st.session_state or st.session_state["authentication_status"] is None:
139
  login_tab, register_tab = st.tabs(["πŸ”‘ Login", "πŸ“ Register"])
140
  with login_tab:
141
  if tracker.check_login():
142
- # Session Isolation Logic
143
  if "last_user" in st.session_state and st.session_state.last_user != st.session_state.username:
144
  st.session_state.messages = []
145
  st.session_state.user_openai_key = None
@@ -154,14 +164,10 @@ if "authentication_status" not in st.session_state or st.session_state["authenti
154
  new_email = st.text_input("Email")
155
  new_pwd = st.text_input("Password", type="password")
156
  invite = st.text_input("Invitation Passcode")
157
-
158
  if st.form_submit_button("Register"):
159
  success, msg = tracker.register_user(new_email, new_user, new_name, new_pwd, invite)
160
- if success:
161
- st.success(msg)
162
- else:
163
- st.error(msg)
164
-
165
  if not st.session_state.get("authentication_status"): st.stop()
166
 
167
  # --- SIDEBAR ---
@@ -169,47 +175,33 @@ metric_placeholder = None
169
  with st.sidebar:
170
  st.header("πŸ‘€ User Profile")
171
  st.write(f"Welcome, **{st.session_state.name}**")
172
-
173
  st.header("πŸ“Š Usage Tracker")
174
  metric_placeholder = st.empty()
175
-
176
- # Admin Tools
177
  if "admin" in st.session_state.roles:
178
  st.divider()
179
  st.header("πŸ›‘οΈ Admin Tools")
180
  log_path = tracker.get_log_path()
181
  if log_path.exists():
182
- with open(log_path, "r") as f:
183
- log_data = f.read()
184
- st.download_button(
185
- label="πŸ“₯ Download Usage Logs",
186
- data=log_data,
187
- file_name=f"usage_log_{datetime.now().strftime('%Y-%m-%d')}.json",
188
- mime="application/json"
189
- )
190
-
191
  st.divider()
192
 
193
  st.header("🌲 Pinecone Settings")
194
- # Initialize Manager
195
  pc_key = os.getenv("PINECONE_API_KEY")
196
  if pc_key:
197
  pm = PineconeManager(pc_key)
198
  indexes = pm.list_indexes()
199
-
200
- # 1. INDEX SELECTOR
201
  selected_index = st.selectbox("Active Index", indexes)
202
  st.session_state.active_index = selected_index
203
 
204
- # 2. SAFETY CHECK VISUAL
205
  if selected_index:
206
- # Get current model dimension dynamically
207
  current_model = st.session_state.get("active_embed_model", "sentence-transformers/all-MiniLM-L6-v2")
208
  try:
209
  emb_fn = rag_engine.get_embedding_func(current_model)
210
- test_vec = emb_fn.embed_query("this is a test")
211
  active_model_dim = len(test_vec)
212
-
213
  is_compatible = pm.check_dimension_compatibility(selected_index, active_model_dim)
214
  if is_compatible:
215
  st.caption(f"βœ… Compatible with Model ({active_model_dim}d)")
@@ -218,47 +210,30 @@ with st.sidebar:
218
  except Exception as e:
219
  st.caption(f"⚠️ Could not verify dims: {e}")
220
 
221
- # 3. CREATE NEW INDEX
222
  with st.expander("Create New Index"):
223
  new_idx_name = st.text_input("Index Name")
224
-
225
- # NEW: Dimension Selector
226
- new_idx_dim = st.selectbox(
227
- "Vector Dimension",
228
- [384, 768, 1024, 1536, 3072],
229
- index=0, # Defaults to 384
230
- help="384=All-MiniLM, 768=MPNet/Nomic, 1536=OpenAI-Small, 3072=OpenAI-Large"
231
- )
232
-
233
  if st.button("Create"):
234
  with st.spinner("Provisioning Cloud Index..."):
235
- # We pass the selected dimension to the manager
236
  ok, msg = pm.create_index(new_idx_name, dimension=new_idx_dim)
237
  if ok:
238
  st.success(msg)
239
- time.sleep(2) # Give Pinecone a moment to propagate
240
  st.rerun()
241
- else:
242
- st.error(msg)
243
  else:
244
  st.warning("No Pinecone Key Found")
245
 
246
- # Model Selector
247
  st.header("🧠 Intelligence")
248
-
249
- # 1. EMBEDDING MODEL SELECTOR (New!)
250
  st.subheader("1. Embeddings (The Memory)")
251
  embed_options = {
252
  "Standard (All-MiniLM, 384d)": "sentence-transformers/all-MiniLM-L6-v2",
253
  "High-Perf (MPNet, 768d)": "sentence-transformers/all-mpnet-base-v2",
254
  "OpenAI Small (1536d)": "text-embedding-3-small"
255
  }
256
-
257
  embed_choice_label = st.selectbox("Select Embedding Model", list(embed_options.keys()))
258
- # Store the actual API string in session state
259
  st.session_state.active_embed_model = embed_options[embed_choice_label]
260
 
261
- # 2. LLM SELECTOR (The Brain)
262
  st.subheader("2. Chat Model (The Brain)")
263
  model_map = {
264
  "Granite 4 (IBM)": "granite4:latest",
@@ -267,51 +242,22 @@ with st.sidebar:
267
  }
268
  opts = list(model_map.keys())
269
  model_captions = ["Slower, free, private" for _ in opts]
270
-
271
- # Vision Key Input (User or Admin)
272
  is_admin = "admin" in st.session_state.roles
273
  user_key = None
274
  if not is_admin:
275
- user_key = st.text_input(
276
- "πŸ”“ Unlock GPT-4o (Enter API Key)",
277
- type="password",
278
- key=f"key_{st.session_state.username}",
279
- help="Required for Vision Mode and GPT-4o."
280
- )
281
  if user_key:
282
  st.session_state.user_openai_key = user_key
283
  st.caption("βœ… Key Active")
284
- else:
285
- st.session_state.user_openai_key = None
286
- else:
287
- # Admin defaults to system key, but we ensure state is clean
288
- st.session_state.user_openai_key = None
289
-
290
- # Unlock GPT-4o option
291
  if is_admin or st.session_state.get("user_openai_key"):
292
  opts.append("GPT-4o (Omni)")
293
  model_captions.append("Fast, smart, sends data to OpenAI")
294
-
295
  model_choice = st.radio("Select Model:", opts, captions=model_captions, key="model_selector_radio")
296
  st.info(f"Connected to: **{model_choice}**")
297
-
298
  st.divider()
299
- if st.session_state.authenticator:
300
- st.session_state.authenticator.logout(location='sidebar')
301
-
302
- st.divider()
303
- st.subheader("πŸ”§ System Diagnostics")
304
-
305
- if st.button("Run Integration Test"):
306
- with st.spinner("Running diagnostics..."):
307
- f = io.StringIO()
308
- try:
309
- with contextlib.redirect_stdout(f):
310
- run_tests()
311
- st.success("Tests Completed")
312
- st.code(f.getvalue(), language="text")
313
- except Exception as e:
314
- st.error(f"Test Execution Failed: {e}")
315
 
316
  update_sidebar_metrics()
317
 
@@ -323,28 +269,20 @@ tab1, tab2, tab3 = st.tabs(["πŸ’¬ Chat Playground", "πŸ“‚ Knowledge & Tools", "
323
  with tab1:
324
  st.header("Discussion & Analysis")
325
  if "messages" not in st.session_state: st.session_state.messages = []
326
-
327
  c1, c2 = st.columns([3, 1])
328
  with c1: st.caption(f"Active Model: **{st.session_state.get('model_selector_radio', 'Granite')}**")
329
  with c2: use_rag = st.toggle("Enable Knowledge Base", value=False)
330
-
331
  for msg in st.session_state.messages:
332
  with st.chat_message(msg["role"]): st.markdown(msg["content"])
333
-
334
  if prompt := st.chat_input("Input command..."):
335
  st.session_state.messages.append({"role": "user", "content": prompt})
336
  with st.chat_message("user"): st.markdown(prompt)
337
-
338
- # RAG Search
339
  context_txt = ""
340
  sys_p = "You are a helpful AI assistant."
341
-
342
  if use_rag:
343
- if not st.session_state.active_index:
344
- st.error("⚠️ Please select an Active Index in the sidebar first.")
345
  else:
346
  with st.spinner("Searching Knowledge Base..."):
347
- # FIXED: Added index_name parameter
348
  docs = rag_engine.search_knowledge_base(
349
  query=prompt,
350
  username=st.session_state.username,
@@ -358,74 +296,44 @@ with tab1:
358
  "If the Context contains the answer, output it clearly. "
359
  "If the Context does NOT contain the answer, simply state: "
360
  "'I cannot find that specific information in the documents provided.'"
361
- )
362
  for i, d in enumerate(docs):
363
  src = d.metadata.get('source', 'Unknown')
364
  context_txt += f"<document index='{i+1}' source='{src}'>\n{d.page_content}\n</document>\n"
365
-
366
- # Construct Payload
367
  if context_txt:
368
- final_prompt = (
369
- f"User Question: {prompt}\n\n"
370
- f"<context>\n{context_txt}\n</context>\n\n"
371
- "Instruction: Answer the question using the context above."
372
- )
373
- else:
374
- final_prompt = prompt
375
-
376
- # Generation
377
  with st.chat_message("assistant"):
378
  with st.spinner("Thinking..."):
379
  hist = [{"role":"system", "content":sys_p}] + st.session_state.messages[-6:-1] + [{"role":"user", "content":final_prompt}]
380
-
381
  resp, usage = query_model_universal(hist, 2000, model_choice, st.session_state.get("user_openai_key"))
382
  st.markdown(resp)
383
-
384
  if usage:
385
  m_name = "GPT-4o" if "GPT-4o" in model_choice else model_choice.split()[0]
386
  tracker.log_usage(m_name, usage["input"], usage["output"])
387
  update_sidebar_metrics()
388
-
389
  st.session_state.messages.append({"role": "assistant", "content": resp})
390
-
391
  if use_rag and context_txt:
392
- with st.expander("πŸ“š View Context Used"):
393
- st.text(context_txt)
394
 
395
  # === TAB 2: KNOWLEDGE & TOOLS ===
396
  with tab2:
397
  st.header("Document Processor")
398
-
399
  c1, c2 = st.columns([1, 1])
400
- with c1:
401
- uploaded_file = st.file_uploader("Upload File (PDF, PPT, Doc, Text)", type=["pdf", "docx", "pptx", "txt", "md"])
402
  with c2:
403
- use_vision = st.toggle("πŸ‘οΈ Enable Vision Mode", help="Use GPT-4o to read diagrams/tables. Requires API Key.")
404
- if use_vision and "GPT-4o" not in opts:
405
- st.warning("Vision requires OpenAI Access.")
406
 
407
  if uploaded_file:
408
- # Save temp
409
  temp_path = rag_engine.save_uploaded_file(uploaded_file, st.session_state.username)
410
-
411
- # ACTION BAR
412
  col_a, col_b, col_c = st.columns(3)
413
-
414
- # 1. ADD TO DB
415
  with col_a:
416
- chunk_strategy = st.selectbox(
417
- "Chunking Strategy",
418
- ["paragraph", "token"],
419
- help="Paragraph: Standard. Token: Dense text.",
420
- key="chunk_selector"
421
- )
422
-
423
  if st.button("πŸ“₯ Add to Knowledge Base", type="primary"):
424
- if not st.session_state.active_index:
425
- st.error("Please select an Active Index in the sidebar.")
426
  else:
427
  with st.spinner("Ingesting..."):
428
- # FIXED: Added index_name parameter
429
  ok, msg = rag_engine.ingest_file(
430
  file_path=temp_path,
431
  username=st.session_state.username,
@@ -433,64 +341,37 @@ with tab2:
433
  strategy=chunk_strategy,
434
  embed_model_name=st.session_state.active_embed_model
435
  )
436
-
437
- if ok:
438
- tracker.upload_user_db(st.session_state.username) # Auto-Sync
439
  st.success(msg)
440
- else:
441
- st.error(msg)
442
-
443
- # 2. SUMMARIZE
444
  with col_b:
445
- st.write("")
446
- st.write("")
447
  if st.button("πŸ“ Summarize Document"):
448
- with st.spinner("Reading & Summarizing..."):
449
  key = st.session_state.get("user_openai_key") or OPENAI_KEY
450
  class FileObj:
451
  def __init__(self, p, n): self.path=p; self.name=n
452
  def read(self):
453
  with open(self.path, "rb") as f: return f.read()
454
-
455
- raw = doc_loader.extract_text_from_file(
456
- FileObj(temp_path, uploaded_file.name),
457
- use_vision=use_vision, api_key=key
458
- )
459
-
460
- prompt = f"Summarize this document into a key executive brief:\n\n{raw[:20000]}"
461
  msgs = [{"role":"user", "content": prompt}]
462
  summ, usage = query_model_universal(msgs, 1000, model_choice, st.session_state.get("user_openai_key"))
463
-
464
- st.subheader("Summary Result")
465
- st.markdown(summ)
466
- if usage:
467
- m_name = "GPT-4o" if "GPT-4o" in model_choice else model_choice.split()[0]
468
- tracker.log_usage(m_name, usage["input"], usage["output"])
469
- update_sidebar_metrics()
470
-
471
- # 3. FLATTEN
472
  with col_c:
473
- st.write("")
474
- st.write("")
475
-
476
- if "flattened_result" not in st.session_state:
477
- st.session_state.flattened_result = None
478
-
479
  if st.button("πŸ“„ Flatten Context"):
480
  with st.spinner("Flattening..."):
481
  key = st.session_state.get("user_openai_key") or OPENAI_KEY
482
-
483
  with open(temp_path, "rb") as f:
484
  class Wrapper:
485
  def __init__(self, data, n): self.data=data; self.name=n
486
  def read(self): return self.data
487
- raw = doc_loader.extract_text_from_file(
488
- Wrapper(f.read(), uploaded_file.name), use_vision=use_vision, api_key=key
489
- )
490
-
491
  proc = OutlineProcessor(raw)
492
  items = proc.parse()
493
-
494
  out_txt = []
495
  bar = st.progress(0)
496
  for i, item in enumerate(items):
@@ -499,76 +380,52 @@ with tab2:
499
  res, _ = query_model_universal(m, 300, model_choice, st.session_state.get("user_openai_key"))
500
  out_txt.append(res)
501
  bar.progress((i+1)/len(items))
502
-
503
  final_flattened_text = "\n".join(out_txt)
504
- st.session_state.flattened_result = {
505
- "text": final_flattened_text,
506
- "source": f"{uploaded_file.name}_flat"
507
- }
508
  st.rerun()
509
-
510
  if st.session_state.flattened_result:
511
  res = st.session_state.flattened_result
512
  st.success("Flattening Complete!")
513
  st.text_area("Result", res["text"], height=200)
514
-
515
  if st.button("πŸ“₯ Index This Flattened Version"):
516
- if not st.session_state.active_index:
517
- st.error("Please select an Active Index in the sidebar.")
518
  else:
519
- with st.spinner("Indexing Flattened Text..."):
520
- # FIXED: Added index_name parameter
521
  ok, msg = rag_engine.process_and_add_text(
522
  text=res["text"],
523
  source_name=res["source"],
524
  username=st.session_state.username,
525
  index_name=st.session_state.active_index
526
  )
527
- if ok:
528
- tracker.upload_user_db(st.session_state.username)
529
  st.success(msg)
530
- else:
531
- st.error(msg)
532
-
533
  st.divider()
534
-
535
- # DB MANAGER
536
  st.subheader("Database Management")
537
- # 1. RESYNC BUTTON (The Fix)
538
  col_db_1, col_db_2 = st.columns([2, 1])
539
- with col_db_1:
540
- st.info("If Quiz Mode is failing, your local files might be missing (due to restart).")
541
  with col_db_2:
542
  if st.button("πŸ”„ Resync from Pinecone"):
543
- if not st.session_state.active_index:
544
- st.error("Select Index first.")
545
  else:
546
- with st.spinner("Downloading memories from Pinecone..."):
547
- ok, msg = rag_engine.rebuild_cache_from_pinecone(
548
- st.session_state.username,
549
- st.session_state.active_index
550
- )
551
  if ok: st.success(msg); time.sleep(1); st.rerun()
552
  else: st.error(msg)
553
  st.divider()
554
-
555
- # 2. FILE LIST
556
- # This reads from local cache so no index needed
557
  docs = rag_engine.list_documents(st.session_state.username)
558
-
559
  if docs:
560
  for d in docs:
561
  c1, c2 = st.columns([4,1])
562
  c1.text(f"πŸ“„ {d['filename']} (Cached)")
563
  if c2.button("πŸ—‘οΈ", key=d['source']):
564
- if not st.session_state.active_index:
565
- st.error("Select Index first.")
566
  else:
567
  rag_engine.delete_document(st.session_state.username, d['source'], st.session_state.active_index)
568
  tracker.upload_user_db(st.session_state.username)
569
  st.rerun()
570
- else:
571
- st.warning("Local Cache Empty. Click 'Resync' above if you have data in Pinecone.")
572
 
573
  # === TAB 3: QUIZ MODE ===
574
  with tab3:
@@ -577,104 +434,82 @@ with tab3:
577
  # 1. MODE SELECTION & RESET LOGIC
578
  col_mode, col_streak = st.columns([3, 1])
579
  with col_mode:
580
- quiz_mode = st.radio(
581
- "Select Quiz Mode:",
582
- ["⚑ Acronym Lightning Round", "πŸ“– Document Deep Dive"],
583
- horizontal=True
584
- )
585
-
586
- # Initialize Session State Variables if missing
587
- if "last_quiz_mode" not in st.session_state:
588
- st.session_state.last_quiz_mode = quiz_mode
589
-
590
- if "quiz_trigger" not in st.session_state:
591
- st.session_state.quiz_trigger = False
592
 
593
- # GHOST IMAGE FIX: Detect Mode Switch
594
- # If the user toggled the radio button since the last run, wipe the state.
595
  if st.session_state.last_quiz_mode != quiz_mode:
596
  st.session_state.quiz_state["active"] = False
597
  st.session_state.quiz_state["question_data"] = None
598
  st.session_state.quiz_state["feedback"] = None
599
  st.session_state.quiz_state["generated_question_text"] = ""
600
  st.session_state.last_quiz_mode = quiz_mode
601
- st.rerun() # Force a clean refresh immediately
602
 
603
- # Initialize Engine & Shortcut to State
604
  quiz = QuizEngine()
605
  qs = st.session_state.quiz_state
606
-
607
- # Display Streak
608
  with col_streak:
609
- st.metric("Current Streak", qs["streak"])
610
  if st.button("Reset"): qs["streak"] = 0
611
 
 
 
 
 
 
 
 
 
 
612
  st.divider()
613
 
614
- # --- GENERATION FUNCTION ---
615
  def generate_question():
616
  with st.spinner("Consulting the Board..."):
617
- # MODE A: ACRONYMS
618
  if "Acronym" in quiz_mode:
619
  q_data = quiz.get_random_acronym()
620
  if q_data:
621
- qs["active"] = True
622
- qs["question_data"] = q_data
623
- qs["feedback"] = None
624
- qs["generated_question_text"] = q_data["question"]
625
- else:
626
- st.error("No acronyms found! Run the extractor first.")
627
-
628
- # MODE B: DOCUMENTS
629
  else:
630
  valid_question_found = False
631
  attempts = 0
632
-
633
- # RETRY LOOP: Increased to 5 attempts to find a good chunk
634
  while not valid_question_found and attempts < 5:
635
  attempts += 1
636
- q_ctx = quiz.get_document_context(st.session_state.username)
 
637
 
638
  if q_ctx:
639
  prompt = quiz.construct_question_generation_prompt(q_ctx["context_text"])
640
- question_text, usage = query_model_universal(
641
- [{"role": "user", "content": prompt}],
642
- 300, model_choice, st.session_state.get("user_openai_key")
643
- )
644
-
645
- # LOGIC UPDATE: Check for 'UNABLE' instead of 'SKIP'
646
- # We also check length to ensure we didn't get a blank response
647
  if "UNABLE" not in question_text and len(question_text) > 10:
648
- valid_question_found = True
649
- qs["active"] = True
650
- qs["question_data"] = q_ctx
651
- qs["generated_question_text"] = question_text
652
- qs["feedback"] = None
653
 
654
  if not valid_question_found:
655
- st.warning("Could not generate a question after 5 attempts. The selected documents might be too sparse or formatted as pure data tables.")
 
656
 
657
- # 2. AUTO-TRIGGER (Chained Question Logic)
658
  if st.session_state.quiz_trigger:
659
  st.session_state.quiz_trigger = False
660
  generate_question()
661
  st.rerun()
662
 
663
- # 3. MANUAL START BUTTON
664
  if not qs["active"]:
665
  if st.button("πŸš€ Generate New Question", type="primary"):
666
  generate_question()
667
  st.rerun()
668
 
669
- # 4. QUIZ INTERFACE
670
  if qs["active"]:
671
  st.markdown(f"### {qs['generated_question_text']}")
 
672
 
673
- # Context Hint
674
- if "document" in qs.get("question_data", {}).get("type", ""):
675
- st.caption(f"Source: *{qs['question_data']['source_file']}*")
676
-
677
- # Answer Form
678
  with st.form(key="quiz_response"):
679
  user_ans = st.text_area("Your Answer:")
680
  sub = st.form_submit_button("Submit Answer")
@@ -682,57 +517,43 @@ with tab3:
682
  if sub and user_ans:
683
  with st.spinner("Grading..."):
684
  data = qs["question_data"]
685
-
686
- # Grading Logic Branch
687
- if data["type"] == "acronym":
688
- prompt = quiz.construct_acronym_grading_prompt(
689
- data["term"], data["correct_definition"], user_ans
690
- )
691
- else:
692
- prompt = quiz.construct_grading_prompt(
693
- qs["generated_question_text"], user_ans, data["context_text"]
694
- )
695
 
696
  msgs = [{"role": "user", "content": prompt}]
697
- grade, _ = query_model_universal(
698
- msgs, 500, model_choice, st.session_state.get("user_openai_key")
699
- )
700
-
701
  qs["feedback"] = grade
702
 
703
- # Streak Logic
704
- if "GRADE:** PASS" in grade or "GRADE:** Pass" in grade:
705
- qs["streak"] += 1
706
- elif "GRADE:** FAIL" in grade:
707
- qs["streak"] = 0
708
-
 
 
 
 
 
 
 
 
709
  st.rerun()
710
 
711
- # 5. FEEDBACK AREA (Deduplicated)
712
  if qs["feedback"]:
713
  st.divider()
714
- if "PASS" in qs["feedback"]:
715
- st.success("βœ… CORRECT")
716
  else:
717
- if "FAIL" in qs["feedback"]:
718
- st.error("❌ INCORRECT")
719
- else:
720
- st.warning("⚠️ PARTIAL / COMMENTARY")
721
-
722
  st.markdown(qs["feedback"])
723
 
724
- # Display Answer Key
725
  data = qs["question_data"]
726
- if data["type"] == "acronym":
727
- st.info(f"**Official Definition:** {data['correct_definition']}")
728
  elif data["type"] == "document":
729
- with st.expander("Show Source Text (Answer Key)"):
730
- st.info(data["context_text"])
731
 
732
- # Next Question Button
733
  if st.button("Next Question ➑️"):
734
  st.session_state.quiz_trigger = True
735
- qs["active"] = False
736
- qs["question_data"] = None
737
- qs["feedback"] = None
738
  st.rerun()
 
13
  from datetime import datetime
14
  from test_integration import run_tests
15
  from core.QuizEngine import QuizEngine
16
+ from core.PineconeManager import PineconeManager
17
 
18
  # --- CONFIGURATION ---
19
  st.set_page_config(page_title="Navy AI Toolkit", page_icon="βš“", layout="wide")
 
35
  "generated_question_text": ""
36
  }
37
 
38
+ # NEW: Quiz History for Study Guide
39
+ if "quiz_history" not in st.session_state:
40
+ st.session_state.quiz_history = []
41
+
42
  if "active_index" not in st.session_state:
43
  st.session_state.active_index = None
44
 
 
86
  # --- HELPER FUNCTIONS ---
87
  def query_model_universal(messages, max_tokens, model_choice, user_key=None):
88
  """Unified router for both Chat and Tools."""
 
89
  if "GPT-4o" in model_choice:
90
  key = user_key if user_key else OPENAI_KEY
91
  if not key: return "[Error: No OpenAI API Key]", None
 
99
  return resp.choices[0].message.content, usage
100
  except Exception as e:
101
  return f"[OpenAI Error: {e}]", None
 
 
102
  else:
103
  model_map = {
104
  "Granite 4 (IBM)": "granite4:latest",
 
110
 
111
  url = f"{API_URL_ROOT}/generate"
112
 
 
113
  hist = ""
114
  sys_msg = "You are a helpful assistant."
115
  for m in messages:
 
128
  return f"[Conn Error: {e}]", None
129
 
130
  def update_sidebar_metrics():
 
131
  if metric_placeholder:
132
  stats = tracker.get_daily_stats()
133
  u_stats = stats["users"].get(st.session_state.username, {"input":0, "output":0})
134
  metric_placeholder.metric("My Tokens Today", u_stats["input"] + u_stats["output"])
135
 
136
+ def generate_study_guide_md(history):
137
+ """Converts quiz history to a Markdown string."""
138
+ md = "# βš“ Study Guide\n\n"
139
+ md += f"Generated: {datetime.now().strftime('%Y-%m-%d %H:%M')}\n\n"
140
+ for item in history:
141
+ md += f"## Q: {item['question']}\n"
142
+ md += f"**Your Answer:** {item['user_answer']}\n\n"
143
+ md += f"**Grade:** {item['grade']}\n\n"
144
+ md += f"**Context/Correct Info:**\n> {item['context']}\n\n"
145
+ md += "---\n\n"
146
+ return md
147
+
148
  # --- LOGIN ---
149
  if "authentication_status" not in st.session_state or st.session_state["authentication_status"] is None:
150
  login_tab, register_tab = st.tabs(["πŸ”‘ Login", "πŸ“ Register"])
151
  with login_tab:
152
  if tracker.check_login():
 
153
  if "last_user" in st.session_state and st.session_state.last_user != st.session_state.username:
154
  st.session_state.messages = []
155
  st.session_state.user_openai_key = None
 
164
  new_email = st.text_input("Email")
165
  new_pwd = st.text_input("Password", type="password")
166
  invite = st.text_input("Invitation Passcode")
 
167
  if st.form_submit_button("Register"):
168
  success, msg = tracker.register_user(new_email, new_user, new_name, new_pwd, invite)
169
+ if success: st.success(msg)
170
+ else: st.error(msg)
 
 
 
171
  if not st.session_state.get("authentication_status"): st.stop()
172
 
173
  # --- SIDEBAR ---
 
175
  with st.sidebar:
176
  st.header("πŸ‘€ User Profile")
177
  st.write(f"Welcome, **{st.session_state.name}**")
 
178
  st.header("πŸ“Š Usage Tracker")
179
  metric_placeholder = st.empty()
 
 
180
  if "admin" in st.session_state.roles:
181
  st.divider()
182
  st.header("πŸ›‘οΈ Admin Tools")
183
  log_path = tracker.get_log_path()
184
  if log_path.exists():
185
+ with open(log_path, "r") as f: log_data = f.read()
186
+ st.download_button("πŸ“₯ Download Usage Logs", log_data, f"usage_log_{datetime.now().strftime('%Y-%m-%d')}.json", "application/json")
 
 
 
 
 
 
 
187
  st.divider()
188
 
189
  st.header("🌲 Pinecone Settings")
 
190
  pc_key = os.getenv("PINECONE_API_KEY")
191
  if pc_key:
192
  pm = PineconeManager(pc_key)
193
  indexes = pm.list_indexes()
 
 
194
  selected_index = st.selectbox("Active Index", indexes)
195
  st.session_state.active_index = selected_index
196
 
197
+ # 2. SAFETY CHECK VISUAL (FIXED)
198
  if selected_index:
199
+ # Check if the user has already selected a model; default to MiniLM if not
200
  current_model = st.session_state.get("active_embed_model", "sentence-transformers/all-MiniLM-L6-v2")
201
  try:
202
  emb_fn = rag_engine.get_embedding_func(current_model)
203
+ test_vec = emb_fn.embed_query("test")
204
  active_model_dim = len(test_vec)
 
205
  is_compatible = pm.check_dimension_compatibility(selected_index, active_model_dim)
206
  if is_compatible:
207
  st.caption(f"βœ… Compatible with Model ({active_model_dim}d)")
 
210
  except Exception as e:
211
  st.caption(f"⚠️ Could not verify dims: {e}")
212
 
 
213
  with st.expander("Create New Index"):
214
  new_idx_name = st.text_input("Index Name")
215
+ new_idx_dim = st.selectbox("Vector Dimension", [384, 768, 1024, 1536, 3072], index=0)
 
 
 
 
 
 
 
 
216
  if st.button("Create"):
217
  with st.spinner("Provisioning Cloud Index..."):
 
218
  ok, msg = pm.create_index(new_idx_name, dimension=new_idx_dim)
219
  if ok:
220
  st.success(msg)
221
+ time.sleep(2)
222
  st.rerun()
223
+ else: st.error(msg)
 
224
  else:
225
  st.warning("No Pinecone Key Found")
226
 
 
227
  st.header("🧠 Intelligence")
 
 
228
  st.subheader("1. Embeddings (The Memory)")
229
  embed_options = {
230
  "Standard (All-MiniLM, 384d)": "sentence-transformers/all-MiniLM-L6-v2",
231
  "High-Perf (MPNet, 768d)": "sentence-transformers/all-mpnet-base-v2",
232
  "OpenAI Small (1536d)": "text-embedding-3-small"
233
  }
 
234
  embed_choice_label = st.selectbox("Select Embedding Model", list(embed_options.keys()))
 
235
  st.session_state.active_embed_model = embed_options[embed_choice_label]
236
 
 
237
  st.subheader("2. Chat Model (The Brain)")
238
  model_map = {
239
  "Granite 4 (IBM)": "granite4:latest",
 
242
  }
243
  opts = list(model_map.keys())
244
  model_captions = ["Slower, free, private" for _ in opts]
 
 
245
  is_admin = "admin" in st.session_state.roles
246
  user_key = None
247
  if not is_admin:
248
+ user_key = st.text_input("πŸ”“ Unlock GPT-4o (Enter API Key)", type="password", key=f"key_{st.session_state.username}")
 
 
 
 
 
249
  if user_key:
250
  st.session_state.user_openai_key = user_key
251
  st.caption("βœ… Key Active")
252
+ else: st.session_state.user_openai_key = None
253
+ else: st.session_state.user_openai_key = None
 
 
 
 
 
254
  if is_admin or st.session_state.get("user_openai_key"):
255
  opts.append("GPT-4o (Omni)")
256
  model_captions.append("Fast, smart, sends data to OpenAI")
 
257
  model_choice = st.radio("Select Model:", opts, captions=model_captions, key="model_selector_radio")
258
  st.info(f"Connected to: **{model_choice}**")
 
259
  st.divider()
260
+ if st.session_state.authenticator: st.session_state.authenticator.logout(location='sidebar')
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
261
 
262
  update_sidebar_metrics()
263
 
 
269
  with tab1:
270
  st.header("Discussion & Analysis")
271
  if "messages" not in st.session_state: st.session_state.messages = []
 
272
  c1, c2 = st.columns([3, 1])
273
  with c1: st.caption(f"Active Model: **{st.session_state.get('model_selector_radio', 'Granite')}**")
274
  with c2: use_rag = st.toggle("Enable Knowledge Base", value=False)
 
275
  for msg in st.session_state.messages:
276
  with st.chat_message(msg["role"]): st.markdown(msg["content"])
 
277
  if prompt := st.chat_input("Input command..."):
278
  st.session_state.messages.append({"role": "user", "content": prompt})
279
  with st.chat_message("user"): st.markdown(prompt)
 
 
280
  context_txt = ""
281
  sys_p = "You are a helpful AI assistant."
 
282
  if use_rag:
283
+ if not st.session_state.active_index: st.error("⚠️ Please select an Active Index in the sidebar first.")
 
284
  else:
285
  with st.spinner("Searching Knowledge Base..."):
 
286
  docs = rag_engine.search_knowledge_base(
287
  query=prompt,
288
  username=st.session_state.username,
 
296
  "If the Context contains the answer, output it clearly. "
297
  "If the Context does NOT contain the answer, simply state: "
298
  "'I cannot find that specific information in the documents provided.'"
299
+ )"
300
  for i, d in enumerate(docs):
301
  src = d.metadata.get('source', 'Unknown')
302
  context_txt += f"<document index='{i+1}' source='{src}'>\n{d.page_content}\n</document>\n"
 
 
303
  if context_txt:
304
+ final_prompt = f"User Question: {prompt}\n\n<context>\n{context_txt}\n</context>\n\nInstruction: Answer using the context above."
305
+ else: final_prompt = prompt
 
 
 
 
 
 
 
306
  with st.chat_message("assistant"):
307
  with st.spinner("Thinking..."):
308
  hist = [{"role":"system", "content":sys_p}] + st.session_state.messages[-6:-1] + [{"role":"user", "content":final_prompt}]
 
309
  resp, usage = query_model_universal(hist, 2000, model_choice, st.session_state.get("user_openai_key"))
310
  st.markdown(resp)
 
311
  if usage:
312
  m_name = "GPT-4o" if "GPT-4o" in model_choice else model_choice.split()[0]
313
  tracker.log_usage(m_name, usage["input"], usage["output"])
314
  update_sidebar_metrics()
 
315
  st.session_state.messages.append({"role": "assistant", "content": resp})
 
316
  if use_rag and context_txt:
317
+ with st.expander("πŸ“š View Context Used"): st.text(context_txt)
 
318
 
319
  # === TAB 2: KNOWLEDGE & TOOLS ===
320
  with tab2:
321
  st.header("Document Processor")
 
322
  c1, c2 = st.columns([1, 1])
323
+ with c1: uploaded_file = st.file_uploader("Upload File (PDF, PPT, Doc, Text)", type=["pdf", "docx", "pptx", "txt", "md"])
 
324
  with c2:
325
+ use_vision = st.toggle("πŸ‘οΈ Enable Vision Mode", help="Use GPT-4o to read diagrams/tables.")
326
+ if use_vision and "GPT-4o" not in opts: st.warning("Vision requires OpenAI Access.")
 
327
 
328
  if uploaded_file:
 
329
  temp_path = rag_engine.save_uploaded_file(uploaded_file, st.session_state.username)
 
 
330
  col_a, col_b, col_c = st.columns(3)
 
 
331
  with col_a:
332
+ chunk_strategy = st.selectbox("Chunking Strategy", ["paragraph", "token"], key="chunk_selector")
 
 
 
 
 
 
333
  if st.button("πŸ“₯ Add to Knowledge Base", type="primary"):
334
+ if not st.session_state.active_index: st.error("Please select an Active Index.")
 
335
  else:
336
  with st.spinner("Ingesting..."):
 
337
  ok, msg = rag_engine.ingest_file(
338
  file_path=temp_path,
339
  username=st.session_state.username,
 
341
  strategy=chunk_strategy,
342
  embed_model_name=st.session_state.active_embed_model
343
  )
344
+ if ok:
345
+ tracker.upload_user_db(st.session_state.username)
 
346
  st.success(msg)
347
+ else: st.error(msg)
 
 
 
348
  with col_b:
349
+ st.write(""); st.write("")
 
350
  if st.button("πŸ“ Summarize Document"):
351
+ with st.spinner("Reading..."):
352
  key = st.session_state.get("user_openai_key") or OPENAI_KEY
353
  class FileObj:
354
  def __init__(self, p, n): self.path=p; self.name=n
355
  def read(self):
356
  with open(self.path, "rb") as f: return f.read()
357
+ raw = doc_loader.extract_text_from_file(FileObj(temp_path, uploaded_file.name), use_vision=use_vision, api_key=key)
358
+ prompt = f"Summarize this document:\n\n{raw[:20000]}"
 
 
 
 
 
359
  msgs = [{"role":"user", "content": prompt}]
360
  summ, usage = query_model_universal(msgs, 1000, model_choice, st.session_state.get("user_openai_key"))
361
+ st.subheader("Summary Result"); st.markdown(summ)
 
 
 
 
 
 
 
 
362
  with col_c:
363
+ st.write(""); st.write("")
364
+ if "flattened_result" not in st.session_state: st.session_state.flattened_result = None
 
 
 
 
365
  if st.button("πŸ“„ Flatten Context"):
366
  with st.spinner("Flattening..."):
367
  key = st.session_state.get("user_openai_key") or OPENAI_KEY
 
368
  with open(temp_path, "rb") as f:
369
  class Wrapper:
370
  def __init__(self, data, n): self.data=data; self.name=n
371
  def read(self): return self.data
372
+ raw = doc_loader.extract_text_from_file(Wrapper(f.read(), uploaded_file.name), use_vision=use_vision, api_key=key)
 
 
 
373
  proc = OutlineProcessor(raw)
374
  items = proc.parse()
 
375
  out_txt = []
376
  bar = st.progress(0)
377
  for i, item in enumerate(items):
 
380
  res, _ = query_model_universal(m, 300, model_choice, st.session_state.get("user_openai_key"))
381
  out_txt.append(res)
382
  bar.progress((i+1)/len(items))
 
383
  final_flattened_text = "\n".join(out_txt)
384
+ st.session_state.flattened_result = {"text": final_flattened_text, "source": f"{uploaded_file.name}_flat"}
 
 
 
385
  st.rerun()
 
386
  if st.session_state.flattened_result:
387
  res = st.session_state.flattened_result
388
  st.success("Flattening Complete!")
389
  st.text_area("Result", res["text"], height=200)
 
390
  if st.button("πŸ“₯ Index This Flattened Version"):
391
+ if not st.session_state.active_index: st.error("Please select an Active Index.")
 
392
  else:
393
+ with st.spinner("Indexing..."):
 
394
  ok, msg = rag_engine.process_and_add_text(
395
  text=res["text"],
396
  source_name=res["source"],
397
  username=st.session_state.username,
398
  index_name=st.session_state.active_index
399
  )
400
+ if ok:
401
+ tracker.upload_user_db(st.session_state.username)
402
  st.success(msg)
403
+ else: st.error(msg)
 
 
404
  st.divider()
 
 
405
  st.subheader("Database Management")
 
406
  col_db_1, col_db_2 = st.columns([2, 1])
407
+ with col_db_1: st.info("If Quiz Mode is failing, your local files might be missing.")
 
408
  with col_db_2:
409
  if st.button("πŸ”„ Resync from Pinecone"):
410
+ if not st.session_state.active_index: st.error("Select Index first.")
 
411
  else:
412
+ with st.spinner("Downloading memories..."):
413
+ ok, msg = rag_engine.rebuild_cache_from_pinecone(st.session_state.username, st.session_state.active_index)
 
 
 
414
  if ok: st.success(msg); time.sleep(1); st.rerun()
415
  else: st.error(msg)
416
  st.divider()
 
 
 
417
  docs = rag_engine.list_documents(st.session_state.username)
 
418
  if docs:
419
  for d in docs:
420
  c1, c2 = st.columns([4,1])
421
  c1.text(f"πŸ“„ {d['filename']} (Cached)")
422
  if c2.button("πŸ—‘οΈ", key=d['source']):
423
+ if not st.session_state.active_index: st.error("Select Index first.")
 
424
  else:
425
  rag_engine.delete_document(st.session_state.username, d['source'], st.session_state.active_index)
426
  tracker.upload_user_db(st.session_state.username)
427
  st.rerun()
428
+ else: st.warning("Local Cache Empty. Click 'Resync' above if you have data in Pinecone.")
 
429
 
430
  # === TAB 3: QUIZ MODE ===
431
  with tab3:
 
434
  # 1. MODE SELECTION & RESET LOGIC
435
  col_mode, col_streak = st.columns([3, 1])
436
  with col_mode:
437
+ quiz_mode = st.radio("Select Quiz Mode:", ["⚑ Acronym Lightning Round", "πŸ“– Document Deep Dive"], horizontal=True)
438
+
439
+ # New: Focus Topic Input
440
+ if "Document" in quiz_mode:
441
+ focus_topic = st.text_input("🎯 Focus Topic (Optional)", placeholder="e.g., PPBE, Shipyards, Radar...", help="Leave empty for random questions.")
442
+ else:
443
+ focus_topic = None
444
+
445
+ if "last_quiz_mode" not in st.session_state: st.session_state.last_quiz_mode = quiz_mode
446
+ if "quiz_trigger" not in st.session_state: st.session_state.quiz_trigger = False
 
 
447
 
 
 
448
  if st.session_state.last_quiz_mode != quiz_mode:
449
  st.session_state.quiz_state["active"] = False
450
  st.session_state.quiz_state["question_data"] = None
451
  st.session_state.quiz_state["feedback"] = None
452
  st.session_state.quiz_state["generated_question_text"] = ""
453
  st.session_state.last_quiz_mode = quiz_mode
454
+ st.rerun()
455
 
 
456
  quiz = QuizEngine()
457
  qs = st.session_state.quiz_state
458
+
 
459
  with col_streak:
460
+ st.metric("Streak", qs["streak"])
461
  if st.button("Reset"): qs["streak"] = 0
462
 
463
+ # New: Study Guide Download
464
+ if st.session_state.quiz_history:
465
+ with st.expander(f"πŸ“š Review Study Guide ({len(st.session_state.quiz_history)} items)"):
466
+ st.download_button(
467
+ "πŸ“₯ Download Markdown",
468
+ generate_study_guide_md(st.session_state.quiz_history),
469
+ f"StudyGuide_{datetime.now().strftime('%Y%m%d')}.md"
470
+ )
471
+
472
  st.divider()
473
 
 
474
  def generate_question():
475
  with st.spinner("Consulting the Board..."):
 
476
  if "Acronym" in quiz_mode:
477
  q_data = quiz.get_random_acronym()
478
  if q_data:
479
+ qs["active"] = True; qs["question_data"] = q_data; qs["feedback"] = None; qs["generated_question_text"] = q_data["question"]
480
+ else: st.error("No acronyms found! Run the extractor first.")
 
 
 
 
 
 
481
  else:
482
  valid_question_found = False
483
  attempts = 0
 
 
484
  while not valid_question_found and attempts < 5:
485
  attempts += 1
486
+ # Pass the focus topic here!
487
+ q_ctx = quiz.get_document_context(st.session_state.username, topic_filter=focus_topic)
488
 
489
  if q_ctx:
490
  prompt = quiz.construct_question_generation_prompt(q_ctx["context_text"])
491
+ question_text, usage = query_model_universal([{"role": "user", "content": prompt}], 300, model_choice, st.session_state.get("user_openai_key"))
 
 
 
 
 
 
492
  if "UNABLE" not in question_text and len(question_text) > 10:
493
+ valid_question_found = True; qs["active"] = True; qs["question_data"] = q_ctx; qs["generated_question_text"] = question_text; qs["feedback"] = None
 
 
 
 
494
 
495
  if not valid_question_found:
496
+ if focus_topic: st.warning(f"No documents found containing '{focus_topic}'. Try a different keyword.")
497
+ else: st.warning("Could not generate a question. Documents may be too sparse.")
498
 
 
499
  if st.session_state.quiz_trigger:
500
  st.session_state.quiz_trigger = False
501
  generate_question()
502
  st.rerun()
503
 
 
504
  if not qs["active"]:
505
  if st.button("πŸš€ Generate New Question", type="primary"):
506
  generate_question()
507
  st.rerun()
508
 
 
509
  if qs["active"]:
510
  st.markdown(f"### {qs['generated_question_text']}")
511
+ if "document" in qs.get("question_data", {}).get("type", ""): st.caption(f"Source: *{qs['question_data']['source_file']}*")
512
 
 
 
 
 
 
513
  with st.form(key="quiz_response"):
514
  user_ans = st.text_area("Your Answer:")
515
  sub = st.form_submit_button("Submit Answer")
 
517
  if sub and user_ans:
518
  with st.spinner("Grading..."):
519
  data = qs["question_data"]
520
+ if data["type"] == "acronym": prompt = quiz.construct_acronym_grading_prompt(data["term"], data["correct_definition"], user_ans)
521
+ else: prompt = quiz.construct_grading_prompt(qs["generated_question_text"], user_ans, data["context_text"])
 
 
 
 
 
 
 
 
522
 
523
  msgs = [{"role": "user", "content": prompt}]
524
+ grade, _ = query_model_universal(msgs, 500, model_choice, st.session_state.get("user_openai_key"))
 
 
 
525
  qs["feedback"] = grade
526
 
527
+ # Update Streak
528
+ is_pass = "PASS" in grade
529
+ if is_pass: qs["streak"] += 1
530
+ elif "FAIL" in grade: qs["streak"] = 0
531
+
532
+ # Save to History
533
+ correct_info = data['correct_definition'] if data['type'] == 'acronym' else data['context_text']
534
+ st.session_state.quiz_history.append({
535
+ "question": qs["generated_question_text"],
536
+ "user_answer": user_ans,
537
+ "grade": "PASS" if is_pass else "FAIL",
538
+ "context": correct_info
539
+ })
540
+
541
  st.rerun()
542
 
 
543
  if qs["feedback"]:
544
  st.divider()
545
+ if "PASS" in qs["feedback"]: st.success("βœ… CORRECT")
 
546
  else:
547
+ if "FAIL" in qs["feedback"]: st.error("❌ INCORRECT")
548
+ else: st.warning("⚠️ PARTIAL / COMMENTARY")
 
 
 
549
  st.markdown(qs["feedback"])
550
 
 
551
  data = qs["question_data"]
552
+ if data["type"] == "acronym": st.info(f"**Official Definition:** {data['correct_definition']}")
 
553
  elif data["type"] == "document":
554
+ with st.expander("Show Source Text (Answer Key)"): st.info(data["context_text"])
 
555
 
 
556
  if st.button("Next Question ➑️"):
557
  st.session_state.quiz_trigger = True
558
+ qs["active"] = False; qs["question_data"] = None; qs["feedback"] = None
 
 
559
  st.rerun()