Files changed (2) hide show
  1. .gitignore +2 -1
  2. app.py +874 -168
.gitignore CHANGED
@@ -162,4 +162,5 @@ cython_debug/
162
  # option (not recommended) you can uncomment the following to ignore the entire idea folder.
163
  .idea/
164
 
165
- .DS_Store
 
 
162
  # option (not recommended) you can uncomment the following to ignore the entire idea folder.
163
  .idea/
164
 
165
+ .DS_Store
166
+ .vscode/
app.py CHANGED
@@ -12,13 +12,14 @@ API_BASE_URL = os.environ.get("BRAIN_API_BASE_URL", "http://127.0.0.1:8000")
12
  CALL_QUALITY_API_URL = f"{API_BASE_URL}/api/v1/call-quality/"
13
  EMOTION_ANALYSIS_API_URL = f"{API_BASE_URL}/api/v1/emotion-analysis/"
14
  KB_IMPROVEMENTS_API_URL = f"{API_BASE_URL}/api/v1/kb-improvements/"
 
15
 
16
  BRAIN_API_TOKEN = os.environ.get("BRAIN_API_TOKEN")
17
 
18
  st.set_page_config(
19
  page_title="Call Quality Analysis & KB Management",
20
  layout="wide",
21
- initial_sidebar_state="expanded"
22
  )
23
 
24
  APP_USERNAME = os.environ.get("USERNAME")
@@ -30,6 +31,7 @@ if "logged_in" not in st.session_state:
30
  if "current_page" not in st.session_state:
31
  st.session_state.current_page = "call_analysis"
32
 
 
33
  def login_form():
34
  with st.form("login"):
35
  st.subheader("πŸ” Login")
@@ -45,12 +47,14 @@ def login_form():
45
  else:
46
  st.error("❌ Invalid username or password.")
47
 
 
48
  if not st.session_state.logged_in:
49
  login_form()
50
  st.stop()
51
 
52
  # --- Enhanced CSS Styling ---
53
- st.markdown("""
 
54
  <style>
55
  /* Base Card Styles */
56
  .metric-card, .recommendation, .finding, .kb-improvement, .prompt-improvement,
@@ -251,8 +255,28 @@ st.markdown("""
251
  background-color: #4e8df5;
252
  color: white;
253
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
254
  </style>
255
- """, unsafe_allow_html=True)
 
 
 
256
 
257
  # --- KB Improvements API Functions ---
258
  def fetch_pending_kb_improvements(limit=50, skip=0):
@@ -266,17 +290,20 @@ def fetch_pending_kb_improvements(limit=50, skip=0):
266
  response = requests.get(
267
  f"{KB_IMPROVEMENTS_API_URL}audits",
268
  headers=headers,
269
- params={"limit": limit, "skip": skip}
270
  )
271
  response.raise_for_status()
272
  return response.json()
273
  except requests.exceptions.HTTPError as e:
274
- st.error(f"❌ KB Improvements API Error: {e.response.status_code} - {e.response.text}")
 
 
275
  return None
276
  except requests.exceptions.RequestException as e:
277
  st.error(f"⚠️ KB Improvements API request failed: {e}")
278
  return None
279
 
 
280
  def fetch_audit_kb_improvements(call_id):
281
  """Fetch specific audit with KB improvements."""
282
  if not BRAIN_API_TOKEN:
@@ -286,8 +313,7 @@ def fetch_audit_kb_improvements(call_id):
286
  headers = {"Authorization": f"Bearer {BRAIN_API_TOKEN}"}
287
  try:
288
  response = requests.get(
289
- f"{KB_IMPROVEMENTS_API_URL}audits/{call_id}",
290
- headers=headers
291
  )
292
  response.raise_for_status()
293
  return response.json()
@@ -295,13 +321,18 @@ def fetch_audit_kb_improvements(call_id):
295
  if e.response.status_code == 404:
296
  st.error(f"❌ Audit not found for call ID: {call_id}")
297
  else:
298
- st.error(f"❌ KB Improvements API Error: {e.response.status_code} - {e.response.text}")
 
 
299
  return None
300
  except requests.exceptions.RequestException as e:
301
  st.error(f"⚠️ KB Improvements API request failed: {e}")
302
  return None
303
 
304
- def submit_kb_review(call_id, improvement_index, action, edited_content=None, review_notes=None):
 
 
 
305
  """Submit KB improvement review."""
306
  if not BRAIN_API_TOKEN:
307
  st.error("🚨 BRAIN_API_TOKEN environment variable is not set.")
@@ -313,24 +344,25 @@ def submit_kb_review(call_id, improvement_index, action, edited_content=None, re
313
  "improvement_index": improvement_index,
314
  "action": action,
315
  "edited_content": edited_content,
316
- "review_notes": review_notes
317
  }
318
 
319
  try:
320
  response = requests.post(
321
- f"{KB_IMPROVEMENTS_API_URL}review",
322
- headers=headers,
323
- json=payload
324
  )
325
  response.raise_for_status()
326
  return response.json()
327
  except requests.exceptions.HTTPError as e:
328
- st.error(f"❌ KB Review API Error: {e.response.status_code} - {e.response.text}")
 
 
329
  return None
330
  except requests.exceptions.RequestException as e:
331
  st.error(f"⚠️ KB Review API request failed: {e}")
332
  return None
333
 
 
334
  def fetch_kb_stats():
335
  """Fetch KB improvement statistics."""
336
  if not BRAIN_API_TOKEN:
@@ -339,10 +371,7 @@ def fetch_kb_stats():
339
 
340
  headers = {"Authorization": f"Bearer {BRAIN_API_TOKEN}"}
341
  try:
342
- response = requests.get(
343
- f"{KB_IMPROVEMENTS_API_URL}stats",
344
- headers=headers
345
- )
346
  response.raise_for_status()
347
  return response.json()
348
  except requests.exceptions.HTTPError as e:
@@ -352,6 +381,7 @@ def fetch_kb_stats():
352
  st.error(f"⚠️ KB Stats API request failed: {e}")
353
  return None
354
 
 
355
  def fetch_pending_count():
356
  """Fetch count of pending KB improvements."""
357
  if not BRAIN_API_TOKEN:
@@ -360,14 +390,156 @@ def fetch_pending_count():
360
  headers = {"Authorization": f"Bearer {BRAIN_API_TOKEN}"}
361
  try:
362
  response = requests.get(
363
- f"{KB_IMPROVEMENTS_API_URL}pending-count",
364
- headers=headers
365
  )
366
  response.raise_for_status()
367
  return response.json().get("pending_count", 0)
368
  except:
369
  return 0
370
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
371
  # --- KB Improvements UI Functions ---
372
  def display_kb_stats():
373
  """Display KB improvement statistics."""
@@ -381,36 +553,49 @@ def display_kb_stats():
381
  col1, col2, col3, col4 = st.columns(4)
382
 
383
  with col1:
384
- st.markdown(f"""
 
385
  <div class="kb-stats-card">
386
- <h3>{stats.get('total_audits_with_improvements', 0)}</h3>
387
  <p>Total Audits with Improvements</p>
388
  </div>
389
- """, unsafe_allow_html=True)
 
 
390
 
391
  with col2:
392
- st.markdown(f"""
 
393
  <div class="kb-stats-card">
394
- <h3>{stats.get('total_chunks_updated', 0)}</h3>
395
  <p>Chunks Updated</p>
396
  </div>
397
- """, unsafe_allow_html=True)
 
 
398
 
399
  with col3:
400
- st.markdown(f"""
 
401
  <div class="kb-stats-card">
402
- <h3>{stats.get('approval_rate', 0):.1%}</h3>
403
  <p>Approval Rate</p>
404
  </div>
405
- """, unsafe_allow_html=True)
 
 
406
 
407
  with col4:
408
- st.markdown(f"""
 
409
  <div class="kb-stats-card">
410
- <h3>{stats.get('avg_time_to_update_hours', 0):.1f}h</h3>
411
  <p>Avg. Time to Update</p>
412
  </div>
413
- """, unsafe_allow_html=True)
 
 
 
414
 
415
  def display_kb_improvements_list():
416
  """Display list of pending KB improvements."""
@@ -428,22 +613,27 @@ def display_kb_improvements_list():
428
 
429
  if isinstance(created_at, str):
430
  try:
431
- created_date = datetime.fromisoformat(created_at.replace('Z', '+00:00'))
432
  formatted_date = created_date.strftime("%Y-%m-%d %H:%M")
433
  except:
434
  formatted_date = created_at
435
  else:
436
  formatted_date = str(created_at)
437
 
438
- with st.expander(f"πŸ“ž Call ID: {call_id} | {len(kb_improvements)} improvements | {formatted_date}"):
 
 
439
  st.markdown(f"**Summary:** {audit.get('summary', 'No summary available')}")
440
- st.markdown(f"**Resolution Status:** {audit.get('resolution_status', 'Unknown')}")
 
 
441
 
442
  if st.button(f"Review Improvements for {call_id}", key=f"review_{call_id}"):
443
  st.session_state.current_page = "kb_review"
444
  st.session_state.selected_call_id = call_id
445
  st.rerun()
446
 
 
447
  def display_kb_review_interface(call_id):
448
  """Display KB improvement review interface for a specific call."""
449
  st.markdown(f"## πŸ” Reviewing KB Improvements for Call: {call_id}")
@@ -462,15 +652,23 @@ def display_kb_review_interface(call_id):
462
  st.info("No KB improvements found for this call.")
463
  return
464
 
465
- st.markdown(f"**Call Summary:** {audit_data.get('summary', 'No summary available')}")
466
- st.markdown(f"**Resolution Status:** {audit_data.get('resolution_status', 'Unknown')}")
 
 
 
 
467
  st.markdown("---")
468
 
469
  for idx, improvement in enumerate(kb_improvements):
470
  chunk_id = improvement.get("chunk_id", "Unknown")
471
  issue = improvement.get("issue", "No issue description")
472
- suggested_improvement = improvement.get("suggested_improvement", "No suggestion provided")
473
- current_content = improvement.get("current_kb_content", "No current content available")
 
 
 
 
474
  rationale = improvement.get("rationale", "No rationale provided")
475
 
476
  # Check if already processed
@@ -484,36 +682,47 @@ def display_kb_review_interface(call_id):
484
  elif processed_action == "reject":
485
  status_badge = '<span class="kb-rejected-badge">❌ Rejected</span>'
486
  elif processed_action == "edit":
487
- status_badge = '<span class="kb-processed-badge">✏️ Edited & Applied</span>'
 
 
488
  else:
489
  status_badge = '<span class="kb-pending-badge">⏳ Pending Review</span>'
490
 
491
- st.markdown(f"""
 
492
  <div class="kb-review-card">
493
  <h4>Improvement #{idx + 1} - Chunk ID: {chunk_id} {status_badge}</h4>
494
  <p><strong>Issue Identified:</strong> {issue}</p>
495
  <p><strong>Rationale:</strong> {rationale}</p>
496
  </div>
497
- """, unsafe_allow_html=True)
 
 
498
 
499
  if not is_processed:
500
  col1, col2 = st.columns(2)
501
 
502
  with col1:
503
  st.markdown("### πŸ“„ Current Content")
504
- st.markdown(f"""
 
505
  <div class="kb-current-content">
506
  {current_content}
507
  </div>
508
- """, unsafe_allow_html=True)
 
 
509
 
510
  with col2:
511
  st.markdown("### πŸ’‘ Suggested Improvement")
512
- st.markdown(f"""
 
513
  <div class="kb-suggested-content">
514
  {suggested_improvement}
515
  </div>
516
- """, unsafe_allow_html=True)
 
 
517
 
518
  # Review interface
519
  st.markdown("### 🎯 Review Actions")
@@ -525,20 +734,28 @@ def display_kb_review_interface(call_id):
525
  with st.spinner("Approving improvement..."):
526
  result = submit_kb_review(call_id, idx, "approve")
527
  if result and result.get("success"):
528
- st.success(f"βœ… Improvement approved! {result.get('message', '')}")
 
 
529
  st.rerun()
530
  else:
531
- st.error(f"❌ Failed to approve: {result.get('message', 'Unknown error') if result else 'No response'}")
 
 
532
 
533
  with col_reject:
534
  if st.button(f"❌ Reject", key=f"reject_{call_id}_{idx}"):
535
  with st.spinner("Rejecting improvement..."):
536
  result = submit_kb_review(call_id, idx, "reject")
537
  if result and result.get("success"):
538
- st.success(f"βœ… Improvement rejected! {result.get('message', '')}")
 
 
539
  st.rerun()
540
  else:
541
- st.error(f"❌ Failed to reject: {result.get('message', 'Unknown error') if result else 'No response'}")
 
 
542
 
543
  with col_edit:
544
  with st.expander("✏️ Edit & Apply"):
@@ -546,47 +763,62 @@ def display_kb_review_interface(call_id):
546
  "Edit the suggested content:",
547
  value=suggested_improvement,
548
  key=f"edit_content_{call_id}_{idx}",
549
- height=200
550
  )
551
 
552
  review_notes = st.text_input(
553
- "Review notes (optional):",
554
- key=f"review_notes_{call_id}_{idx}"
555
  )
556
 
557
- if st.button(f"Apply Edited Content", key=f"apply_edit_{call_id}_{idx}"):
 
 
558
  if edited_content.strip():
559
  with st.spinner("Applying edited content..."):
560
- result = submit_kb_review(call_id, idx, "edit", edited_content, review_notes)
 
 
561
  if result and result.get("success"):
562
- st.success(f"βœ… Edited content applied! {result.get('message', '')}")
 
 
563
  st.rerun()
564
  else:
565
- st.error(f"❌ Failed to apply edit: {result.get('message', 'Unknown error') if result else 'No response'}")
 
 
566
  else:
567
  st.warning("⚠️ Please provide edited content.")
568
  else:
569
  # Show processed status
570
- st.info(f"βœ… This improvement has been {processed_action}ed and is no longer pending review.")
 
 
571
 
572
  st.markdown("---")
573
 
 
574
  # --- Original Call Analysis Functions (keeping all existing functionality) ---
575
  def fetch_emotion_analysis(call_id, num_chunks):
576
  """Fetches emotion analysis results from the API."""
577
  headers = {"Authorization": f"Bearer {BRAIN_API_TOKEN}"}
578
  payload = {"call_id": call_id, "num_chunks": num_chunks}
579
  try:
580
- response = requests.post(EMOTION_ANALYSIS_API_URL, json=payload, headers=headers)
 
 
581
  response.raise_for_status()
582
  return response.json()
583
  except requests.exceptions.HTTPError as e:
584
- st.error(f"❌ Emotion Analysis API Error: {e.response.status_code} - {e.response.text}")
 
 
585
  return None
586
  except requests.exceptions.RequestException as e:
587
  st.error(f"⚠️ Emotion Analysis API request failed: {e}")
588
  return None
589
 
 
590
  def fetch_call_analysis(call_id, custom_prompt, analysis_focus, emotion_results=None):
591
  """Fetches call quality analysis data from the API."""
592
  if not call_id:
@@ -603,33 +835,46 @@ def fetch_call_analysis(call_id, custom_prompt, analysis_focus, emotion_results=
603
  payload = {
604
  "call_id": call_id,
605
  "custom_prompt": custom_prompt if custom_prompt else "",
606
- "analysis_focus": analysis_focus
607
  }
608
  if emotion_results:
609
  payload["emotion_analysis_result"] = emotion_results
610
  st.info("Including emotion analysis results in call quality assessment.")
611
 
612
  try:
613
- response = requests.post(CALL_QUALITY_API_URL, json=payload, headers=headers)
 
 
614
  response.raise_for_status()
615
  return response.json()
616
  except requests.exceptions.HTTPError as e:
617
- st.error(f"❌ Call Quality API Error: {e.response.status_code} - {e.response.text}")
 
 
618
  return None
619
  except requests.exceptions.RequestException as e:
620
  st.error(f"⚠️ Call Quality API request failed: {e}")
621
  return None
622
 
 
623
  def create_issues_chart(issues):
624
  """Creates a bar chart of issues by area and severity."""
625
- if not issues: return None
 
626
  df = pd.DataFrame(issues)
627
- fig = px.bar(df, x='area', y=None, color='severity', title='Issues by Area and Severity',
628
- labels={'area': 'Issue Area', 'count': 'Number of Issues'},
629
- color_discrete_map={'high': '#ff4b4b', 'medium': '#ffa64b', 'low': '#ffee4b'})
630
- fig.update_layout(plot_bgcolor='rgba(0,0,0,0)')
 
 
 
 
 
 
631
  return fig
632
 
 
633
  def display_issues(issues):
634
  """Displays issues with styled cards."""
635
  if not issues:
@@ -637,82 +882,132 @@ def display_issues(issues):
637
  return
638
  for issue in issues:
639
  severity_class = f"{issue.get('severity', 'unknown').lower()}-severity"
640
- st.markdown(f"""
 
641
  <div class="metric-card {severity_class}">
642
- <h4>{issue.get('issue', 'Unknown Issue')}</h4>
643
- <p><strong>Severity:</strong> {issue.get('severity', 'N/A').upper()} | <strong>Area:</strong> {issue.get('area', 'N/A').replace('_', ' ').title()}</p>
644
- <p><strong>Details:</strong> {issue.get('details', 'No details provided.')}</p>
645
  </div>
646
- """, unsafe_allow_html=True)
 
 
 
647
 
648
  def get_resolution_card_class(status):
649
  status = status.lower() if status else ""
650
  class_map = {
651
- "solved": "resolution-solved", "partially solved": "resolution-partially-solved",
652
- "not solve": "resolution-not-solved", "did not solve": "resolution-not-solved",
653
- "escalation": "resolution-escalation", "abandoned": "resolution-abandoned",
654
- "technical": "resolution-technical"
 
 
 
655
  }
656
  for key, value in class_map.items():
657
- if key in status: return value
 
658
  return "resolution-incomplete"
659
 
 
660
  def display_resolution_status(resolution_status, resolution_evidence):
661
  """Displays resolution status with styling."""
662
  card_class = get_resolution_card_class(resolution_status)
663
- st.markdown(f'<div class="resolution-card {card_class}"><h2>{resolution_status}</h2></div>', unsafe_allow_html=True)
 
 
 
664
  if resolution_evidence:
665
- st.markdown(f'''
 
666
  <div class="resolution-evidence">
667
  <h4>Supporting Evidence:</h4>
668
  <p>{resolution_evidence}</p>
669
- </div>''', unsafe_allow_html=True)
 
 
 
670
 
671
  def display_emotion_assessment(emotion_data):
672
  """Displays emotion assessment data."""
673
- if not emotion_data: return
 
674
  st.markdown("### πŸ˜€ Emotional Assessment")
675
  with st.container(border=True):
676
- col1, col2 = st.columns([2,1])
677
  with col1:
678
  st.markdown(f"**Emotional Journey:**")
679
- st.markdown(f'<div class="emotion-journey">{emotion_data.get("emotional_journey", "N/A")}</div>', unsafe_allow_html=True)
 
 
 
680
  with col2:
681
- shift = emotion_data.get('emotional_shift', 'Unknown').title()
682
- shift_color = {'Improved': '#4CAF50', 'Worsened': '#F44336', 'Neutral': '#607D8B', 'Fluctuated': '#FFC107'}.get(shift, '#9E9E9E')
 
 
 
 
 
683
  st.metric("Emotional Shift", shift)
684
- st.markdown(f'<div style="width:100%; height: 5px; background-color:{shift_color}; border-radius: 5px;"></div>', unsafe_allow_html=True)
 
 
 
685
 
686
  st.markdown(f"**Assessment Notes**:")
687
- st.markdown(f'<div class="emotion-journey">{emotion_data.get("assessment_notes", "N/A")}</div>', unsafe_allow_html=True)
 
 
 
688
 
689
- primary_emotions = emotion_data.get('primary_emotions', [])
690
  if primary_emotions:
691
  st.markdown("**Primary Emotions Detected:**")
692
- emotion_html = "".join(f'<span class="emotion-tag">{e.title()}</span>' for e in primary_emotions)
 
 
 
693
  st.markdown(f"<div>{emotion_html}</div>", unsafe_allow_html=True)
694
 
695
- resp = emotion_data.get('ai_responsiveness', 'Unknown').title()
696
- resp_color = {'Excellent': '#4CAF50', 'Good': '#8BC34A', 'Adequate': '#FFC107', 'Poor': '#FF5722', 'Inappropriate': '#F44336'}.get(resp, '#9E9E9E')
697
- st.markdown(f"**AI Responsiveness to Emotion:** <span style='color: {resp_color}; font-weight: bold;'>{resp}</span>", unsafe_allow_html=True)
 
 
 
 
 
 
 
 
 
 
698
 
699
  def display_single_assessment(data):
700
  """Displays a single category assessment card."""
701
- if not data: return
702
- is_correct = data.get('is_correct', True)
 
703
  card_class = "assessment-correct" if is_correct else "assessment-incorrect"
704
  status_icon = "βœ…" if is_correct else "❌"
705
- st.markdown(f"""
 
706
  <div class="assessment-card {card_class}">
707
- <p><strong>Status:</strong> {status_icon} {'Correct' if is_correct else 'Incorrect'}</p>
708
- <p><strong>Original:</strong> {data.get('original', 'N/A')}</p>
709
- {'<p><strong>Suggested:</strong> ' + data.get('suggested', 'N/A') + '</p>' if not is_correct else ''}
710
- Reasoning: {data.get('reasoning', 'No reasoning provided.')}
711
- </div>""", unsafe_allow_html=True)
 
 
 
712
 
713
  def display_categorization_assessment(assessment_data):
714
  """Displays the categorization assessment section."""
715
- if not assessment_data: return
 
716
  st.markdown("---")
717
  st.markdown("## 🏷️ Call Categorization Assessment")
718
  col1, col2 = st.columns(2)
@@ -723,6 +1018,7 @@ def display_categorization_assessment(assessment_data):
723
  st.markdown("### Subcategory")
724
  display_single_assessment(assessment_data.get("subcategory_assessment"))
725
 
 
726
  # --- Sidebar Navigation ---
727
  with st.sidebar:
728
  st.title("🧠 BrAIn Dashboard")
@@ -738,11 +1034,19 @@ with st.sidebar:
738
  st.session_state.current_page = "call_analysis"
739
  st.rerun()
740
 
741
- kb_button_text = f"πŸ”§ KB Improvements ({pending_count})" if pending_count > 0 else "πŸ”§ KB Improvements"
 
 
 
 
742
  if st.button(kb_button_text, use_container_width=True):
743
  st.session_state.current_page = "kb_improvements"
744
  st.rerun()
745
 
 
 
 
 
746
  st.markdown("---")
747
 
748
  # Page-specific sidebar content
@@ -754,14 +1058,14 @@ with st.sidebar:
754
  "full_analysis": "Full Analysis",
755
  "end_reason_analysis": "Call End Reason Analysis",
756
  "kb_improvements": "Knowledge Base Improvements",
757
- "prompt_improvements": "System Prompt Improvements"
758
  }
759
 
760
  analysis_focus = st.selectbox(
761
  "Analysis Focus",
762
  options=list(analysis_focus_options.keys()),
763
  format_func=lambda x: analysis_focus_options[x],
764
- index=0
765
  )
766
 
767
  st.markdown("---")
@@ -769,25 +1073,33 @@ with st.sidebar:
769
  use_emotion_recognition = st.toggle(
770
  "Include audio emotion recognition",
771
  value=False,
772
- help="When enabled, audio will be analyzed for emotional content before call quality analysis."
773
  )
774
 
775
  if use_emotion_recognition:
776
- st.info("Audio emotion analysis results will be incorporated into the main call quality analysis.")
777
- num_emotion_chunks = st.slider("Number of audio chunks", min_value=2, max_value=5, value=3)
778
-
779
- custom_prompt = st.text_area("Custom Analysis Prompt (Optional)",
780
- placeholder="Enter any specific analysis questions...")
 
 
 
 
 
 
781
 
782
- analyze_button = st.button("Analyze Call", type="primary", use_container_width=True)
 
 
783
 
784
  st.markdown("---")
785
  st.subheader("Display Options")
786
  show_json = st.checkbox("Show Raw JSON", value=False)
787
 
788
  if st.button("Clear Results", type="secondary", use_container_width=True):
789
- if 'analysis_result' in st.session_state:
790
- del st.session_state['analysis_result']
791
  st.rerun()
792
 
793
  elif st.session_state.current_page == "kb_improvements":
@@ -808,7 +1120,7 @@ with st.sidebar:
808
  if st.session_state.current_page == "call_analysis":
809
  st.title("πŸ“Š Call Quality Analysis Dashboard")
810
 
811
- if analyze_button or ('analysis_result' in st.session_state):
812
  if analyze_button:
813
  emotion_results = None
814
  # if use_emotion_recognition:
@@ -819,27 +1131,34 @@ if st.session_state.current_page == "call_analysis":
819
  # else:
820
  # st.error("Could not complete emotion analysis. Continuing without it.")
821
 
822
- result = fetch_call_analysis(call_id, custom_prompt, analysis_focus, emotion_results)
 
 
823
  if result:
824
- st.session_state['analysis_result'] = result
825
- st.session_state['current_focus'] = analysis_focus
826
  st.success("βœ… Analysis complete!")
827
 
828
- if 'analysis_result' in st.session_state:
829
- analysis = st.session_state['analysis_result']
830
- current_focus = st.session_state.get('current_focus', 'full_analysis')
831
 
832
  st.metric("Call ID", analysis.get("call_id", call_id))
833
 
834
- if current_focus != 'full_analysis':
835
- st.info(f"πŸ” Analysis focused on: **{analysis_focus_options.get(current_focus, current_focus).upper()}**")
 
 
836
 
837
  # --- Section: Resolution & Emotion ---
838
  st.markdown("## 🎯 Resolution & Emotion")
839
  col1, col2 = st.columns([1, 1])
840
  with col1:
841
  st.markdown("### Resolution Status")
842
- display_resolution_status(analysis.get("resolution_status"), analysis.get("resolution_evidence"))
 
 
 
843
  with col2:
844
  if "emotion_assessment" in analysis:
845
  display_emotion_assessment(analysis.get("emotion_assessment"))
@@ -850,19 +1169,29 @@ if st.session_state.current_page == "call_analysis":
850
  col1, col2 = st.columns(2)
851
  with col1:
852
  st.markdown("### Call Summary")
853
- st.markdown(f'<div class="finding">{analysis.get("summary", "No summary.")}</div>', unsafe_allow_html=True)
 
 
 
854
  with col2:
855
  st.markdown("### Key Findings")
856
  findings = analysis.get("key_findings", [])
857
  if findings:
858
  for finding in findings:
859
- st.markdown(f'<div class="finding">βœ… {finding}</div>', unsafe_allow_html=True)
 
 
 
860
  else:
861
  st.info("No key findings identified.")
862
 
863
  # --- Section: Categorization Assessment ---
864
- if "categorization_assessment" in analysis and analysis.get("categorization_assessment"):
865
- display_categorization_assessment(analysis.get("categorization_assessment"))
 
 
 
 
866
 
867
  # --- Section: Focused Analysis Displays ---
868
  if current_focus == "full_analysis":
@@ -873,7 +1202,8 @@ if st.session_state.current_page == "call_analysis":
873
  chart_col, list_col = st.columns([1, 2])
874
  with chart_col:
875
  issues_chart = create_issues_chart(issues)
876
- if issues_chart: st.plotly_chart(issues_chart, use_container_width=True)
 
877
  with list_col:
878
  display_issues(issues)
879
  else:
@@ -881,19 +1211,29 @@ if st.session_state.current_page == "call_analysis":
881
 
882
  if current_focus in ["full_analysis", "end_reason_analysis"]:
883
  st.markdown("---")
884
- st.markdown(f"<div class='{'focus-section' if current_focus == 'end_reason_analysis' else ''}'>", unsafe_allow_html=True)
 
 
 
885
  st.markdown("## πŸ›‘ Call End Reason Analysis")
886
  end_analysis = analysis.get("end_reason_analysis", {})
887
  if end_analysis:
888
- st.metric("Call End Reason", end_analysis.get('end_reason', 'N/A').replace('-', ' ').title())
889
- st.markdown(f"**Assessment:** {end_analysis.get('assessment', 'N/A')}")
 
 
 
 
 
890
  col1, col2 = st.columns(2)
891
  with col1:
892
  st.markdown("#### Contributing Factors")
893
- for factor in end_analysis.get("contributing_factors", []): st.markdown(f"- {factor}")
 
894
  with col2:
895
  st.markdown("#### Improvement Opportunities")
896
- for opp in end_analysis.get("improvement_opportunities", []): st.markdown(f"πŸ’‘ {opp}")
 
897
  else:
898
  st.info("No end reason analysis available.")
899
  st.markdown("</div>", unsafe_allow_html=True)
@@ -904,7 +1244,13 @@ if st.session_state.current_page == "call_analysis":
904
 
905
  # Define tabs based on analysis focus
906
  if current_focus == "full_analysis":
907
- tab_names = ["General", "Conversation Flow", "Escalation", "Prompt", "Knowledge Base"]
 
 
 
 
 
 
908
  elif current_focus == "kb_improvements":
909
  tab_names = ["Knowledge Base", "General"]
910
  elif current_focus == "prompt_improvements":
@@ -920,51 +1266,76 @@ if st.session_state.current_page == "call_analysis":
920
  with tab_map["General"]:
921
  recs = analysis.get("general_recommendations", [])
922
  if recs:
923
- for rec in recs: st.markdown(f'<div class="recommendation">πŸ”Ή {rec}</div>', unsafe_allow_html=True)
924
- else: st.info("No general recommendations.")
 
 
 
 
 
925
 
926
  if "Conversation Flow" in tab_map:
927
  with tab_map["Conversation Flow"]:
928
  recs = analysis.get("conversation_recommendations", [])
929
  if recs:
930
- for rec in recs: st.markdown(f'<div class="conversation-recommendation">πŸ’¬ {rec}</div>', unsafe_allow_html=True)
931
- else: st.info("No conversation flow recommendations.")
 
 
 
 
 
932
 
933
  if "Escalation" in tab_map:
934
  with tab_map["Escalation"]:
935
  recs = analysis.get("escalation_recommendations", [])
936
  if recs:
937
- for rec in recs: st.markdown(f'<div class="escalation-recommendation">πŸ“ˆ {rec}</div>', unsafe_allow_html=True)
938
- else: st.info("No escalation recommendations.")
 
 
 
 
 
939
 
940
  if "Prompt" in tab_map:
941
  with tab_map["Prompt"]:
942
  improvements = analysis.get("prompt_improvements", [])
943
  if improvements:
944
  for imp in improvements:
945
- st.markdown(f"""
 
946
  <div class="prompt-improvement">
947
- <h4>Issue: {imp.get('issue', 'N/A')}</h4>
948
- <p><strong>Current Section:</strong> {imp.get('current_section', 'N/A')}</p>
949
- <p><strong>Suggested Change:</strong> {imp.get('suggested_change', 'N/A')}</p>
950
- <p><strong>Expected Outcome:</strong> {imp.get('expected_outcome', 'N/A')}</p>
951
- </div>""", unsafe_allow_html=True)
952
- else: st.info("No prompt improvements found.")
 
 
 
953
 
954
  if "Knowledge Base" in tab_map:
955
  with tab_map["Knowledge Base"]:
956
  improvements = analysis.get("kb_improvements", [])
957
  if improvements:
958
  for imp in improvements:
959
- with st.expander(f"**Query:** '{imp.get('query', 'Unknown')}'"):
960
- st.markdown(f"""
 
 
 
961
  <div class="kb-improvement">
962
- <p><strong>Issue:</strong> {imp.get('issue', 'N/A')}</p>
963
- <p><strong>Suggestion:</strong> {imp.get('suggested_improvement', 'N/A')}</p>
964
- <p><strong>Rationale:</strong> {imp.get('rationale', 'N/A')}</p>
965
- <p><strong>Current Content:</strong> {imp.get('current_kb_content', 'N/A')}</p>
966
- </div>""", unsafe_allow_html=True)
967
- else: st.info("No KB improvements found.")
 
 
 
968
 
969
  if show_json:
970
  st.markdown("---")
@@ -981,28 +1352,363 @@ if st.session_state.current_page == "call_analysis":
981
  4. Click **Analyze Call** to generate the report.
982
  """)
983
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
984
  elif st.session_state.current_page == "kb_improvements":
985
  st.title("πŸ”§ Knowledge Base Improvements")
986
 
987
- # Initialize kb_view if not exists
988
- if "kb_view" not in st.session_state:
989
- st.session_state.kb_view = "stats"
990
 
991
- # Display based on current view
992
- if st.session_state.kb_view == "stats":
993
  display_kb_stats()
994
  st.markdown("---")
995
  display_kb_improvements_list()
996
- elif st.session_state.kb_view == "list":
997
  display_kb_improvements_list()
998
 
999
  elif st.session_state.current_page == "kb_review":
1000
- if "selected_call_id" in st.session_state:
1001
- display_kb_review_interface(st.session_state.selected_call_id)
 
1002
  else:
1003
- st.error("No call ID selected for review.")
1004
- st.session_state.current_page = "kb_improvements"
1005
- st.rerun()
 
1006
 
1007
  st.markdown("---")
1008
- st.caption(f"🧠 BrAIn Dashboard | Last updated: {datetime.now().strftime('%B %d, %Y')}")
 
12
  CALL_QUALITY_API_URL = f"{API_BASE_URL}/api/v1/call-quality/"
13
  EMOTION_ANALYSIS_API_URL = f"{API_BASE_URL}/api/v1/emotion-analysis/"
14
  KB_IMPROVEMENTS_API_URL = f"{API_BASE_URL}/api/v1/kb-improvements/"
15
+ EVALUATION_API_URL = f"{API_BASE_URL}/api/v1/evaluation/"
16
 
17
  BRAIN_API_TOKEN = os.environ.get("BRAIN_API_TOKEN")
18
 
19
  st.set_page_config(
20
  page_title="Call Quality Analysis & KB Management",
21
  layout="wide",
22
+ initial_sidebar_state="expanded",
23
  )
24
 
25
  APP_USERNAME = os.environ.get("USERNAME")
 
31
  if "current_page" not in st.session_state:
32
  st.session_state.current_page = "call_analysis"
33
 
34
+
35
  def login_form():
36
  with st.form("login"):
37
  st.subheader("πŸ” Login")
 
47
  else:
48
  st.error("❌ Invalid username or password.")
49
 
50
+
51
  if not st.session_state.logged_in:
52
  login_form()
53
  st.stop()
54
 
55
  # --- Enhanced CSS Styling ---
56
+ st.markdown(
57
+ """
58
  <style>
59
  /* Base Card Styles */
60
  .metric-card, .recommendation, .finding, .kb-improvement, .prompt-improvement,
 
255
  background-color: #4e8df5;
256
  color: white;
257
  }
258
+
259
+ /* Assistant Leaderboard Styles */
260
+ .leaderboard-card {
261
+ background-color: #f8f9fa;
262
+ border-radius: 10px;
263
+ padding: 15px;
264
+ margin-bottom: 10px;
265
+ border-left: 5px solid #4e8df5;
266
+ }
267
+ .leaderboard-rank {
268
+ font-size: 24px;
269
+ font-weight: bold;
270
+ color: #4e8df5;
271
+ }
272
+ .hallucination-high { border-left-color: #F44336; }
273
+ .hallucination-medium { border-left-color: #FFC107; }
274
+ .hallucination-low { border-left-color: #4CAF50; }
275
  </style>
276
+ """,
277
+ unsafe_allow_html=True,
278
+ )
279
+
280
 
281
  # --- KB Improvements API Functions ---
282
  def fetch_pending_kb_improvements(limit=50, skip=0):
 
290
  response = requests.get(
291
  f"{KB_IMPROVEMENTS_API_URL}audits",
292
  headers=headers,
293
+ params={"limit": limit, "skip": skip},
294
  )
295
  response.raise_for_status()
296
  return response.json()
297
  except requests.exceptions.HTTPError as e:
298
+ st.error(
299
+ f"❌ KB Improvements API Error: {e.response.status_code} - {e.response.text}"
300
+ )
301
  return None
302
  except requests.exceptions.RequestException as e:
303
  st.error(f"⚠️ KB Improvements API request failed: {e}")
304
  return None
305
 
306
+
307
  def fetch_audit_kb_improvements(call_id):
308
  """Fetch specific audit with KB improvements."""
309
  if not BRAIN_API_TOKEN:
 
313
  headers = {"Authorization": f"Bearer {BRAIN_API_TOKEN}"}
314
  try:
315
  response = requests.get(
316
+ f"{KB_IMPROVEMENTS_API_URL}audits/{call_id}", headers=headers
 
317
  )
318
  response.raise_for_status()
319
  return response.json()
 
321
  if e.response.status_code == 404:
322
  st.error(f"❌ Audit not found for call ID: {call_id}")
323
  else:
324
+ st.error(
325
+ f"❌ KB Improvements API Error: {e.response.status_code} - {e.response.text}"
326
+ )
327
  return None
328
  except requests.exceptions.RequestException as e:
329
  st.error(f"⚠️ KB Improvements API request failed: {e}")
330
  return None
331
 
332
+
333
+ def submit_kb_review(
334
+ call_id, improvement_index, action, edited_content=None, review_notes=None
335
+ ):
336
  """Submit KB improvement review."""
337
  if not BRAIN_API_TOKEN:
338
  st.error("🚨 BRAIN_API_TOKEN environment variable is not set.")
 
344
  "improvement_index": improvement_index,
345
  "action": action,
346
  "edited_content": edited_content,
347
+ "review_notes": review_notes,
348
  }
349
 
350
  try:
351
  response = requests.post(
352
+ f"{KB_IMPROVEMENTS_API_URL}review", headers=headers, json=payload
 
 
353
  )
354
  response.raise_for_status()
355
  return response.json()
356
  except requests.exceptions.HTTPError as e:
357
+ st.error(
358
+ f"❌ KB Review API Error: {e.response.status_code} - {e.response.text}"
359
+ )
360
  return None
361
  except requests.exceptions.RequestException as e:
362
  st.error(f"⚠️ KB Review API request failed: {e}")
363
  return None
364
 
365
+
366
  def fetch_kb_stats():
367
  """Fetch KB improvement statistics."""
368
  if not BRAIN_API_TOKEN:
 
371
 
372
  headers = {"Authorization": f"Bearer {BRAIN_API_TOKEN}"}
373
  try:
374
+ response = requests.get(f"{KB_IMPROVEMENTS_API_URL}stats", headers=headers)
 
 
 
375
  response.raise_for_status()
376
  return response.json()
377
  except requests.exceptions.HTTPError as e:
 
381
  st.error(f"⚠️ KB Stats API request failed: {e}")
382
  return None
383
 
384
+
385
  def fetch_pending_count():
386
  """Fetch count of pending KB improvements."""
387
  if not BRAIN_API_TOKEN:
 
390
  headers = {"Authorization": f"Bearer {BRAIN_API_TOKEN}"}
391
  try:
392
  response = requests.get(
393
+ f"{KB_IMPROVEMENTS_API_URL}pending-count", headers=headers
 
394
  )
395
  response.raise_for_status()
396
  return response.json().get("pending_count", 0)
397
  except:
398
  return 0
399
 
400
+
401
+ # --- Hallucination Evaluation API Functions ---
402
+ def fetch_eval_aggregates(assistant_id=None, topic=None, limit=50):
403
+ """Fetch hallucination aggregates."""
404
+ if not BRAIN_API_TOKEN:
405
+ st.error("🚨 BRAIN_API_TOKEN environment variable is not set.")
406
+ return None
407
+
408
+ headers = {"Authorization": f"Bearer {BRAIN_API_TOKEN}"}
409
+ params = {"limit": limit}
410
+ if assistant_id:
411
+ params["assistant_id"] = assistant_id
412
+ if topic:
413
+ params["topic"] = topic
414
+
415
+ try:
416
+ response = requests.get(
417
+ f"{EVALUATION_API_URL}aggregates", headers=headers, params=params
418
+ )
419
+ response.raise_for_status()
420
+ return response.json()
421
+ except requests.exceptions.HTTPError as e:
422
+ st.error(
423
+ f"❌ Hallucination Aggregates API Error: {e.response.status_code} - {e.response.text}"
424
+ )
425
+ return None
426
+ except requests.exceptions.RequestException as e:
427
+ st.error(f"⚠️ Hallucination Aggregates API request failed: {e}")
428
+ return None
429
+
430
+
431
+ def fetch_top_hallucinators(assistant_id=None, topic=None, limit=10):
432
+ """Fetch top hallucinators."""
433
+ if not BRAIN_API_TOKEN:
434
+ st.error("🚨 BRAIN_API_TOKEN environment variable is not set.")
435
+ return None
436
+
437
+ headers = {"Authorization": f"Bearer {BRAIN_API_TOKEN}"}
438
+ params = {"limit": limit}
439
+ if assistant_id:
440
+ params["assistant_id"] = assistant_id
441
+ if topic:
442
+ params["topic"] = topic
443
+
444
+ try:
445
+ response = requests.get(
446
+ f"{EVALUATION_API_URL}top-hallucinators", headers=headers, params=params
447
+ )
448
+ response.raise_for_status()
449
+ return response.json()
450
+ except requests.exceptions.HTTPError as e:
451
+ st.error(
452
+ f"❌ Top Hallucinators API Error: {e.response.status_code} - {e.response.text}"
453
+ )
454
+ return None
455
+ except requests.exceptions.RequestException as e:
456
+ st.error(f"⚠️ Top Hallucinators API request failed: {e}")
457
+ return None
458
+
459
+
460
+ def fetch_assistant_leaderboard(topic=None, limit=20):
461
+ """Fetch assistant leaderboard with hallucination metrics."""
462
+ if not BRAIN_API_TOKEN:
463
+ st.error("🚨 BRAIN_API_TOKEN environment variable is not set.")
464
+ return None
465
+
466
+ headers = {"Authorization": f"Bearer {BRAIN_API_TOKEN}"}
467
+ params = {"limit": limit}
468
+ if topic:
469
+ params["topic"] = topic
470
+
471
+ try:
472
+ response = requests.get(
473
+ f"{EVALUATION_API_URL}assistant-leaderboard", headers=headers, params=params
474
+ )
475
+ response.raise_for_status()
476
+ return response.json()
477
+ except requests.exceptions.HTTPError as e:
478
+ st.error(
479
+ f"❌ Assistant Leaderboard API Error: {e.response.status_code} - {e.response.text}"
480
+ )
481
+ return None
482
+ except requests.exceptions.RequestException as e:
483
+ st.error(f"⚠️ Assistant Leaderboard API request failed: {e}")
484
+ return None
485
+
486
+
487
+ def fetch_sample_unsupported(assistant_id=None, topic=None, limit=20):
488
+ """Fetch sample of unsupported claims."""
489
+ if not BRAIN_API_TOKEN:
490
+ st.error("🚨 BRAIN_API_TOKEN environment variable is not set.")
491
+ return None
492
+
493
+ headers = {"Authorization": f"Bearer {BRAIN_API_TOKEN}"}
494
+ params = {"limit": limit}
495
+ if assistant_id:
496
+ params["assistant_id"] = assistant_id
497
+ if topic:
498
+ params["topic"] = topic
499
+
500
+ try:
501
+ response = requests.get(
502
+ f"{EVALUATION_API_URL}sample-unsupported", headers=headers, params=params
503
+ )
504
+ response.raise_for_status()
505
+ return response.json()
506
+ except requests.exceptions.HTTPError as e:
507
+ st.error(
508
+ f"❌ Sample Unsupported API Error: {e.response.status_code} - {e.response.text}"
509
+ )
510
+ return None
511
+ except requests.exceptions.RequestException as e:
512
+ st.error(f"⚠️ Sample Unsupported API request failed: {e}")
513
+ return None
514
+
515
+
516
+ def submit_annotation(run_id, claim_idx, label, assistant_id=None):
517
+ """Submit annotation for a claim."""
518
+ if not BRAIN_API_TOKEN:
519
+ st.error("🚨 BRAIN_API_TOKEN environment variable is not set.")
520
+ return None
521
+
522
+ headers = {"Authorization": f"Bearer {BRAIN_API_TOKEN}"}
523
+ payload = {"run_id": run_id, "claim_idx": claim_idx, "label": label}
524
+ if assistant_id:
525
+ payload["assistant_id"] = assistant_id
526
+
527
+ try:
528
+ response = requests.post(
529
+ f"{EVALUATION_API_URL}annotate", headers=headers, json=payload
530
+ )
531
+ response.raise_for_status()
532
+ return True
533
+ except requests.exceptions.HTTPError as e:
534
+ st.error(
535
+ f"❌ Annotation API Error: {e.response.status_code} - {e.response.text}"
536
+ )
537
+ return None
538
+ except requests.exceptions.RequestException as e:
539
+ st.error(f"⚠️ Annotation API request failed: {e}")
540
+ return None
541
+
542
+
543
  # --- KB Improvements UI Functions ---
544
  def display_kb_stats():
545
  """Display KB improvement statistics."""
 
553
  col1, col2, col3, col4 = st.columns(4)
554
 
555
  with col1:
556
+ st.markdown(
557
+ f"""
558
  <div class="kb-stats-card">
559
+ <h3>{stats.get("total_audits_with_improvements", 0)}</h3>
560
  <p>Total Audits with Improvements</p>
561
  </div>
562
+ """,
563
+ unsafe_allow_html=True,
564
+ )
565
 
566
  with col2:
567
+ st.markdown(
568
+ f"""
569
  <div class="kb-stats-card">
570
+ <h3>{stats.get("total_chunks_updated", 0)}</h3>
571
  <p>Chunks Updated</p>
572
  </div>
573
+ """,
574
+ unsafe_allow_html=True,
575
+ )
576
 
577
  with col3:
578
+ st.markdown(
579
+ f"""
580
  <div class="kb-stats-card">
581
+ <h3>{stats.get("approval_rate", 0):.1%}</h3>
582
  <p>Approval Rate</p>
583
  </div>
584
+ """,
585
+ unsafe_allow_html=True,
586
+ )
587
 
588
  with col4:
589
+ st.markdown(
590
+ f"""
591
  <div class="kb-stats-card">
592
+ <h3>{stats.get("avg_time_to_update_hours", 0):.1f}h</h3>
593
  <p>Avg. Time to Update</p>
594
  </div>
595
+ """,
596
+ unsafe_allow_html=True,
597
+ )
598
+
599
 
600
  def display_kb_improvements_list():
601
  """Display list of pending KB improvements."""
 
613
 
614
  if isinstance(created_at, str):
615
  try:
616
+ created_date = datetime.fromisoformat(created_at.replace("Z", "+00:00"))
617
  formatted_date = created_date.strftime("%Y-%m-%d %H:%M")
618
  except:
619
  formatted_date = created_at
620
  else:
621
  formatted_date = str(created_at)
622
 
623
+ with st.expander(
624
+ f"πŸ“ž Call ID: {call_id} | {len(kb_improvements)} improvements | {formatted_date}"
625
+ ):
626
  st.markdown(f"**Summary:** {audit.get('summary', 'No summary available')}")
627
+ st.markdown(
628
+ f"**Resolution Status:** {audit.get('resolution_status', 'Unknown')}"
629
+ )
630
 
631
  if st.button(f"Review Improvements for {call_id}", key=f"review_{call_id}"):
632
  st.session_state.current_page = "kb_review"
633
  st.session_state.selected_call_id = call_id
634
  st.rerun()
635
 
636
+
637
  def display_kb_review_interface(call_id):
638
  """Display KB improvement review interface for a specific call."""
639
  st.markdown(f"## πŸ” Reviewing KB Improvements for Call: {call_id}")
 
652
  st.info("No KB improvements found for this call.")
653
  return
654
 
655
+ st.markdown(
656
+ f"**Call Summary:** {audit_data.get('summary', 'No summary available')}"
657
+ )
658
+ st.markdown(
659
+ f"**Resolution Status:** {audit_data.get('resolution_status', 'Unknown')}"
660
+ )
661
  st.markdown("---")
662
 
663
  for idx, improvement in enumerate(kb_improvements):
664
  chunk_id = improvement.get("chunk_id", "Unknown")
665
  issue = improvement.get("issue", "No issue description")
666
+ suggested_improvement = improvement.get(
667
+ "suggested_improvement", "No suggestion provided"
668
+ )
669
+ current_content = improvement.get(
670
+ "current_kb_content", "No current content available"
671
+ )
672
  rationale = improvement.get("rationale", "No rationale provided")
673
 
674
  # Check if already processed
 
682
  elif processed_action == "reject":
683
  status_badge = '<span class="kb-rejected-badge">❌ Rejected</span>'
684
  elif processed_action == "edit":
685
+ status_badge = (
686
+ '<span class="kb-processed-badge">✏️ Edited & Applied</span>'
687
+ )
688
  else:
689
  status_badge = '<span class="kb-pending-badge">⏳ Pending Review</span>'
690
 
691
+ st.markdown(
692
+ f"""
693
  <div class="kb-review-card">
694
  <h4>Improvement #{idx + 1} - Chunk ID: {chunk_id} {status_badge}</h4>
695
  <p><strong>Issue Identified:</strong> {issue}</p>
696
  <p><strong>Rationale:</strong> {rationale}</p>
697
  </div>
698
+ """,
699
+ unsafe_allow_html=True,
700
+ )
701
 
702
  if not is_processed:
703
  col1, col2 = st.columns(2)
704
 
705
  with col1:
706
  st.markdown("### πŸ“„ Current Content")
707
+ st.markdown(
708
+ f"""
709
  <div class="kb-current-content">
710
  {current_content}
711
  </div>
712
+ """,
713
+ unsafe_allow_html=True,
714
+ )
715
 
716
  with col2:
717
  st.markdown("### πŸ’‘ Suggested Improvement")
718
+ st.markdown(
719
+ f"""
720
  <div class="kb-suggested-content">
721
  {suggested_improvement}
722
  </div>
723
+ """,
724
+ unsafe_allow_html=True,
725
+ )
726
 
727
  # Review interface
728
  st.markdown("### 🎯 Review Actions")
 
734
  with st.spinner("Approving improvement..."):
735
  result = submit_kb_review(call_id, idx, "approve")
736
  if result and result.get("success"):
737
+ st.success(
738
+ f"βœ… Improvement approved! {result.get('message', '')}"
739
+ )
740
  st.rerun()
741
  else:
742
+ st.error(
743
+ f"❌ Failed to approve: {result.get('message', 'Unknown error') if result else 'No response'}"
744
+ )
745
 
746
  with col_reject:
747
  if st.button(f"❌ Reject", key=f"reject_{call_id}_{idx}"):
748
  with st.spinner("Rejecting improvement..."):
749
  result = submit_kb_review(call_id, idx, "reject")
750
  if result and result.get("success"):
751
+ st.success(
752
+ f"βœ… Improvement rejected! {result.get('message', '')}"
753
+ )
754
  st.rerun()
755
  else:
756
+ st.error(
757
+ f"❌ Failed to reject: {result.get('message', 'Unknown error') if result else 'No response'}"
758
+ )
759
 
760
  with col_edit:
761
  with st.expander("✏️ Edit & Apply"):
 
763
  "Edit the suggested content:",
764
  value=suggested_improvement,
765
  key=f"edit_content_{call_id}_{idx}",
766
+ height=200,
767
  )
768
 
769
  review_notes = st.text_input(
770
+ "Review notes (optional):", key=f"review_notes_{call_id}_{idx}"
 
771
  )
772
 
773
+ if st.button(
774
+ f"Apply Edited Content", key=f"apply_edit_{call_id}_{idx}"
775
+ ):
776
  if edited_content.strip():
777
  with st.spinner("Applying edited content..."):
778
+ result = submit_kb_review(
779
+ call_id, idx, "edit", edited_content, review_notes
780
+ )
781
  if result and result.get("success"):
782
+ st.success(
783
+ f"βœ… Edited content applied! {result.get('message', '')}"
784
+ )
785
  st.rerun()
786
  else:
787
+ st.error(
788
+ f"❌ Failed to apply edit: {result.get('message', 'Unknown error') if result else 'No response'}"
789
+ )
790
  else:
791
  st.warning("⚠️ Please provide edited content.")
792
  else:
793
  # Show processed status
794
+ st.info(
795
+ f"βœ… This improvement has been {processed_action}ed and is no longer pending review."
796
+ )
797
 
798
  st.markdown("---")
799
 
800
+
801
  # --- Original Call Analysis Functions (keeping all existing functionality) ---
802
  def fetch_emotion_analysis(call_id, num_chunks):
803
  """Fetches emotion analysis results from the API."""
804
  headers = {"Authorization": f"Bearer {BRAIN_API_TOKEN}"}
805
  payload = {"call_id": call_id, "num_chunks": num_chunks}
806
  try:
807
+ response = requests.post(
808
+ EMOTION_ANALYSIS_API_URL, json=payload, headers=headers
809
+ )
810
  response.raise_for_status()
811
  return response.json()
812
  except requests.exceptions.HTTPError as e:
813
+ st.error(
814
+ f"❌ Emotion Analysis API Error: {e.response.status_code} - {e.response.text}"
815
+ )
816
  return None
817
  except requests.exceptions.RequestException as e:
818
  st.error(f"⚠️ Emotion Analysis API request failed: {e}")
819
  return None
820
 
821
+
822
  def fetch_call_analysis(call_id, custom_prompt, analysis_focus, emotion_results=None):
823
  """Fetches call quality analysis data from the API."""
824
  if not call_id:
 
835
  payload = {
836
  "call_id": call_id,
837
  "custom_prompt": custom_prompt if custom_prompt else "",
838
+ "analysis_focus": analysis_focus,
839
  }
840
  if emotion_results:
841
  payload["emotion_analysis_result"] = emotion_results
842
  st.info("Including emotion analysis results in call quality assessment.")
843
 
844
  try:
845
+ response = requests.post(
846
+ CALL_QUALITY_API_URL, json=payload, headers=headers
847
+ )
848
  response.raise_for_status()
849
  return response.json()
850
  except requests.exceptions.HTTPError as e:
851
+ st.error(
852
+ f"❌ Call Quality API Error: {e.response.status_code} - {e.response.text}"
853
+ )
854
  return None
855
  except requests.exceptions.RequestException as e:
856
  st.error(f"⚠️ Call Quality API request failed: {e}")
857
  return None
858
 
859
+
860
  def create_issues_chart(issues):
861
  """Creates a bar chart of issues by area and severity."""
862
+ if not issues:
863
+ return None
864
  df = pd.DataFrame(issues)
865
+ fig = px.bar(
866
+ df,
867
+ x="area",
868
+ y=None,
869
+ color="severity",
870
+ title="Issues by Area and Severity",
871
+ labels={"area": "Issue Area", "count": "Number of Issues"},
872
+ color_discrete_map={"high": "#ff4b4b", "medium": "#ffa64b", "low": "#ffee4b"},
873
+ )
874
+ fig.update_layout(plot_bgcolor="rgba(0,0,0,0)")
875
  return fig
876
 
877
+
878
  def display_issues(issues):
879
  """Displays issues with styled cards."""
880
  if not issues:
 
882
  return
883
  for issue in issues:
884
  severity_class = f"{issue.get('severity', 'unknown').lower()}-severity"
885
+ st.markdown(
886
+ f"""
887
  <div class="metric-card {severity_class}">
888
+ <h4>{issue.get("issue", "Unknown Issue")}</h4>
889
+ <p><strong>Severity:</strong> {issue.get("severity", "N/A").upper()} | <strong>Area:</strong> {issue.get("area", "N/A").replace("_", " ").title()}</p>
890
+ <p><strong>Details:</strong> {issue.get("details", "No details provided.")}</p>
891
  </div>
892
+ """,
893
+ unsafe_allow_html=True,
894
+ )
895
+
896
 
897
  def get_resolution_card_class(status):
898
  status = status.lower() if status else ""
899
  class_map = {
900
+ "solved": "resolution-solved",
901
+ "partially solved": "resolution-partially-solved",
902
+ "not solve": "resolution-not-solved",
903
+ "did not solve": "resolution-not-solved",
904
+ "escalation": "resolution-escalation",
905
+ "abandoned": "resolution-abandoned",
906
+ "technical": "resolution-technical",
907
  }
908
  for key, value in class_map.items():
909
+ if key in status:
910
+ return value
911
  return "resolution-incomplete"
912
 
913
+
914
  def display_resolution_status(resolution_status, resolution_evidence):
915
  """Displays resolution status with styling."""
916
  card_class = get_resolution_card_class(resolution_status)
917
+ st.markdown(
918
+ f'<div class="resolution-card {card_class}"><h2>{resolution_status}</h2></div>',
919
+ unsafe_allow_html=True,
920
+ )
921
  if resolution_evidence:
922
+ st.markdown(
923
+ f"""
924
  <div class="resolution-evidence">
925
  <h4>Supporting Evidence:</h4>
926
  <p>{resolution_evidence}</p>
927
+ </div>""",
928
+ unsafe_allow_html=True,
929
+ )
930
+
931
 
932
  def display_emotion_assessment(emotion_data):
933
  """Displays emotion assessment data."""
934
+ if not emotion_data:
935
+ return
936
  st.markdown("### πŸ˜€ Emotional Assessment")
937
  with st.container(border=True):
938
+ col1, col2 = st.columns([2, 1])
939
  with col1:
940
  st.markdown(f"**Emotional Journey:**")
941
+ st.markdown(
942
+ f'<div class="emotion-journey">{emotion_data.get("emotional_journey", "N/A")}</div>',
943
+ unsafe_allow_html=True,
944
+ )
945
  with col2:
946
+ shift = emotion_data.get("emotional_shift", "Unknown").title()
947
+ shift_color = {
948
+ "Improved": "#4CAF50",
949
+ "Worsened": "#F44336",
950
+ "Neutral": "#607D8B",
951
+ "Fluctuated": "#FFC107",
952
+ }.get(shift, "#9E9E9E")
953
  st.metric("Emotional Shift", shift)
954
+ st.markdown(
955
+ f'<div style="width:100%; height: 5px; background-color:{shift_color}; border-radius: 5px;"></div>',
956
+ unsafe_allow_html=True,
957
+ )
958
 
959
  st.markdown(f"**Assessment Notes**:")
960
+ st.markdown(
961
+ f'<div class="emotion-journey">{emotion_data.get("assessment_notes", "N/A")}</div>',
962
+ unsafe_allow_html=True,
963
+ )
964
 
965
+ primary_emotions = emotion_data.get("primary_emotions", [])
966
  if primary_emotions:
967
  st.markdown("**Primary Emotions Detected:**")
968
+ emotion_html = "".join(
969
+ f'<span class="emotion-tag">{e.title()}</span>'
970
+ for e in primary_emotions
971
+ )
972
  st.markdown(f"<div>{emotion_html}</div>", unsafe_allow_html=True)
973
 
974
+ resp = emotion_data.get("ai_responsiveness", "Unknown").title()
975
+ resp_color = {
976
+ "Excellent": "#4CAF50",
977
+ "Good": "#8BC34A",
978
+ "Adequate": "#FFC107",
979
+ "Poor": "#FF5722",
980
+ "Inappropriate": "#F44336",
981
+ }.get(resp, "#9E9E9E")
982
+ st.markdown(
983
+ f"**AI Responsiveness to Emotion:** <span style='color: {resp_color}; font-weight: bold;'>{resp}</span>",
984
+ unsafe_allow_html=True,
985
+ )
986
+
987
 
988
  def display_single_assessment(data):
989
  """Displays a single category assessment card."""
990
+ if not data:
991
+ return
992
+ is_correct = data.get("is_correct", True)
993
  card_class = "assessment-correct" if is_correct else "assessment-incorrect"
994
  status_icon = "βœ…" if is_correct else "❌"
995
+ st.markdown(
996
+ f"""
997
  <div class="assessment-card {card_class}">
998
+ <p><strong>Status:</strong> {status_icon} {"Correct" if is_correct else "Incorrect"}</p>
999
+ <p><strong>Original:</strong> {data.get("original", "N/A")}</p>
1000
+ {"<p><strong>Suggested:</strong> " + data.get("suggested", "N/A") + "</p>" if not is_correct else ""}
1001
+ Reasoning: {data.get("reasoning", "No reasoning provided.")}
1002
+ </div>""",
1003
+ unsafe_allow_html=True,
1004
+ )
1005
+
1006
 
1007
  def display_categorization_assessment(assessment_data):
1008
  """Displays the categorization assessment section."""
1009
+ if not assessment_data:
1010
+ return
1011
  st.markdown("---")
1012
  st.markdown("## 🏷️ Call Categorization Assessment")
1013
  col1, col2 = st.columns(2)
 
1018
  st.markdown("### Subcategory")
1019
  display_single_assessment(assessment_data.get("subcategory_assessment"))
1020
 
1021
+
1022
  # --- Sidebar Navigation ---
1023
  with st.sidebar:
1024
  st.title("🧠 BrAIn Dashboard")
 
1034
  st.session_state.current_page = "call_analysis"
1035
  st.rerun()
1036
 
1037
+ kb_button_text = (
1038
+ f"πŸ”§ KB Improvements ({pending_count})"
1039
+ if pending_count > 0
1040
+ else "πŸ”§ KB Improvements"
1041
+ )
1042
  if st.button(kb_button_text, use_container_width=True):
1043
  st.session_state.current_page = "kb_improvements"
1044
  st.rerun()
1045
 
1046
+ if st.button("🚦 Hallucination Metrics", use_container_width=True):
1047
+ st.session_state.current_page = "hallucinations"
1048
+ st.rerun()
1049
+
1050
  st.markdown("---")
1051
 
1052
  # Page-specific sidebar content
 
1058
  "full_analysis": "Full Analysis",
1059
  "end_reason_analysis": "Call End Reason Analysis",
1060
  "kb_improvements": "Knowledge Base Improvements",
1061
+ "prompt_improvements": "System Prompt Improvements",
1062
  }
1063
 
1064
  analysis_focus = st.selectbox(
1065
  "Analysis Focus",
1066
  options=list(analysis_focus_options.keys()),
1067
  format_func=lambda x: analysis_focus_options[x],
1068
+ index=0,
1069
  )
1070
 
1071
  st.markdown("---")
 
1073
  use_emotion_recognition = st.toggle(
1074
  "Include audio emotion recognition",
1075
  value=False,
1076
+ help="When enabled, audio will be analyzed for emotional content before call quality analysis.",
1077
  )
1078
 
1079
  if use_emotion_recognition:
1080
+ st.info(
1081
+ "Audio emotion analysis results will be incorporated into the main call quality analysis."
1082
+ )
1083
+ num_emotion_chunks = st.slider(
1084
+ "Number of audio chunks", min_value=2, max_value=5, value=3
1085
+ )
1086
+
1087
+ custom_prompt = st.text_area(
1088
+ "Custom Analysis Prompt (Optional)",
1089
+ placeholder="Enter any specific analysis questions...",
1090
+ )
1091
 
1092
+ analyze_button = st.button(
1093
+ "Analyze Call", type="primary", use_container_width=True
1094
+ )
1095
 
1096
  st.markdown("---")
1097
  st.subheader("Display Options")
1098
  show_json = st.checkbox("Show Raw JSON", value=False)
1099
 
1100
  if st.button("Clear Results", type="secondary", use_container_width=True):
1101
+ if "analysis_result" in st.session_state:
1102
+ del st.session_state["analysis_result"]
1103
  st.rerun()
1104
 
1105
  elif st.session_state.current_page == "kb_improvements":
 
1120
  if st.session_state.current_page == "call_analysis":
1121
  st.title("πŸ“Š Call Quality Analysis Dashboard")
1122
 
1123
+ if analyze_button or ("analysis_result" in st.session_state):
1124
  if analyze_button:
1125
  emotion_results = None
1126
  # if use_emotion_recognition:
 
1131
  # else:
1132
  # st.error("Could not complete emotion analysis. Continuing without it.")
1133
 
1134
+ result = fetch_call_analysis(
1135
+ call_id, custom_prompt, analysis_focus, emotion_results
1136
+ )
1137
  if result:
1138
+ st.session_state["analysis_result"] = result
1139
+ st.session_state["current_focus"] = analysis_focus
1140
  st.success("βœ… Analysis complete!")
1141
 
1142
+ if "analysis_result" in st.session_state:
1143
+ analysis = st.session_state["analysis_result"]
1144
+ current_focus = st.session_state.get("current_focus", "full_analysis")
1145
 
1146
  st.metric("Call ID", analysis.get("call_id", call_id))
1147
 
1148
+ if current_focus != "full_analysis":
1149
+ st.info(
1150
+ f"πŸ” Analysis focused on: **{analysis_focus_options.get(current_focus, current_focus).upper()}**"
1151
+ )
1152
 
1153
  # --- Section: Resolution & Emotion ---
1154
  st.markdown("## 🎯 Resolution & Emotion")
1155
  col1, col2 = st.columns([1, 1])
1156
  with col1:
1157
  st.markdown("### Resolution Status")
1158
+ display_resolution_status(
1159
+ analysis.get("resolution_status"),
1160
+ analysis.get("resolution_evidence"),
1161
+ )
1162
  with col2:
1163
  if "emotion_assessment" in analysis:
1164
  display_emotion_assessment(analysis.get("emotion_assessment"))
 
1169
  col1, col2 = st.columns(2)
1170
  with col1:
1171
  st.markdown("### Call Summary")
1172
+ st.markdown(
1173
+ f'<div class="finding">{analysis.get("summary", "No summary.")}</div>',
1174
+ unsafe_allow_html=True,
1175
+ )
1176
  with col2:
1177
  st.markdown("### Key Findings")
1178
  findings = analysis.get("key_findings", [])
1179
  if findings:
1180
  for finding in findings:
1181
+ st.markdown(
1182
+ f'<div class="finding">βœ… {finding}</div>',
1183
+ unsafe_allow_html=True,
1184
+ )
1185
  else:
1186
  st.info("No key findings identified.")
1187
 
1188
  # --- Section: Categorization Assessment ---
1189
+ if "categorization_assessment" in analysis and analysis.get(
1190
+ "categorization_assessment"
1191
+ ):
1192
+ display_categorization_assessment(
1193
+ analysis.get("categorization_assessment")
1194
+ )
1195
 
1196
  # --- Section: Focused Analysis Displays ---
1197
  if current_focus == "full_analysis":
 
1202
  chart_col, list_col = st.columns([1, 2])
1203
  with chart_col:
1204
  issues_chart = create_issues_chart(issues)
1205
+ if issues_chart:
1206
+ st.plotly_chart(issues_chart, use_container_width=True)
1207
  with list_col:
1208
  display_issues(issues)
1209
  else:
 
1211
 
1212
  if current_focus in ["full_analysis", "end_reason_analysis"]:
1213
  st.markdown("---")
1214
+ st.markdown(
1215
+ f"<div class='{'focus-section' if current_focus == 'end_reason_analysis' else ''}'>",
1216
+ unsafe_allow_html=True,
1217
+ )
1218
  st.markdown("## πŸ›‘ Call End Reason Analysis")
1219
  end_analysis = analysis.get("end_reason_analysis", {})
1220
  if end_analysis:
1221
+ st.metric(
1222
+ "Call End Reason",
1223
+ end_analysis.get("end_reason", "N/A").replace("-", " ").title(),
1224
+ )
1225
+ st.markdown(
1226
+ f"**Assessment:** {end_analysis.get('assessment', 'N/A')}"
1227
+ )
1228
  col1, col2 = st.columns(2)
1229
  with col1:
1230
  st.markdown("#### Contributing Factors")
1231
+ for factor in end_analysis.get("contributing_factors", []):
1232
+ st.markdown(f"- {factor}")
1233
  with col2:
1234
  st.markdown("#### Improvement Opportunities")
1235
+ for opp in end_analysis.get("improvement_opportunities", []):
1236
+ st.markdown(f"πŸ’‘ {opp}")
1237
  else:
1238
  st.info("No end reason analysis available.")
1239
  st.markdown("</div>", unsafe_allow_html=True)
 
1244
 
1245
  # Define tabs based on analysis focus
1246
  if current_focus == "full_analysis":
1247
+ tab_names = [
1248
+ "General",
1249
+ "Conversation Flow",
1250
+ "Escalation",
1251
+ "Prompt",
1252
+ "Knowledge Base",
1253
+ ]
1254
  elif current_focus == "kb_improvements":
1255
  tab_names = ["Knowledge Base", "General"]
1256
  elif current_focus == "prompt_improvements":
 
1266
  with tab_map["General"]:
1267
  recs = analysis.get("general_recommendations", [])
1268
  if recs:
1269
+ for rec in recs:
1270
+ st.markdown(
1271
+ f'<div class="recommendation">πŸ”Ή {rec}</div>',
1272
+ unsafe_allow_html=True,
1273
+ )
1274
+ else:
1275
+ st.info("No general recommendations.")
1276
 
1277
  if "Conversation Flow" in tab_map:
1278
  with tab_map["Conversation Flow"]:
1279
  recs = analysis.get("conversation_recommendations", [])
1280
  if recs:
1281
+ for rec in recs:
1282
+ st.markdown(
1283
+ f'<div class="conversation-recommendation">πŸ’¬ {rec}</div>',
1284
+ unsafe_allow_html=True,
1285
+ )
1286
+ else:
1287
+ st.info("No conversation flow recommendations.")
1288
 
1289
  if "Escalation" in tab_map:
1290
  with tab_map["Escalation"]:
1291
  recs = analysis.get("escalation_recommendations", [])
1292
  if recs:
1293
+ for rec in recs:
1294
+ st.markdown(
1295
+ f'<div class="escalation-recommendation">πŸ“ˆ {rec}</div>',
1296
+ unsafe_allow_html=True,
1297
+ )
1298
+ else:
1299
+ st.info("No escalation recommendations.")
1300
 
1301
  if "Prompt" in tab_map:
1302
  with tab_map["Prompt"]:
1303
  improvements = analysis.get("prompt_improvements", [])
1304
  if improvements:
1305
  for imp in improvements:
1306
+ st.markdown(
1307
+ f"""
1308
  <div class="prompt-improvement">
1309
+ <h4>Issue: {imp.get("issue", "N/A")}</h4>
1310
+ <p><strong>Current Section:</strong> {imp.get("current_section", "N/A")}</p>
1311
+ <p><strong>Suggested Change:</strong> {imp.get("suggested_change", "N/A")}</p>
1312
+ <p><strong>Expected Outcome:</strong> {imp.get("expected_outcome", "N/A")}</p>
1313
+ </div>""",
1314
+ unsafe_allow_html=True,
1315
+ )
1316
+ else:
1317
+ st.info("No prompt improvements found.")
1318
 
1319
  if "Knowledge Base" in tab_map:
1320
  with tab_map["Knowledge Base"]:
1321
  improvements = analysis.get("kb_improvements", [])
1322
  if improvements:
1323
  for imp in improvements:
1324
+ with st.expander(
1325
+ f"**Query:** '{imp.get('query', 'Unknown')}'"
1326
+ ):
1327
+ st.markdown(
1328
+ f"""
1329
  <div class="kb-improvement">
1330
+ <p><strong>Issue:</strong> {imp.get("issue", "N/A")}</p>
1331
+ <p><strong>Suggestion:</strong> {imp.get("suggested_improvement", "N/A")}</p>
1332
+ <p><strong>Rationale:</strong> {imp.get("rationale", "N/A")}</p>
1333
+ <p><strong>Current Content:</strong> {imp.get("current_kb_content", "N/A")}</p>
1334
+ </div>""",
1335
+ unsafe_allow_html=True,
1336
+ )
1337
+ else:
1338
+ st.info("No KB improvements found.")
1339
 
1340
  if show_json:
1341
  st.markdown("---")
 
1352
  4. Click **Analyze Call** to generate the report.
1353
  """)
1354
 
1355
+ elif st.session_state.current_page == "hallucinations":
1356
+ st.title("🚦 Hallucination Metrics")
1357
+
1358
+ if not BRAIN_API_TOKEN:
1359
+ st.error("🚨 BRAIN_API_TOKEN environment variable is not set.")
1360
+ st.stop()
1361
+
1362
+ # Initialize session state for hallucination page
1363
+ if "hallucination_leaderboard" not in st.session_state:
1364
+ st.session_state.hallucination_leaderboard = None
1365
+ if "hallucination_aggregates" not in st.session_state:
1366
+ st.session_state.hallucination_aggregates = None
1367
+ if "hallucination_top" not in st.session_state:
1368
+ st.session_state.hallucination_top = None
1369
+ if "hallucination_samples" not in st.session_state:
1370
+ st.session_state.hallucination_samples = None
1371
+ if "selected_assistant_id" not in st.session_state:
1372
+ st.session_state.selected_assistant_id = None
1373
+
1374
+ # Fetch leaderboard first to populate assistant filter
1375
+ if st.session_state.hallucination_leaderboard is None:
1376
+ st.session_state.hallucination_leaderboard = fetch_assistant_leaderboard(
1377
+ limit=50
1378
+ )
1379
+
1380
+ leaderboard_data = st.session_state.hallucination_leaderboard
1381
+ leaderboard_list = []
1382
+ if leaderboard_data:
1383
+ leaderboard_list = (
1384
+ leaderboard_data.get("leaderboard", [])
1385
+ if isinstance(leaderboard_data, dict)
1386
+ else leaderboard_data
1387
+ )
1388
+
1389
+ # Build assistant options from leaderboard
1390
+ assistant_options = ["All Assistants"]
1391
+ assistant_id_map = {"All Assistants": None}
1392
+ for item in leaderboard_list:
1393
+ asst_name = item.get("assistant_name", "Unknown")
1394
+ asst_id = item.get("assistant_id", "")
1395
+ display_name = f"{asst_name} ({asst_id[:8]}...)" if asst_id else asst_name
1396
+ assistant_options.append(display_name)
1397
+ assistant_id_map[display_name] = asst_id
1398
+
1399
+ # Filters
1400
+ col_filter1, col_filter2 = st.columns(2)
1401
+ with col_filter1:
1402
+ selected_assistant_display = st.selectbox(
1403
+ "Filter by Assistant",
1404
+ options=assistant_options,
1405
+ index=0,
1406
+ )
1407
+ assistant_filter = assistant_id_map.get(selected_assistant_display)
1408
+ with col_filter2:
1409
+ topic_filter = st.text_input(
1410
+ "Topic (optional filter)",
1411
+ value=st.session_state.get("hallucination_topic", ""),
1412
+ )
1413
+
1414
+ col1, col2 = st.columns(2)
1415
+ with col1:
1416
+ aggregate_limit = st.number_input(
1417
+ "Rows (aggregates)", min_value=1, max_value=200, value=50, step=1
1418
+ )
1419
+ with col2:
1420
+ top_n = st.number_input(
1421
+ "Top hallucinators", min_value=1, max_value=50, value=10, step=1
1422
+ )
1423
+
1424
+ if st.button("Refresh metrics", type="primary", use_container_width=True):
1425
+ st.session_state.hallucination_topic = topic_filter
1426
+ with st.spinner("Loading hallucination metrics..."):
1427
+ st.session_state.hallucination_leaderboard = fetch_assistant_leaderboard(
1428
+ topic_filter or None, limit=50
1429
+ )
1430
+ st.session_state.hallucination_aggregates = fetch_eval_aggregates(
1431
+ assistant_filter, topic_filter or None, aggregate_limit
1432
+ )
1433
+ st.session_state.hallucination_top = fetch_top_hallucinators(
1434
+ assistant_filter, topic_filter or None, top_n
1435
+ )
1436
+ st.session_state.hallucination_samples = fetch_sample_unsupported(
1437
+ assistant_filter, topic_filter or None, 20
1438
+ )
1439
+ st.rerun()
1440
+
1441
+ # --- Assistant Leaderboard Section ---
1442
+ st.markdown("---")
1443
+ st.subheader("πŸ† Assistant Leaderboard")
1444
+ st.caption("Ranking of assistants by hallucination rate (highest first)")
1445
+
1446
+ leaderboard_data = st.session_state.get("hallucination_leaderboard")
1447
+ if leaderboard_data:
1448
+ leaderboard_rows = (
1449
+ leaderboard_data.get("leaderboard", [])
1450
+ if isinstance(leaderboard_data, dict)
1451
+ else leaderboard_data
1452
+ )
1453
+ if leaderboard_rows:
1454
+ # Create DataFrame for display
1455
+ lb_df = pd.DataFrame(leaderboard_rows)
1456
+
1457
+ # Display summary metrics
1458
+ if not lb_df.empty:
1459
+ metric_cols = st.columns(4)
1460
+ with metric_cols[0]:
1461
+ avg_hallucination = (
1462
+ lb_df["unsupported_claim_rate"].mean()
1463
+ if "unsupported_claim_rate" in lb_df.columns
1464
+ else 0
1465
+ )
1466
+ st.metric("Avg Hallucination Rate", f"{avg_hallucination:.1%}")
1467
+ with metric_cols[1]:
1468
+ total_claims = (
1469
+ lb_df["total_claims"].sum()
1470
+ if "total_claims" in lb_df.columns
1471
+ else 0
1472
+ )
1473
+ st.metric("Total Claims Evaluated", f"{total_claims:,}")
1474
+ with metric_cols[2]:
1475
+ total_unsupported = (
1476
+ lb_df["unsupported_claims"].sum()
1477
+ if "unsupported_claims" in lb_df.columns
1478
+ else 0
1479
+ )
1480
+ st.metric("Total Unsupported Claims", f"{total_unsupported:,}")
1481
+ with metric_cols[3]:
1482
+ num_assistants = len(lb_df)
1483
+ st.metric("Assistants Tracked", num_assistants)
1484
+
1485
+ # Display leaderboard chart
1486
+ if not lb_df.empty and "unsupported_claim_rate" in lb_df.columns:
1487
+ # Create bar chart
1488
+ chart_df = lb_df.head(15).copy()
1489
+ chart_df["assistant_label"] = chart_df.apply(
1490
+ lambda x: x.get("assistant_name", "Unknown")[:20], axis=1
1491
+ )
1492
+ chart_df["hallucination_pct"] = chart_df["unsupported_claim_rate"] * 100
1493
+
1494
+ fig = px.bar(
1495
+ chart_df,
1496
+ x="assistant_label",
1497
+ y="hallucination_pct",
1498
+ title="Hallucination Rate by Assistant",
1499
+ labels={
1500
+ "assistant_label": "Assistant",
1501
+ "hallucination_pct": "Hallucination Rate (%)",
1502
+ },
1503
+ color="hallucination_pct",
1504
+ color_continuous_scale=["green", "yellow", "red"],
1505
+ )
1506
+ fig.update_layout(
1507
+ xaxis_tickangle=-45,
1508
+ showlegend=False,
1509
+ plot_bgcolor="rgba(0,0,0,0)",
1510
+ )
1511
+ st.plotly_chart(fig, use_container_width=True)
1512
+
1513
+ # Display leaderboard table
1514
+ st.markdown("#### Detailed Leaderboard")
1515
+ display_df = lb_df.copy()
1516
+ if "unsupported_claim_rate" in display_df.columns:
1517
+ display_df["Hallucination Rate"] = display_df[
1518
+ "unsupported_claim_rate"
1519
+ ].apply(lambda x: f"{x:.1%}")
1520
+ if "grounded_claim_rate" in display_df.columns:
1521
+ display_df["Grounded Rate"] = display_df["grounded_claim_rate"].apply(
1522
+ lambda x: f"{x:.1%}"
1523
+ )
1524
+
1525
+ # Select columns for display
1526
+ display_cols = [
1527
+ "assistant_name",
1528
+ "total_claims",
1529
+ "unsupported_claims",
1530
+ "supported_claims",
1531
+ "Hallucination Rate",
1532
+ "Grounded Rate",
1533
+ ]
1534
+ available_cols = [c for c in display_cols if c in display_df.columns]
1535
+ if available_cols:
1536
+ st.dataframe(
1537
+ display_df[available_cols].rename(
1538
+ columns={
1539
+ "assistant_name": "Assistant",
1540
+ "total_claims": "Total Claims",
1541
+ "unsupported_claims": "Unsupported",
1542
+ "supported_claims": "Supported",
1543
+ }
1544
+ ),
1545
+ use_container_width=True,
1546
+ hide_index=True,
1547
+ )
1548
+ else:
1549
+ st.info("No leaderboard data available. Run some evaluations first.")
1550
+ else:
1551
+ st.info("Click 'Refresh metrics' to load the assistant leaderboard.")
1552
+
1553
+ # --- Aggregates Section ---
1554
+ aggregates = st.session_state.get("hallucination_aggregates")
1555
+ if aggregates is not None:
1556
+ st.markdown("---")
1557
+ st.subheader("πŸ“Š Run Aggregates")
1558
+ agg_rows = (
1559
+ aggregates.get("aggregates")
1560
+ if isinstance(aggregates, dict) and "aggregates" in aggregates
1561
+ else aggregates
1562
+ )
1563
+ if agg_rows:
1564
+ agg_df = pd.DataFrame(agg_rows)
1565
+ if not agg_df.empty:
1566
+ # Format rates as percentages for display
1567
+ display_agg_df = agg_df.copy()
1568
+ if "unsupported_claim_rate" in display_agg_df.columns:
1569
+ display_agg_df["unsupported_claim_rate"] = display_agg_df[
1570
+ "unsupported_claim_rate"
1571
+ ].apply(lambda x: f"{x:.1%}" if pd.notna(x) else "N/A")
1572
+ if "grounded_claim_rate" in display_agg_df.columns:
1573
+ display_agg_df["grounded_claim_rate"] = display_agg_df[
1574
+ "grounded_claim_rate"
1575
+ ].apply(lambda x: f"{x:.1%}" if pd.notna(x) else "N/A")
1576
+ if "citation_precision" in display_agg_df.columns:
1577
+ display_agg_df["citation_precision"] = display_agg_df[
1578
+ "citation_precision"
1579
+ ].apply(lambda x: f"{x:.1%}" if pd.notna(x) else "N/A")
1580
+
1581
+ st.dataframe(display_agg_df, use_container_width=True, hide_index=True)
1582
+ else:
1583
+ st.info("No aggregate data available.")
1584
+
1585
+ # --- Top Hallucinators Section ---
1586
+ top_hallucinators = st.session_state.get("hallucination_top")
1587
+ if top_hallucinators is not None:
1588
+ st.markdown("---")
1589
+ st.subheader("πŸ” Top Hallucinators (by Run)")
1590
+ st.caption("Runs with the highest count of unsupported claims")
1591
+ top_rows = (
1592
+ top_hallucinators.get("top_prompts")
1593
+ if isinstance(top_hallucinators, dict)
1594
+ and "top_prompts" in top_hallucinators
1595
+ else top_hallucinators
1596
+ )
1597
+ if top_rows:
1598
+ for idx, item in enumerate(top_rows):
1599
+ assistant_name = item.get("assistant_name", "Unknown Assistant")
1600
+ prompt_id = item.get("prompt_id", "Unknown")
1601
+ unsupported_count = item.get("unsupported_count", 0)
1602
+ run_id = item.get("run_id", "")
1603
+
1604
+ # Determine severity class
1605
+ if unsupported_count >= 5:
1606
+ severity_class = "hallucination-high"
1607
+ elif unsupported_count >= 3:
1608
+ severity_class = "hallucination-medium"
1609
+ else:
1610
+ severity_class = "hallucination-low"
1611
+
1612
+ st.markdown(
1613
+ f"""
1614
+ <div class="leaderboard-card {severity_class}">
1615
+ <span class="leaderboard-rank">#{idx + 1}</span>
1616
+ <strong>{assistant_name}</strong> - {unsupported_count} unsupported claims
1617
+ <br><small>Prompt: {prompt_id[:30]}{"..." if len(str(prompt_id)) > 30 else ""}</small>
1618
+ </div>
1619
+ """,
1620
+ unsafe_allow_html=True,
1621
+ )
1622
+ else:
1623
+ st.info("No top hallucinators found.")
1624
+
1625
+ # --- Sample Unsupported Claims Section ---
1626
+ samples = st.session_state.get("hallucination_samples")
1627
+ if samples is not None:
1628
+ st.markdown("---")
1629
+ st.subheader("πŸ” Sample Unsupported Claims")
1630
+ st.caption("Random sample of claims marked as unsupported for review")
1631
+ sample_rows = (
1632
+ samples.get("samples")
1633
+ if isinstance(samples, dict) and "samples" in samples
1634
+ else samples
1635
+ )
1636
+ if sample_rows:
1637
+ for sample in sample_rows:
1638
+ assistant_name = sample.get("assistant_name", "Unknown Assistant")
1639
+ claim_text = sample.get("claim_text", "No claim text")
1640
+ run_id = sample.get("run_id", "")
1641
+ claim_idx = sample.get("claim_idx", 0)
1642
+ citations = sample.get("citations_json", [])
1643
+ notes = sample.get("notes", "")
1644
+ assistant_id = sample.get("assistant_id")
1645
+
1646
+ with st.expander(
1647
+ f"[{assistant_name}] {claim_text[:60]}{'...' if len(claim_text) > 60 else ''}"
1648
+ ):
1649
+ st.markdown(f"**Assistant:** {assistant_name}")
1650
+ st.markdown(f"**Claim:** {claim_text}")
1651
+ st.markdown(f"**Citations:** {citations if citations else 'None'}")
1652
+ if notes:
1653
+ st.markdown(f"**Notes:** {notes}")
1654
+
1655
+ # Annotation buttons
1656
+ st.markdown("**Annotate this claim:**")
1657
+ col_tp, col_fp, col_skip = st.columns(3)
1658
+ with col_tp:
1659
+ if st.button(
1660
+ "βœ… True Positive",
1661
+ key=f"tp_{run_id}_{claim_idx}_{assistant_id or ''}",
1662
+ ):
1663
+ result = submit_annotation(
1664
+ run_id, claim_idx, "true_positive", assistant_id
1665
+ )
1666
+ if result:
1667
+ st.success("Marked as true positive!")
1668
+ with col_fp:
1669
+ if st.button(
1670
+ "❌ False Positive",
1671
+ key=f"fp_{run_id}_{claim_idx}_{assistant_id or ''}",
1672
+ ):
1673
+ result = submit_annotation(
1674
+ run_id, claim_idx, "false_positive", assistant_id
1675
+ )
1676
+ if result:
1677
+ st.success("Marked as false positive!")
1678
+ with col_skip:
1679
+ if st.button(
1680
+ "⏭️ Skip",
1681
+ key=f"skip_{run_id}_{claim_idx}_{assistant_id or ''}",
1682
+ ):
1683
+ result = submit_annotation(
1684
+ run_id, claim_idx, "skipped", assistant_id
1685
+ )
1686
+ if result:
1687
+ st.info("Skipped.")
1688
+ else:
1689
+ st.info("No sample unsupported claims found.")
1690
+
1691
  elif st.session_state.current_page == "kb_improvements":
1692
  st.title("πŸ”§ Knowledge Base Improvements")
1693
 
1694
+ kb_view = st.session_state.get("kb_view", "list")
 
 
1695
 
1696
+ if kb_view == "stats":
 
1697
  display_kb_stats()
1698
  st.markdown("---")
1699
  display_kb_improvements_list()
1700
+ elif kb_view == "list":
1701
  display_kb_improvements_list()
1702
 
1703
  elif st.session_state.current_page == "kb_review":
1704
+ selected_call_id = st.session_state.get("selected_call_id")
1705
+ if selected_call_id:
1706
+ display_kb_review_interface(selected_call_id)
1707
  else:
1708
+ st.warning("No call selected for review.")
1709
+ if st.button("Go back to KB Improvements"):
1710
+ st.session_state.current_page = "kb_improvements"
1711
+ st.rerun()
1712
 
1713
  st.markdown("---")
1714
+ st.caption(f"🧠 BrAIn Dashboard | Last updated: {datetime.now().strftime('%B %d, %Y')}")