Rajan Sharma commited on
Commit
a00b1e9
·
verified ·
1 Parent(s): 6b58036

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +109 -107
app.py CHANGED
@@ -2,12 +2,9 @@
2
  #
3
  # Universal AI Data Analyst with:
4
  # - Unchanged analysis & assessment logic
5
- # - Top: Current Assessment / Assessment History (full width, fills screen)
6
- # - Bottom: New Assessment (files, prompt, actions, voice)
7
- # - Sleek "ChatGPT-ish" UI polish (soft dark, rounded, green accents)
8
- # - Fixed Gradio wiring (uses gr.State for history)
9
- # - Triple-quoted progress strings
10
- # - Voice-to-Text via browser Web Speech API
11
  # - Optional HIPAA flags (fallback defaults if not present in settings.py)
12
 
13
  from __future__ import annotations
@@ -33,8 +30,7 @@ from settings import (
33
  COHERE_TIMEOUT_S, # noqa: F401
34
  USE_OPEN_FALLBACKS # noqa: F401
35
  )
36
-
37
- # Optional HIPAA flags; default to safe, non-breaking values if absent.
38
  try:
39
  from settings import PHI_MODE, PERSIST_HISTORY, HISTORY_TTL_DAYS, REDACT_BEFORE_LLM, ALLOW_EXTERNAL_PHI
40
  except Exception:
@@ -66,7 +62,7 @@ def _sanitize_text(s: str) -> str:
66
  return re2.sub(r"[\p{C}--[\n\t]]+", "", s)
67
 
68
 
69
- # Conservative PHI redaction patterns (applied only if PHI_MODE & REDACT_BEFORE_LLM are enabled)
70
  PHI_PATTERNS = [
71
  (re.compile(r"\b\d{3}-\d{2}-\d{4}\b"), "[REDACTED_SSN]"),
72
  (re.compile(r"\b\d{9}\b"), "[REDACTED_MRN]"),
@@ -92,6 +88,7 @@ def safe_log(event_name: str, meta: dict | None = None):
92
  meta.pop("raw", None)
93
  log_event(event_name, None, meta)
94
  except Exception:
 
95
  pass
96
 
97
 
@@ -275,60 +272,61 @@ TERMS_OF_SERVICE_TEXT = load_markdown_text("terms_of_service.md")
275
  # ---------------------- Sleek UI assets (CSS/JS only) ----------------------
276
 
277
  SLEEK_CSS = """
278
- /* Global */
279
  :root, body, #root, .gradio-container { height: 100%; }
280
- .gradio-container { padding: 0 !important; background: #0e1117; color: #e6e9ef; }
281
  .block { padding: 0 !important; }
282
 
283
  /* Header */
284
  .header {
285
- padding: 18px 22px;
286
- background: linear-gradient(180deg, #11161f 0%, #0f141b 100%);
287
- border-bottom: 1px solid #1e2633;
288
- display: flex; align-items: center; justify-content: space-between; gap: 12px;
 
289
  }
290
- .header h1 { margin: 0; font-size: 18px; font-weight: 600; letter-spacing: .2px; color: #eaf2ee; }
291
- .header .badge {
292
- font-size: 12px; background: #1a2a1f; color: #a6e3b1; padding: 6px 10px; border-radius: 999px; border: 1px solid #23402a;
 
 
 
 
 
 
 
 
293
  }
294
-
295
- /* App body: top (tabs) + bottom (input) */
296
- .app { display: flex; flex-direction: column; height: calc(100vh - 60px); }
297
-
298
- /* Top area */
299
- .top-area { flex: 1 1 auto; padding: 12px 12px 0 12px; box-sizing: border-box; }
300
- .top-card {
301
- height: 100%; background: #0f141b; border: 1px solid #1e2633;
302
- border-radius: 14px; overflow: hidden; display: flex; flex-direction: column;
303
- box-shadow: 0 0 0 1px #0a0d12 inset, 0 8px 20px rgba(0,0,0,.35);
304
  }
 
 
305
 
306
- /* Tabs */
307
- .tabs { flex: 1 1 auto; display: flex; flex-direction: column; }
308
- .tabitem { flex: 1 1 auto; display: flex; flex-direction: column; }
309
- #chatbot_container { flex: 1 1 auto; display: flex; }
310
- #chatbot_container .gr-chatbot { flex: 1 1 auto; height: 100%; }
311
-
312
- /* Bottom input area */
313
- .bottom-area { flex: 0 0 auto; padding: 10px 12px 12px 12px; box-sizing: border-box; }
314
- .bottom-card {
315
- background: #0f141b; border: 1px solid #1e2633; border-radius: 14px; padding: 12px;
316
- box-shadow: 0 0 0 1px #0a0d12 inset, 0 8px 20px rgba(0,0,0,.35);
317
- }
318
- .panel-title { font-size: 13px; font-weight: 600; color: #c9d4cf; margin-bottom: 4px; }
319
- .helper { font-size: 12px; color: #96a1a7; margin-bottom: 8px; }
320
 
321
- /* Inputs & actions */
322
- .actions { display: flex; gap: 8px; }
 
 
323
  .actions .gr-button { flex: 1; }
324
- .gr-button.primary { background: #1f8a53 !important; border: 1px solid #2ea36a !important; }
325
- .gr-button.primary:hover { filter: brightness(1.05); }
326
 
327
- .voice-hint { font-size: 12px; color: #9fb0cc; margin-top: 4px; }
328
- .hr { height: 1px; background: #1a2230; margin: 10px 0; }
 
 
 
 
 
 
329
 
330
- /* Markdown / content */
331
- .markdown-body { color: #e6e9ef; }
332
  """
333
 
334
  VOICE_STT_HTML = """
@@ -365,10 +363,10 @@ function rs_toggle_stt(elemId){
365
  """
366
 
367
 
368
- # ---------------------- UI: Top tabs (full width) + Bottom input ----------------------
369
 
370
  with gr.Blocks(theme=gr.themes.Soft(), css=SLEEK_CSS, fill_width=True) as demo:
371
- # In-memory history component (fixes Gradio list/_id issue)
372
  assessment_history = gr.State([])
373
 
374
  # Header
@@ -378,65 +376,61 @@ with gr.Blocks(theme=gr.themes.Soft(), css=SLEEK_CSS, fill_width=True) as demo:
378
  "PHI Mode ON" if PHI_MODE else "PHI Mode OFF"
379
  gr.Markdown(f"<span class='badge'>{pill}</span>")
380
 
381
- # App body: top tabs + bottom input
382
- with gr.Column(elem_classes=["app"]):
383
-
384
- # ===== TOP: Current Assessment / Assessment History =====
385
- with gr.Row(elem_classes=["top-area"]):
386
- with gr.Column(elem_classes=["top-card"]):
387
- with gr.Tabs(elem_classes=["tabs"]):
388
- with gr.TabItem("Current Assessment", id=0, elem_classes=["tabitem"]):
389
- with gr.Column(elem_id="chatbot_container"):
390
- chat_history_output = gr.Chatbot(label="Analysis Output", type="messages")
391
- with gr.TabItem("Assessment History", id=1, elem_classes=["tabitem"]):
392
- gr.Markdown("### Review Past Assessments")
393
- history_dropdown = gr.Dropdown(label="Select an assessment to review", choices=[])
394
- history_display = gr.Markdown(label="Selected Assessment Details")
395
-
396
- # ===== BOTTOM: New Assessment (files + prompt + actions) =====
397
- with gr.Row(elem_classes=["bottom-area"]):
398
- with gr.Column(elem_classes=["bottom-card"]):
399
- gr.Markdown("<div class='panel-title'>New Assessment</div>")
400
- gr.Markdown("<div class='helper'>Upload CSVs for analysis, or enter a prompt. Voice works in modern browsers.</div>")
401
-
402
- files_input = gr.Files(
403
- label="Upload Data Files (.csv)",
404
- file_count="multiple",
405
- type="filepath",
406
- file_types=[".csv"],
 
 
 
 
 
 
 
407
  )
408
- prompt_input = gr.Textbox(
409
- label="Prompt",
410
- placeholder="Paste your scenario or question here...",
411
- lines=6,
412
- elem_id="prompt_box",
413
- autofocus=True,
414
- )
415
-
416
- with gr.Row(elem_classes=["actions"]):
417
- send_btn = gr.Button("▶️ Run Analysis", variant="primary")
418
- clear_btn = gr.Button("🧹 Clear")
419
- voice_btn = gr.Button("��️ Voice")
420
-
421
- gr.Markdown("<div class='voice-hint'>Click Voice to start/stop dictation into the prompt box.</div>")
422
- ping_btn = gr.Button("🔌 Ping Cohere")
423
- ping_out = gr.Markdown()
424
 
 
 
425
  gr.Markdown("<div class='hr'></div>")
426
- if PHI_MODE:
427
- gr.Markdown(
428
- "⚠️ **PHI Mode:** History persistence is disabled by default. Avoid unnecessary identifiers."
429
- )
430
-
431
- with gr.Accordion("Privacy & Terms", open=False):
432
- gr.Markdown(PRIVACY_POLICY_TEXT)
433
- gr.Markdown("<div class='hr'></div>")
434
- gr.Markdown(TERMS_OF_SERVICE_TEXT)
435
-
436
- # Inject voice-to-text helper (browser-side only)
 
 
 
437
  gr.HTML(VOICE_STT_HTML)
438
 
439
- # ================= Event logic (unchanged analysis flow) =================
440
 
441
  def run_analysis_wrapper(prompt, files, chat_history_list, history_state_list):
442
  if not prompt:
@@ -444,11 +438,14 @@ with gr.Blocks(theme=gr.themes.Soft(), css=SLEEK_CSS, fill_width=True) as demo:
444
  yield chat_history_list, history_state_list, gr.update()
445
  return
446
 
 
447
  chat_with_user_msg = _append_msg(chat_history_list, "user", prompt)
448
 
 
449
  def dummy_update(message: str):
450
  pass
451
 
 
452
  thinking_message = _append_msg(
453
  chat_with_user_msg,
454
  "assistant",
@@ -458,17 +455,21 @@ with gr.Blocks(theme=gr.themes.Soft(), css=SLEEK_CSS, fill_width=True) as demo:
458
  )
459
  yield thinking_message, history_state_list, gr.update()
460
 
 
461
  ai_response_text = handle(prompt, files, dummy_update)
462
 
 
463
  final_chat = _append_msg(chat_with_user_msg, "assistant", ai_response_text)
464
  timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
465
 
 
466
  file_names: List[str] = []
467
  if files:
468
  file_names = [
469
  os.path.basename(f.name if hasattr(f, "name") else f) for f in files
470
  ]
471
 
 
472
  new_entry = {
473
  "id": timestamp,
474
  "prompt": prompt,
@@ -477,6 +478,7 @@ with gr.Blocks(theme=gr.themes.Soft(), css=SLEEK_CSS, fill_width=True) as demo:
477
  "chat_history": final_chat,
478
  }
479
 
 
480
  if PERSIST_HISTORY and (not PHI_MODE or (PHI_MODE and HISTORY_TTL_DAYS > 0)):
481
  updated_history: List[Dict[str, Any]] = (history_state_list or []) + [new_entry]
482
  else:
@@ -525,7 +527,7 @@ with gr.Blocks(theme=gr.themes.Soft(), css=SLEEK_CSS, fill_width=True) as demo:
525
  {chat_md}
526
  """
527
 
528
- # Wire events (State component used for history)
529
  send_btn.click(
530
  run_analysis_wrapper,
531
  inputs=[prompt_input, files_input, chat_history_output, assessment_history],
 
2
  #
3
  # Universal AI Data Analyst with:
4
  # - Unchanged analysis & assessment logic
5
+ # - Fixed Gradio event wiring (uses gr.State for history)
6
+ # - Triple-quoted progress strings (no unterminated literals)
7
+ # - Sleek full-width UI and Voice-to-Text (browser Web Speech API)
 
 
 
8
  # - Optional HIPAA flags (fallback defaults if not present in settings.py)
9
 
10
  from __future__ import annotations
 
30
  COHERE_TIMEOUT_S, # noqa: F401
31
  USE_OPEN_FALLBACKS # noqa: F401
32
  )
33
+ # Try to import optional HIPAA flags; fall back to safe defaults if not defined.
 
34
  try:
35
  from settings import PHI_MODE, PERSIST_HISTORY, HISTORY_TTL_DAYS, REDACT_BEFORE_LLM, ALLOW_EXTERNAL_PHI
36
  except Exception:
 
62
  return re2.sub(r"[\p{C}--[\n\t]]+", "", s)
63
 
64
 
65
+ # Conservative PHI redaction patterns (only applied if PHI_MODE & REDACT_BEFORE_LLM are enabled)
66
  PHI_PATTERNS = [
67
  (re.compile(r"\b\d{3}-\d{2}-\d{4}\b"), "[REDACTED_SSN]"),
68
  (re.compile(r"\b\d{9}\b"), "[REDACTED_MRN]"),
 
88
  meta.pop("raw", None)
89
  log_event(event_name, None, meta)
90
  except Exception:
91
+ # Never raise from logging
92
  pass
93
 
94
 
 
272
  # ---------------------- Sleek UI assets (CSS/JS only) ----------------------
273
 
274
  SLEEK_CSS = """
275
+ /* Full-bleed, modern look */
276
  :root, body, #root, .gradio-container { height: 100%; }
277
+ .gradio-container { padding: 0 !important; }
278
  .block { padding: 0 !important; }
279
 
280
  /* Header */
281
  .header {
282
+ padding: 20px 28px;
283
+ background: linear-gradient(135deg, #0e1726, #1d2a44 60%, #243a5e);
284
+ color: #fff;
285
+ display: flex; align-items: center; justify-content: space-between;
286
+ gap: 16px;
287
  }
288
+ .header h1 { margin: 0; font-size: 22px; letter-spacing: 0.3px; font-weight: 600; }
289
+ .header .badge { font-size: 12px; opacity: 0.9; background:#ffffff22; padding:6px 10px; border-radius: 999px; }
290
+
291
+ /* Main layout */
292
+ .main {
293
+ display: grid;
294
+ grid-template-columns: 420px 1fr;
295
+ gap: 16px;
296
+ padding: 16px;
297
+ height: calc(100vh - 72px);
298
+ box-sizing: border-box;
299
  }
300
+ .left, .right {
301
+ background: #0b1020;
302
+ color: #e9edf3;
303
+ border-radius: 16px;
304
+ border: 1px solid #1c2642;
 
 
 
 
 
305
  }
306
+ .left { padding: 16px; display: flex; flex-direction: column; gap: 12px; }
307
+ .right { padding: 0; display: flex; flex-direction: column; }
308
 
309
+ /* Panels */
310
+ .panel-title { font-size: 14px; font-weight: 600; color: #aeb8cc; margin-bottom: 6px; }
311
+ .helper { font-size: 12px; color: #97a3bb; margin-bottom: 8px; }
 
 
 
 
 
 
 
 
 
 
 
312
 
313
+ /* Sticky actions */
314
+ .actions {
315
+ display: flex; gap: 8px; align-items: center; justify-content: stretch;
316
+ }
317
  .actions .gr-button { flex: 1; }
 
 
318
 
319
+ /* Tabs full height */
320
+ .right .tabs { height: 100%; display: flex; flex-direction: column; }
321
+ .right .tabitem { flex: 1; display: flex; flex-direction: column; }
322
+ #chatbot_container { flex: 1; }
323
+ #chatbot_container .gr-chatbot { height: 100%; }
324
+
325
+ /* Tiny separators */
326
+ .hr { height: 1px; background: #16203b; margin: 10px 0; }
327
 
328
+ /* Voice hint */
329
+ .voice-hint { font-size: 12px; color:#9fb0cc; margin-top: 4px; }
330
  """
331
 
332
  VOICE_STT_HTML = """
 
363
  """
364
 
365
 
366
+ # ---------------------- Sleek UI (with fixed State wiring) ----------------------
367
 
368
  with gr.Blocks(theme=gr.themes.Soft(), css=SLEEK_CSS, fill_width=True) as demo:
369
+ # Persistent in-memory history component (fixes list/_id error)
370
  assessment_history = gr.State([])
371
 
372
  # Header
 
376
  "PHI Mode ON" if PHI_MODE else "PHI Mode OFF"
377
  gr.Markdown(f"<span class='badge'>{pill}</span>")
378
 
379
+ # Main layout
380
+ with gr.Row(elem_classes=["main"]):
381
+ # Left panel
382
+ with gr.Column(elem_classes=["left"]):
383
+ gr.Markdown("<div class='panel-title'>New Assessment</div>")
384
+ gr.Markdown("<div class='helper'>Upload CSVs for analysis, or enter a prompt. Voice works in modern browsers.</div>")
385
+ files_input = gr.Files(
386
+ label="Upload Data Files (.csv)",
387
+ file_count="multiple",
388
+ type="filepath",
389
+ file_types=[".csv"],
390
+ )
391
+ prompt_input = gr.Textbox(
392
+ label="Prompt",
393
+ placeholder="Paste your scenario or question here...",
394
+ lines=12,
395
+ elem_id="prompt_box",
396
+ autofocus=True,
397
+ )
398
+
399
+ with gr.Row(elem_classes=["actions"]):
400
+ send_btn = gr.Button("▶️ Run Analysis", variant="primary")
401
+ clear_btn = gr.Button("🧹 Clear")
402
+ voice_btn = gr.Button("🎙️ Voice")
403
+
404
+ gr.Markdown("<div class='voice-hint'>Click Voice to start/stop dictation into the prompt box.</div>")
405
+ ping_btn = gr.Button("🔌 Ping Cohere")
406
+ ping_out = gr.Markdown()
407
+
408
+ gr.Markdown("<div class='hr'></div>")
409
+ if PHI_MODE:
410
+ gr.Markdown(
411
+ "⚠️ **PHI Mode:** History persistence is disabled by default. Avoid unnecessary identifiers."
412
  )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
413
 
414
+ with gr.Accordion("Privacy & Terms", open=False):
415
+ gr.Markdown(PRIVACY_POLICY_TEXT)
416
  gr.Markdown("<div class='hr'></div>")
417
+ gr.Markdown(TERMS_OF_SERVICE_TEXT)
418
+
419
+ # Right panel
420
+ with gr.Column(elem_classes=["right"]):
421
+ with gr.Tabs(elem_classes=["tabs"]):
422
+ with gr.TabItem("Current Assessment", id=0, elem_classes=["tabitem"]):
423
+ with gr.Column(elem_id="chatbot_container"):
424
+ chat_history_output = gr.Chatbot(label="Analysis Output", type="messages")
425
+ with gr.TabItem("Assessment History", id=1, elem_classes=["tabitem"]):
426
+ gr.Markdown("### Review Past Assessments")
427
+ history_dropdown = gr.Dropdown(label="Select an assessment to review", choices=[])
428
+ history_display = gr.Markdown(label="Selected Assessment Details")
429
+
430
+ # Inject voice-to-text helper
431
  gr.HTML(VOICE_STT_HTML)
432
 
433
+ # --------- Event logic (unchanged analysis flow) ----------
434
 
435
  def run_analysis_wrapper(prompt, files, chat_history_list, history_state_list):
436
  if not prompt:
 
438
  yield chat_history_list, history_state_list, gr.update()
439
  return
440
 
441
+ # Append user's message
442
  chat_with_user_msg = _append_msg(chat_history_list, "user", prompt)
443
 
444
+ # Optional progress callback (not streaming in this UI)
445
  def dummy_update(message: str):
446
  pass
447
 
448
+ # Thinking bubble
449
  thinking_message = _append_msg(
450
  chat_with_user_msg,
451
  "assistant",
 
455
  )
456
  yield thinking_message, history_state_list, gr.update()
457
 
458
+ # Run analysis/chat
459
  ai_response_text = handle(prompt, files, dummy_update)
460
 
461
+ # Append final assistant response
462
  final_chat = _append_msg(chat_with_user_msg, "assistant", ai_response_text)
463
  timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
464
 
465
+ # Capture filenames (if any)
466
  file_names: List[str] = []
467
  if files:
468
  file_names = [
469
  os.path.basename(f.name if hasattr(f, "name") else f) for f in files
470
  ]
471
 
472
+ # Build history record
473
  new_entry = {
474
  "id": timestamp,
475
  "prompt": prompt,
 
478
  "chat_history": final_chat,
479
  }
480
 
481
+ # Respect PHI/history flags
482
  if PERSIST_HISTORY and (not PHI_MODE or (PHI_MODE and HISTORY_TTL_DAYS > 0)):
483
  updated_history: List[Dict[str, Any]] = (history_state_list or []) + [new_entry]
484
  else:
 
527
  {chat_md}
528
  """
529
 
530
+ # Wire events (using proper gr.State component for history)
531
  send_btn.click(
532
  run_analysis_wrapper,
533
  inputs=[prompt_input, files_input, chat_history_output, assessment_history],