NavyDevilDoc commited on
Commit
6d20f65
Β·
verified Β·
1 Parent(s): e0f2368

Update src/app.py

Browse files

refactored to add the document loader, add document summarization, and remove email builder and prompt writer

Files changed (1) hide show
  1. src/app.py +275 -480
src/app.py CHANGED
@@ -1,46 +1,133 @@
1
  import streamlit as st
2
  import requests
3
  import os
4
- import unicodedata
5
- import resources
 
6
  import tracker
7
- import rag_engine
 
8
  from openai import OpenAI
9
  from datetime import datetime
10
 
11
  # --- CONFIGURATION ---
12
  st.set_page_config(page_title="Navy AI Toolkit", page_icon="βš“", layout="wide")
13
 
14
- # 1. SETUP CREDENTIALS
15
- API_URL_ROOT = os.getenv("API_URL") # For Ollama models
16
- OPENAI_KEY = os.getenv("OPENAI_API_KEY") # For GPT-4o
17
 
18
  # --- INITIALIZATION ---
19
  if "roles" not in st.session_state:
20
  st.session_state.roles = []
21
 
22
- # --- LOGIN / REGISTER LOGIC ---
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
23
  if "authentication_status" not in st.session_state or st.session_state["authentication_status"] is None:
24
- # If not logged in, show tabs
25
  login_tab, register_tab = st.tabs(["πŸ”‘ Login", "πŸ“ Register"])
26
-
27
  with login_tab:
28
- is_logged_in = tracker.check_login()
29
- if is_logged_in:
30
- # Check if a different user was previously logged in
31
  if "last_user" in st.session_state and st.session_state.last_user != st.session_state.username:
32
- # WIPE EVERYTHING
33
  st.session_state.messages = []
34
- st.session_state.email_draft = ""
35
  st.session_state.user_openai_key = None
36
-
37
- # Update the tracker
38
  st.session_state.last_user = st.session_state.username
39
-
40
- # Download DB and Refresh
41
  tracker.download_user_db(st.session_state.username)
42
- st.rerun() # Refresh to show the app
43
-
44
  with register_tab:
45
  st.header("Create Account")
46
  with st.form("reg_form"):
@@ -56,16 +143,11 @@ if "authentication_status" not in st.session_state or st.session_state["authenti
56
  st.success(msg)
57
  else:
58
  st.error(msg)
 
 
59
 
60
- # Stop execution if not logged in
61
- if not st.session_state.get("authentication_status"):
62
- st.stop()
63
-
64
- # --- GLOBAL PLACEHOLDERS ---
65
  metric_placeholder = None
66
- admin_metric_placeholder = None
67
-
68
- # --- SIDEBAR (CONSOLIDATED) ---
69
  with st.sidebar:
70
  st.header("πŸ‘€ User Profile")
71
  st.write(f"Welcome, **{st.session_state.name}**")
@@ -77,8 +159,6 @@ with st.sidebar:
77
  if "admin" in st.session_state.roles:
78
  st.divider()
79
  st.header("πŸ›‘οΈ Admin Tools")
80
- admin_metric_placeholder = st.empty()
81
-
82
  log_path = tracker.get_log_path()
83
  if log_path.exists():
84
  with open(log_path, "r") as f:
@@ -89,502 +169,217 @@ with st.sidebar:
89
  file_name=f"usage_log_{datetime.now().strftime('%Y-%m-%d')}.json",
90
  mime="application/json"
91
  )
92
- else:
93
- st.warning("No logs found yet.")
94
-
95
- # Logout
96
- if "authenticator" in st.session_state:
97
- st.session_state.authenticator.logout(location='sidebar')
98
 
99
  st.divider()
100
 
101
- # --- MODEL SELECTOR ---
102
- st.header("🧠 Model Selector")
103
-
104
  model_map = {
105
- "Granite 4 (IBM)": "granite4:latest",
106
  "Llama 3.2 (Meta)": "llama3.2:latest",
107
  "Gemma 3 (Google)": "gemma3:latest"
108
  }
 
 
109
 
110
- model_options = list(model_map.keys())
111
- model_captions = ["Slower for now, but free and private" for _ in model_options]
112
-
113
- # 2. CHECK FOR GPT-4o ACCESS (Admin OR User Key)
114
- # We moved the input UP so the user can unlock the option immediately
115
-
116
- # Check if user is admin
117
  is_admin = "admin" in st.session_state.roles
118
-
119
- # Input for Non-Admins
120
- user_api_key = None
121
  if not is_admin:
122
- user_api_key = st.text_input(
123
  "πŸ”“ Unlock GPT-4o (Enter API Key)",
124
  type="password",
125
- help="Enter your OpenAI API key to access GPT-4o. Press Enter to apply.",
126
- key=f"user_key_{st.session_state.username}"
127
  )
128
- if user_api_key:
129
- st.session_state.user_openai_key = user_api_key
130
  st.caption("βœ… Key Active")
131
- else:
132
  st.session_state.user_openai_key = None
133
  else:
 
134
  st.session_state.user_openai_key = None
135
-
136
- # 3. DYNAMICALLY ADD GPT-4o TO THE LIST
137
- # If Admin OR if they just entered a key, show the option
138
  if is_admin or st.session_state.get("user_openai_key"):
139
- model_options.append("GPT-4o (Omni)")
140
  model_captions.append("Fast, smart, sends data to OpenAI")
141
-
142
- # 4. RENDER THE SELECTOR
143
- model_choice = st.radio(
144
- "Choose your Intelligence:",
145
- model_options,
146
- captions=model_captions,
147
- key="model_selector_radio"
148
- )
149
  st.info(f"Connected to: **{model_choice}**")
150
 
151
  st.divider()
152
- st.header("βš™οΈ Controls")
153
- max_len = st.slider("Max Response Length (Tokens)", 100, 2000, 500)
154
-
155
- # --- HELPER FUNCTIONS ---
156
- def update_sidebar_metrics():
157
- """Refreshes the global placeholders defined in the sidebar."""
158
- if metric_placeholder is None:
159
- return
160
-
161
- stats = tracker.get_daily_stats()
162
- user_stats = stats["users"].get(st.session_state.username, {"input":0, "output":0})
163
-
164
- metric_placeholder.metric("My Tokens Today", user_stats["input"] + user_stats["output"])
165
-
166
- if "admin" in st.session_state.roles and admin_metric_placeholder is not None:
167
- admin_metric_placeholder.metric("Team Total Today", stats["total_tokens"])
168
 
169
- # Call metrics once on load
170
  update_sidebar_metrics()
171
 
172
- def query_local_model(messages, max_tokens, model_name):
173
- if not API_URL_ROOT:
174
- return "Error: API_URL not set.", None
175
-
176
- url = API_URL_ROOT + "/generate"
177
-
178
- # --- FLATTEN MESSAGE HISTORY ---
179
- formatted_history = ""
180
- system_persona = "You are a helpful assistant." # Default
181
-
182
- for msg in messages:
183
- if msg['role'] == 'system':
184
- system_persona = msg['content']
185
- elif msg['role'] == 'user':
186
- formatted_history += f"User: {msg['content']}\n"
187
- elif msg['role'] == 'assistant':
188
- formatted_history += f"Assistant: {msg['content']}\n"
189
-
190
- # Append the "Assistant:" prompt at the end to cue the model
191
- formatted_history += "Assistant: "
192
-
193
- payload = {
194
- "text": formatted_history,
195
- "persona": system_persona,
196
- "max_tokens": max_tokens,
197
- "model": model_name
198
- }
199
-
200
- try:
201
- response = requests.post(url, json=payload, timeout=300)
202
-
203
- if response.status_code == 200:
204
- response_data = response.json()
205
- ans = response_data.get("response", "")
206
- usage = response_data.get("usage", {"input":0, "output":0})
207
- return ans, usage
208
-
209
- return f"Error {response.status_code}: {response.text}", None
210
-
211
- except Exception as e:
212
- return f"Connection Error: {e}", None
213
-
214
- def query_openai_model(messages, max_tokens):
215
- # 1. Check for User Key first
216
- api_key_to_use = st.session_state.get("user_openai_key")
217
-
218
- # 2. Fallback to System Key
219
- if not api_key_to_use:
220
- api_key_to_use = OPENAI_KEY
221
-
222
- # 3. Final Safety Check
223
- if not api_key_to_use:
224
- return "Error: No API Key available. Please enter one in the sidebar.", None
225
-
226
- client = OpenAI(api_key=api_key_to_use)
227
-
228
- try:
229
- response = client.chat.completions.create(
230
- model="gpt-4o",
231
- max_tokens=max_tokens,
232
- messages=messages,
233
- temperature=0.3
234
- )
235
- usage_obj = response.usage
236
- usage_dict = {"input": usage_obj.prompt_tokens, "output": usage_obj.completion_tokens}
237
- return response.choices[0].message.content, usage_dict
238
-
239
- except Exception as e:
240
- return f"OpenAI Error: {e}", None
241
 
242
- def clean_text(text):
243
- if not text: return ""
244
- text = unicodedata.normalize('NFKC', text)
245
- replacements = {'β€œ': '"', '”': '"', 'β€˜': "'", '’': "'", '–': '-', 'β€”': '-', '…': '...', '\u00a0': ' '}
246
- for old, new in replacements.items():
247
- text = text.replace(old, new)
248
- return text.strip()
249
-
250
- def ask_ai(user_prompt, system_persona, max_tokens):
251
- # 1. Standardize Input: Convert the strings into the Message List format
252
- messages_payload = [
253
- {"role": "system", "content": system_persona},
254
- {"role": "user", "content": user_prompt}
255
- ]
256
-
257
- # 2. Routing Logic
258
- if "GPT-4o" in model_choice:
259
- return query_openai_model(messages_payload, max_tokens)
260
- else:
261
- technical_name = model_map[model_choice]
262
- return query_local_model(messages_payload, max_tokens, technical_name)
263
-
264
- # --- MAIN UI ---
265
- st.title("AI Toolkit")
266
- tab1, tab2, tab3, tab4 = st.tabs(["πŸ“§ Email Builder", "πŸ’¬ Chat Playground", "πŸ› οΈ Prompt Architect", "πŸ“š Knowledge Base"])
267
-
268
- # --- TAB 1: EMAIL BUILDER ---
269
  with tab1:
270
- st.header("Structured Email Generator")
271
- if "email_draft" not in st.session_state:
272
- st.session_state.email_draft = ""
273
-
274
- st.subheader("1. Define the Voice")
275
- style_mode = st.radio("How should the AI write?", ["Use a Preset Persona", "Mimic My Style"], horizontal=True)
276
 
277
- selected_persona_instruction = ""
278
- if style_mode == "Use a Preset Persona":
279
- persona_name = st.selectbox("Select a Persona", list(resources.TONE_LIBRARY.keys()))
280
- selected_persona_instruction = resources.TONE_LIBRARY[persona_name]
281
- st.info(f"**System Instruction:** {selected_persona_instruction}")
282
- else:
283
- st.info("Upload 1-3 text files of your previous emails.")
284
- uploaded_style_files = st.file_uploader("Upload Samples (.txt)", type=["txt"], accept_multiple_files=True)
285
- if uploaded_style_files:
286
- style_context = ""
287
- for uploaded_file in uploaded_style_files:
288
- string_data = uploaded_file.read().decode("utf-8")
289
- style_context += f"---\n{string_data}\n---\n"
290
- selected_persona_instruction = f"Analyze these examples and mimic the style:\n{style_context}"
291
-
292
- st.divider()
293
- st.subheader("2. Details")
294
- c1, c2 = st.columns(2)
295
- with c1: recipient = st.text_input("Recipient")
296
- with c2: topic = st.text_input("Topic")
297
 
298
- st.caption("Content Source")
299
- input_method = st.toggle("Upload notes file?")
300
- raw_notes = ""
301
- if input_method:
302
- notes_file = st.file_uploader("Upload Notes (.txt)", type=["txt"])
303
- if notes_file: raw_notes = notes_file.read().decode("utf-8")
304
- else:
305
- raw_notes = st.text_area("Paste notes:", height=150)
306
-
307
- # Context Bar
308
- est_tokens = len(raw_notes) / 4
309
- st.progress(min(est_tokens / 128000, 1.0), text=f"Context: {int(est_tokens)} tokens")
310
-
311
- if st.button("Draft Email", type="primary"):
312
- if not raw_notes:
313
- st.warning("Please provide notes.")
314
- else:
315
- clean_notes = clean_text(raw_notes)
316
- with st.spinner(f"Drafting with {model_choice}..."):
317
- prompt = f"TASK: Write email.\nTO: {recipient}\nTOPIC: {topic}\nSTYLE: {selected_persona_instruction}\nDATA: {clean_notes}"
318
-
319
- reply, usage = ask_ai(prompt, "You are an expert ghostwriter.", max_len)
320
- st.session_state.email_draft = reply
321
-
322
- if usage:
323
- if "GPT-4o" in model_choice:
324
- m_name = "GPT-4o"
325
- else:
326
- m_name = model_choice.split(" ")[0]
327
- tracker.log_usage(m_name, usage["input"], usage["output"])
328
- update_sidebar_metrics()
329
-
330
- if st.session_state.email_draft:
331
- st.subheader("Draft Result")
332
- st.text_area("Copy your email:", value=st.session_state.email_draft, height=300)
333
-
334
- # --- TAB 2: CHAT PLAYGROUND ---
335
- with tab2:
336
- st.header("Choose Your Model and Start a Discussion")
337
-
338
- # --- INITIALIZE CHAT MEMORY (MUST BE DONE FIRST) ---
339
- if "messages" not in st.session_state:
340
- st.session_state.messages = []
341
-
342
- # --- CONTROLS AND METRICS ---
343
- c1, c2, c3 = st.columns([2, 1, 1])
344
- with c1:
345
- # FIX: Access the correct key from the sidebar widget
346
- # We default to the global variable 'model_choice' if state is missing
347
- selected_model_name = st.session_state.get('model_selector_radio', model_choice)
348
- st.caption(f"Active Model: **{selected_model_name}**")
349
 
350
- with c2:
351
- use_rag = st.toggle("πŸ”Œ Enable Knowledge Base", value=False)
352
-
353
- with c3:
354
- # --- NEW FEATURE: DOWNLOAD CHAT ---
355
- chat_log = ""
356
- for msg in st.session_state.messages:
357
- role = "USER" if msg['role'] == 'user' else "ASSISTANT"
358
- chat_log += f"[{role}]: {msg['content']}\n\n"
359
-
360
- if chat_log:
361
- st.download_button(
362
- label="πŸ’Ύ Save Chat",
363
- data=chat_log,
364
- file_name="mission_log.txt",
365
- mime="text/plain",
366
- help="Download the current conversation history."
367
- )
368
-
369
- st.divider()
370
-
371
- # --- DISPLAY CONVERSATION HISTORY ---
372
- for message in st.session_state.messages:
373
- with st.chat_message(message["role"]):
374
- st.markdown(message["content"])
375
-
376
- # --- CHAT INPUT HANDLING ---
377
- if prompt := st.chat_input("Ask a question..."):
378
- # 1. Display User Message and save to history
379
  st.session_state.messages.append({"role": "user", "content": prompt})
380
- with st.chat_message("user"):
381
- st.markdown(prompt)
382
-
383
- # 2. Default Configuration (Standard AI Mode)
384
- system_persona = "You are a helpful AI assistant. Answer the user's question to the best of your ability."
385
- final_user_content = prompt
386
- retrieved_docs = []
387
 
388
- # 3. Handle RAG Logic (Only if enabled)
389
  if use_rag:
390
- with st.spinner("🧠 Searching Knowledge Base..."):
391
- retrieved_docs = rag_engine.search_knowledge_base(
392
- prompt,
393
- st.session_state.username
394
- )
395
-
396
- if retrieved_docs:
397
- # RAG SUCCESS: Switch to Strict Navy Persona
398
- system_persona = (
399
- "You are a Navy Document Analyst. Your task is to answer the user's question "
400
- "using ONLY the Context provided below. "
401
- "If the answer is not present in the Context, return ONLY this exact phrase: "
402
- "'I cannot find that information in the provided documents.'"
403
  )
 
404
 
405
- # Format Context
406
- context_text = ""
407
- for doc in retrieved_docs:
408
- score = doc.metadata.get('relevance_score', 'N/A')
409
- src = os.path.basename(doc.metadata.get('source', 'Unknown'))
410
- context_text += f"---\nSOURCE: {src} (Rel: {score})\nTEXT: {doc.page_content}\n"
411
-
412
- # Augment User Prompt
413
- final_user_content = (
414
- f"User Question: {prompt}\n\n"
415
- f"Relevant Context:\n{context_text}\n\n"
416
- "Answer the question using the context provided."
417
- )
418
 
419
- # 4. Construct Payload (Now using the CORRECT persona)
420
- messages_payload = [{"role": "system", "content": system_persona}]
421
-
422
- # --- MEMORY LOGIC: SLIDING WINDOW ---
423
- history_depth = 8
424
- recent_history = st.session_state.messages[-(history_depth+1):-1]
425
- messages_payload.extend(recent_history)
426
-
427
- # Add the final (potentially augmented) user message to payload
428
- messages_payload.append({"role": "user", "content": final_user_content})
429
-
430
- # 5. Generate Response
431
  with st.chat_message("assistant"):
432
- with st.spinner(f"Thinking with {selected_model_name}..."):
433
- # Determine model ID
434
- model_id = ""
435
- ollama_map = {
436
- "Granite 4 (IBM)": "granite4:latest",
437
- "Llama 3.2 (Meta)": "llama3.2:latest",
438
- "Gemma 3 (Google)": "gemma3:latest"
439
- }
440
- for key, val in ollama_map.items():
441
- if key in selected_model_name:
442
- model_id = val
443
- break
444
 
445
- # ROUTING CHECK
446
- if not model_id and "gpt" in selected_model_name.lower():
447
- # If it's the GPT model choice
448
- response, usage = query_openai_model(messages_payload, max_len)
449
- elif model_id:
450
- # If it's the local Ollama model
451
- response, usage = query_local_model(messages_payload, max_len, model_id)
452
- else:
453
- response, usage = "Error: Could not determine model to use.", None
454
 
455
- st.markdown(response)
456
-
457
- # 6. Save Assistant Response
458
- st.session_state.messages.append({"role": "assistant", "content": response})
 
 
459
 
460
- # 7. Metrics & Context Display
461
- if usage:
462
- if "GPT-4o" in selected_model_name:
463
- m_name = "GPT-4o"
464
- else:
465
- m_name = selected_model_name.split(" ")[0]
466
- tracker.log_usage(m_name, usage["input"], usage["output"])
467
- update_sidebar_metrics()
468
-
469
- if use_rag and retrieved_docs:
470
  with st.expander("πŸ“š View Context Used"):
471
- for i, doc in enumerate(retrieved_docs):
472
- score = doc.metadata.get('relevance_score', 'N/A')
473
- src = os.path.basename(doc.metadata.get('source', 'Unknown'))
474
- st.caption(f"Rank {i+1} (Source: {src}, Rel: {score})")
475
- st.text(doc.page_content)
476
- st.divider()
477
-
478
- # --- TAB 3: PROMPT ARCHITECT ---
479
- with tab3:
480
- st.header("πŸ› οΈ Mega-Prompt Factory")
481
- st.info("Build standard templates for NIPRGPT.")
482
 
483
- c1, c2 = st.columns([1,1])
484
  with c1:
485
- st.subheader("1. Parameters")
486
- p = st.text_area("Persona", placeholder="Act as...", height=100)
487
- c = st.text_area("Context", placeholder="Background...", height=100)
488
- t = st.text_area("Task", placeholder="Action...", height=100)
489
- v = st.text_input("Placeholder Name", value="PASTE_DATA_HERE")
490
-
491
  with c2:
492
- st.subheader("2. Result")
493
- final = f"### ROLE\n{p}\n### CONTEXT\n{c}\n### TASK\n{t}\n### INPUT DATA\n\"\"\"\n[{v}]\n\"\"\""
494
- st.code(final, language="markdown")
495
- st.download_button("πŸ’Ύ Download .txt", final, "template.txt")
496
 
497
- # --- TAB 4: KNOWLEDGE BASE ---
498
- with tab4:
499
- st.header("🧠 Personal Knowledge Base")
500
- st.info(f"Managing knowledge for: **{st.session_state.username}**")
501
-
502
- # We no longer check 'is_admin' for the whole tab
503
- kb_tab1, kb_tab2 = st.tabs(["πŸ“€ Add Documents", "πŸ—‚οΈ Manage Database"])
504
-
505
- # --- SUB-TAB 1: UPLOAD (Unlocked for Everyone) ---
506
- with kb_tab1:
507
- st.subheader("Ingest New Knowledge")
508
- uploaded_file = st.file_uploader("Upload Instructions, Manuals, or Logs", type=["pdf", "docx", "txt", "md"])
509
 
510
- col1, col2 = st.columns([1, 2])
511
- with col1:
512
- chunk_strategy = st.selectbox(
513
- "Chunking Strategy",
514
- ["paragraph", "token", "page"],
515
- help="Paragraph: Manuals. Token: Dense text. Page: Forms."
516
- )
517
 
518
- if uploaded_file and st.button("Process & Add"):
519
- with st.spinner("Analyzing and Indexing..."):
520
- # 1. Save temp file
521
- temp_path = rag_engine.save_uploaded_file(uploaded_file)
522
-
523
- # 2. Process locally
524
- success, msg = rag_engine.process_and_add_document(
525
- temp_path,
526
- st.session_state.username,
527
- chunk_strategy
528
- )
529
-
530
- if success:
531
- # 3. FIX: SYNC TO CLOUD IMMEDIATELY
532
- with st.spinner("Backing up to Cloud..."):
533
- tracker.upload_user_db(st.session_state.username)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
534
 
535
- st.success(msg)
536
- st.rerun()
537
- else:
538
- st.error(f"Failed: {msg}")
539
-
540
- st.divider()
541
- st.subheader("πŸ”Ž Quick Test")
542
- test_query = st.text_input("Ask your brain something...")
543
- if test_query:
544
- results = rag_engine.search_knowledge_base(test_query, st.session_state.username)
545
- if not results:
546
- st.warning("No matches found.")
547
- for i, doc in enumerate(results):
548
- src_name = os.path.basename(doc.metadata.get('source', '?'))
549
- score = doc.metadata.get('relevance_score', 'N/A')
550
- with st.expander(f"Match {i+1}: {src_name} (Score: {score})"):
551
- st.write(doc.page_content)
552
 
553
- # --- SUB-TAB 2: MANAGE (Unlocked for Everyone) ---
554
- with kb_tab2:
555
- st.subheader("πŸ—„οΈ Database Inventory")
556
-
557
- docs = rag_engine.list_documents(st.session_state.username)
558
-
559
- if not docs:
560
- st.info("Your Knowledge Base is empty.")
561
- else:
562
- st.markdown(f"**Total Documents:** {len(docs)}")
563
-
564
- for doc in docs:
565
- c1, c2, c3, c4 = st.columns([3, 2, 1, 1])
566
- with c1:
567
- st.text(f"πŸ“„ {doc['filename']}")
568
- with c2:
569
- st.caption(f"βš™οΈ {doc.get('strategy', 'Unknown')}")
570
- with c3:
571
- st.caption(f"{doc['chunks']}")
572
- with c4:
573
- if st.button("πŸ—‘οΈ", key=doc['source'], help="Delete Document"):
574
- with st.spinner("Deleting..."):
575
- success, msg = rag_engine.delete_document(st.session_state.username, doc['source'])
576
- if success:
577
- tracker.upload_user_db(st.session_state.username)
578
- st.success(msg)
579
- st.rerun()
580
- else:
581
- st.error(msg)
582
-
583
- st.divider()
584
- with st.expander("🚨 Danger Zone"):
585
- # Allow ANY user to reset their OWN database
586
- if st.button("☒️ RESET MY DATABASE", type="primary"):
587
- success, msg = rag_engine.reset_knowledge_base(st.session_state.username)
588
- if success:
589
- st.success(msg)
590
- st.rerun()
 
1
  import streamlit as st
2
  import requests
3
  import os
4
+ import re
5
+ import io
6
+ import zipfile
7
  import tracker
8
+ import rag_engine
9
+ import doc_loader
10
  from openai import OpenAI
11
  from datetime import datetime
12
 
13
  # --- CONFIGURATION ---
14
  st.set_page_config(page_title="Navy AI Toolkit", page_icon="βš“", layout="wide")
15
 
16
+ API_URL_ROOT = os.getenv("API_URL")
17
+ OPENAI_KEY = os.getenv("OPENAI_API_KEY")
 
18
 
19
  # --- INITIALIZATION ---
20
  if "roles" not in st.session_state:
21
  st.session_state.roles = []
22
 
23
+ # --- FLATTENER LOGIC (Integrated) ---
24
+ class OutlineProcessor:
25
+ """Parses text outlines for the Flattener tool."""
26
+ def __init__(self, file_content):
27
+ self.raw_lines = file_content.split('\n')
28
+
29
+ def _is_list_item(self, line):
30
+ pattern = r"^\s*(\d+\.|[a-zA-Z]\.|-|\*)\s+"
31
+ return bool(re.match(pattern, line))
32
+
33
+ def _merge_multiline_items(self):
34
+ merged_lines = []
35
+ for line in self.raw_lines:
36
+ stripped = line.strip()
37
+ if not stripped: continue
38
+ if not merged_lines:
39
+ merged_lines.append(line)
40
+ continue
41
+ if not self._is_list_item(line):
42
+ merged_lines[-1] = merged_lines[-1].rstrip() + " " + stripped
43
+ else:
44
+ merged_lines.append(line)
45
+ return merged_lines
46
+
47
+ def parse(self):
48
+ clean_lines = self._merge_multiline_items()
49
+ stack = []
50
+ results = []
51
+ for line in clean_lines:
52
+ stripped = line.strip()
53
+ indent = len(line) - len(line.lstrip())
54
+ while stack and stack[-1]['indent'] >= indent:
55
+ stack.pop()
56
+ stack.append({'indent': indent, 'text': stripped})
57
+ if len(stack) > 1:
58
+ context_str = " > ".join([item['text'] for item in stack[:-1]])
59
+ else:
60
+ context_str = "ROOT"
61
+ results.append({"context": context_str, "target": stripped})
62
+ return results
63
+
64
+ # --- HELPER FUNCTIONS ---
65
+ def query_model_universal(messages, max_tokens, model_choice, user_key=None):
66
+ """Unified router for both Chat and Tools."""
67
+ # 1. OpenAI Path
68
+ if "GPT-4o" in model_choice:
69
+ key = user_key if user_key else OPENAI_KEY
70
+ if not key: return "[Error: No OpenAI API Key]", None
71
+
72
+ client = OpenAI(api_key=key)
73
+ try:
74
+ resp = client.chat.completions.create(
75
+ model="gpt-4o", max_tokens=max_tokens, messages=messages, temperature=0.3
76
+ )
77
+ usage = {"input": resp.usage.prompt_tokens, "output": resp.usage.completion_tokens}
78
+ return resp.choices[0].message.content, usage
79
+ except Exception as e:
80
+ return f"[OpenAI Error: {e}]", None
81
+
82
+ # 2. Local Path
83
+ else:
84
+ model_map = {
85
+ "Granite 4 (IBM)": "granite4:latest",
86
+ "Llama 3.2 (Meta)": "llama3.2:latest",
87
+ "Gemma 3 (Google)": "gemma3:latest"
88
+ }
89
+ tech_name = model_map.get(model_choice)
90
+ if not tech_name: return "[Error: Model Map Failed]", None
91
+
92
+ url = f"{API_URL_ROOT}/generate"
93
+
94
+ # Flatten history for Ollama
95
+ hist = ""
96
+ sys_msg = "You are a helpful assistant."
97
+ for m in messages:
98
+ if m['role']=='system': sys_msg = m['content']
99
+ elif m['role']=='user': hist += f"User: {m['content']}\n"
100
+ elif m['role']=='assistant': hist += f"Assistant: {m['content']}\n"
101
+ hist += "Assistant: "
102
+
103
+ try:
104
+ r = requests.post(url, json={"text": hist, "persona": sys_msg, "max_tokens": max_tokens, "model": tech_name}, timeout=300)
105
+ if r.status_code == 200:
106
+ d = r.json()
107
+ return d.get("response", ""), d.get("usage", {"input":0,"output":0})
108
+ return f"[Local Error {r.status_code}]", None
109
+ except Exception as e:
110
+ return f"[Conn Error: {e}]", None
111
+
112
+ def update_sidebar_metrics():
113
+ # Helper to safely update metrics if placeholder exists
114
+ if metric_placeholder:
115
+ stats = tracker.get_daily_stats()
116
+ u_stats = stats["users"].get(st.session_state.username, {"input":0, "output":0})
117
+ metric_placeholder.metric("My Tokens Today", u_stats["input"] + u_stats["output"])
118
+
119
+ # --- LOGIN ---
120
  if "authentication_status" not in st.session_state or st.session_state["authentication_status"] is None:
 
121
  login_tab, register_tab = st.tabs(["πŸ”‘ Login", "πŸ“ Register"])
 
122
  with login_tab:
123
+ if tracker.check_login():
124
+ # Session Isolation Logic
 
125
  if "last_user" in st.session_state and st.session_state.last_user != st.session_state.username:
 
126
  st.session_state.messages = []
 
127
  st.session_state.user_openai_key = None
 
 
128
  st.session_state.last_user = st.session_state.username
 
 
129
  tracker.download_user_db(st.session_state.username)
130
+ st.rerun()
 
131
  with register_tab:
132
  st.header("Create Account")
133
  with st.form("reg_form"):
 
143
  st.success(msg)
144
  else:
145
  st.error(msg)
146
+
147
+ if not st.session_state.get("authentication_status"): st.stop()
148
 
149
+ # --- SIDEBAR ---
 
 
 
 
150
  metric_placeholder = None
 
 
 
151
  with st.sidebar:
152
  st.header("πŸ‘€ User Profile")
153
  st.write(f"Welcome, **{st.session_state.name}**")
 
159
  if "admin" in st.session_state.roles:
160
  st.divider()
161
  st.header("πŸ›‘οΈ Admin Tools")
 
 
162
  log_path = tracker.get_log_path()
163
  if log_path.exists():
164
  with open(log_path, "r") as f:
 
169
  file_name=f"usage_log_{datetime.now().strftime('%Y-%m-%d')}.json",
170
  mime="application/json"
171
  )
 
 
 
 
 
 
172
 
173
  st.divider()
174
 
175
+ # Model Selector
176
+ st.header("🧠 Intelligence")
 
177
  model_map = {
178
+ "Granite 4 (IBM)": "granite4:latest",
179
  "Llama 3.2 (Meta)": "llama3.2:latest",
180
  "Gemma 3 (Google)": "gemma3:latest"
181
  }
182
+ opts = list(model_map.keys())
183
+ model_captions = ["Slower, free, private" for _ in opts]
184
 
185
+ # Vision Key Input (User or Admin)
 
 
 
 
 
 
186
  is_admin = "admin" in st.session_state.roles
187
+ user_key = None
 
 
188
  if not is_admin:
189
+ user_key = st.text_input(
190
  "πŸ”“ Unlock GPT-4o (Enter API Key)",
191
  type="password",
192
+ key=f"key_{st.session_state.username}",
193
+ help="Required for Vision Mode and GPT-4o."
194
  )
195
+ if user_key:
196
+ st.session_state.user_openai_key = user_key
197
  st.caption("βœ… Key Active")
198
+ else:
199
  st.session_state.user_openai_key = None
200
  else:
201
+ # Admin defaults to system key, but we ensure state is clean
202
  st.session_state.user_openai_key = None
203
+
204
+ # Unlock GPT-4o option
 
205
  if is_admin or st.session_state.get("user_openai_key"):
206
+ opts.append("GPT-4o (Omni)")
207
  model_captions.append("Fast, smart, sends data to OpenAI")
208
+
209
+ model_choice = st.radio("Select Model:", opts, captions=model_captions, key="model_selector_radio")
 
 
 
 
 
 
210
  st.info(f"Connected to: **{model_choice}**")
211
 
212
  st.divider()
213
+ if st.session_state.authenticator:
214
+ st.session_state.authenticator.logout(location='sidebar')
 
 
 
 
 
 
 
 
 
 
 
 
 
 
215
 
 
216
  update_sidebar_metrics()
217
 
218
+ # --- MAIN APP ---
219
+ st.title("βš“ Navy AI Toolkit")
220
+ tab1, tab2 = st.tabs(["πŸ’¬ Chat Playground", "πŸ“‚ Knowledge & Tools"])
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
221
 
222
+ # === TAB 1: CHAT ===
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
223
  with tab1:
224
+ st.header("Discussion & Analysis")
225
+ if "messages" not in st.session_state: st.session_state.messages = []
 
 
 
 
226
 
227
+ c1, c2 = st.columns([3, 1])
228
+ with c1: st.caption(f"Active Model: **{st.session_state.get('model_selector_radio', 'Granite')}**")
229
+ with c2: use_rag = st.toggle("Enable Knowledge Base", value=False)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
230
 
231
+ for msg in st.session_state.messages:
232
+ with st.chat_message(msg["role"]): st.markdown(msg["content"])
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
233
 
234
+ if prompt := st.chat_input("Input command..."):
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
235
  st.session_state.messages.append({"role": "user", "content": prompt})
236
+ with st.chat_message("user"): st.markdown(prompt)
237
+
238
+ # RAG Search
239
+ context_txt = ""
240
+ sys_p = "You are a helpful assistant."
 
 
241
 
 
242
  if use_rag:
243
+ with st.spinner("Searching DB..."):
244
+ docs = rag_engine.search_knowledge_base(prompt, st.session_state.username)
245
+ if docs:
246
+ sys_p = (
247
+ "You are a Navy Document Analyst. Answer using ONLY the Context below. "
248
+ "If the answer is not in the context, say 'I cannot find that information'."
 
 
 
 
 
 
 
249
  )
250
+ for d in docs: context_txt += f"\n---\n{d.page_content}"
251
 
252
+ final_prompt = f"{prompt}\n\nCONTEXT:\n{context_txt}" if context_txt else prompt
 
 
 
 
 
 
 
 
 
 
 
 
253
 
254
+ # Generation
 
 
 
 
 
 
 
 
 
 
 
255
  with st.chat_message("assistant"):
256
+ with st.spinner("Thinking..."):
257
+ # Memory Window
258
+ hist = [{"role":"system", "content":sys_p}] + st.session_state.messages[-6:-1] + [{"role":"user", "content":final_prompt}]
 
 
 
 
 
 
 
 
 
259
 
260
+ resp, usage = query_model_universal(hist, 2000, model_choice, st.session_state.get("user_openai_key"))
261
+ st.markdown(resp)
 
 
 
 
 
 
 
262
 
263
+ if usage:
264
+ m_name = "GPT-4o" if "GPT-4o" in model_choice else model_choice.split()[0]
265
+ tracker.log_usage(m_name, usage["input"], usage["output"])
266
+ update_sidebar_metrics()
267
+
268
+ st.session_state.messages.append({"role": "assistant", "content": resp})
269
 
270
+ if use_rag and context_txt:
 
 
 
 
 
 
 
 
 
271
  with st.expander("πŸ“š View Context Used"):
272
+ st.text(context_txt)
273
+
274
+ # === TAB 2: KNOWLEDGE & TOOLS ===
275
+ with tab2:
276
+ st.header("Document Processor")
 
 
 
 
 
 
277
 
278
+ c1, c2 = st.columns([1, 1])
279
  with c1:
280
+ uploaded_file = st.file_uploader("Upload File (PDF, PPT, Doc, Text)", type=["pdf", "docx", "pptx", "txt", "md"])
 
 
 
 
 
281
  with c2:
282
+ use_vision = st.toggle("πŸ‘οΈ Enable Vision Mode", help="Use GPT-4o to read diagrams/tables. Requires API Key.")
283
+ if use_vision and "GPT-4o" not in opts:
284
+ st.warning("Vision requires OpenAI Access.")
 
285
 
286
+ if uploaded_file:
287
+ # Save temp
288
+ temp_path = rag_engine.save_uploaded_file(uploaded_file)
 
 
 
 
 
 
 
 
 
289
 
290
+ # ACTION BAR
291
+ col_a, col_b, col_c = st.columns(3)
 
 
 
 
 
292
 
293
+ # 1. ADD TO DB
294
+ with col_a:
295
+ if st.button("πŸ“₯ Add to Knowledge Base", type="primary"):
296
+ with st.spinner("Ingesting..."):
297
+ # Use Admin or User Key for Vision
298
+ key = st.session_state.get("user_openai_key") or OPENAI_KEY
299
+
300
+ ok, msg = rag_engine.process_and_add_document(
301
+ temp_path, st.session_state.username, "paragraph",
302
+ use_vision=use_vision, api_key=key
303
+ )
304
+
305
+ if ok:
306
+ tracker.upload_user_db(st.session_state.username) # Auto-Sync
307
+ st.success(msg)
308
+ else: st.error(msg)
309
+
310
+ # 2. SUMMARIZE
311
+ with col_b:
312
+ if st.button("πŸ“ Summarize Document"):
313
+ with st.spinner("Reading & Summarizing..."):
314
+ key = st.session_state.get("user_openai_key") or OPENAI_KEY
315
+ # Extract raw text first
316
+ class FileObj:
317
+ def __init__(self, p, n): self.path=p; self.name=n
318
+ def read(self):
319
+ with open(self.path, "rb") as f: return f.read()
320
+
321
+ # Extraction
322
+ raw = doc_loader.extract_text_from_file(
323
+ FileObj(temp_path, uploaded_file.name),
324
+ use_vision=use_vision, api_key=key
325
+ )
326
+
327
+ # Call LLM
328
+ prompt = f"Summarize this document into a key executive brief:\n\n{raw[:20000]}" # Truncate for safety
329
+ msgs = [{"role":"user", "content": prompt}]
330
+ summ, usage = query_model_universal(msgs, 1000, model_choice, st.session_state.get("user_openai_key"))
331
+
332
+ st.subheader("Summary Result")
333
+ st.markdown(summ)
334
+ if usage:
335
+ m_name = "GPT-4o" if "GPT-4o" in model_choice else model_choice.split()[0]
336
+ tracker.log_usage(m_name, usage["input"], usage["output"])
337
+ update_sidebar_metrics()
338
+
339
+ # 3. FLATTEN
340
+ with col_c:
341
+ if st.button("πŸ“„ Flatten Context"):
342
+ with st.spinner("Flattening..."):
343
+ key = st.session_state.get("user_openai_key") or OPENAI_KEY
344
+ # Extract
345
+ with open(temp_path, "rb") as f:
346
+ # Dummy object again for the loader
347
+ class Wrapper:
348
+ def __init__(self, data, n): self.data=data; self.name=n
349
+ def read(self): return self.data
350
+ raw = doc_loader.extract_text_from_file(
351
+ Wrapper(f.read(), uploaded_file.name), use_vision=use_vision, api_key=key
352
+ )
353
+
354
+ # Parse
355
+ proc = OutlineProcessor(raw)
356
+ items = proc.parse()
357
+
358
+ # Flatten
359
+ out_txt = []
360
+ bar = st.progress(0)
361
+ for i, item in enumerate(items):
362
+ # Use the Universal Router so it works with Granite too!
363
+ p = f"Context: {item['context']}\nTarget: {item['target']}\nRewrite as one sentence."
364
+ m = [{"role":"user", "content": p}]
365
+ res, _ = query_model_universal(m, 300, model_choice, st.session_state.get("user_openai_key"))
366
+ out_txt.append(res)
367
+ bar.progress((i+1)/len(items))
368
 
369
+ st.text_area("Result", "\n".join(out_txt), height=300)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
370
 
371
+ st.divider()
372
+
373
+ # DB MANAGER
374
+ st.subheader("Database Management")
375
+ docs = rag_engine.list_documents(st.session_state.username)
376
+ if docs:
377
+ for d in docs:
378
+ c1, c2 = st.columns([4,1])
379
+ c1.text(f"πŸ“„ {d['filename']} ({d['chunks']} chunks)")
380
+ if c2.button("πŸ—‘οΈ", key=d['source']):
381
+ rag_engine.delete_document(st.session_state.username, d['source'])
382
+ tracker.upload_user_db(st.session_state.username)
383
+ st.rerun()
384
+ else:
385
+ st.info("Database Empty.")