Spaces:
Sleeping
Sleeping
| import gradio as gr | |
| import pandas as pd | |
| from pymongo import MongoClient | |
| from typing import List, Dict, Any, Optional, Tuple | |
| from datetime import datetime | |
| import json | |
| import re | |
| class ConversationAnalysisUI: | |
| """Gradio UI for displaying conversation analysis results.""" | |
| def __init__(self): | |
| # Use keshavchhaparia MongoDB instance (same as RAG system) | |
| self.mongodb_uri = "mongodb+srv://keshavchhaparia:bUSBXeVCGWDyQhDG@saaslabs.awtivxf.mongodb.net/" | |
| self.database_name = "second_brain_course" | |
| self.collection_name = "test_intercom_data" | |
| self.setup_mongodb() | |
| self.setup_ui() | |
| def setup_mongodb(self): | |
| """Initialize MongoDB connection.""" | |
| try: | |
| self.client = MongoClient(self.mongodb_uri) | |
| self.db = self.client[self.database_name] | |
| self.collection = self.db[self.collection_name] | |
| print(f"β Connected to MongoDB: {self.database_name}.{self.collection_name}") | |
| except Exception as e: | |
| print(f"β MongoDB connection failed: {e}") | |
| raise | |
| def load_conversations(self, | |
| quality_min: float = 0.0, | |
| quality_max: float = 1.0, | |
| sentiment: str = "All", | |
| search_text: str = "", | |
| limit: int = 100) -> pd.DataFrame: | |
| """Load and filter conversations.""" | |
| try: | |
| # Build query | |
| query = { | |
| 'conversation_analysis': {'$exists': True, '$ne': None}, | |
| 'content_quality_score': {'$gte': quality_min, '$lte': quality_max} | |
| } | |
| # Add sentiment filter | |
| if sentiment != "All": | |
| query['conversation_analysis.aggregated_marketing_insights.quotes.sentiment'] = sentiment | |
| # Add text search | |
| if search_text: | |
| query['$or'] = [ | |
| {'content': {'$regex': search_text, '$options': 'i'}}, | |
| {'conversation_analysis.aggregated_contextual_summary': {'$regex': search_text, '$options': 'i'}} | |
| ] | |
| # Fetch documents | |
| docs = list(self.collection.find(query).limit(limit)) | |
| # Convert to DataFrame | |
| data = [] | |
| seen_conversation_ids = set() | |
| for doc in docs: | |
| conversation_id = doc.get('metadata', {}).get('properties', {}).get('conversation_id', 'N/A') | |
| # Skip duplicates | |
| if conversation_id in seen_conversation_ids: | |
| continue | |
| seen_conversation_ids.add(conversation_id) | |
| analysis = doc.get('conversation_analysis', {}) | |
| insights = analysis.get('aggregated_marketing_insights', {}) | |
| quotes = insights.get('quotes', []) | |
| # Extract primary sentiment | |
| primary_sentiment = quotes[0].get('sentiment', 'Unknown') if quotes else 'Unknown' | |
| # Format date | |
| created_at = analysis.get('created_at', '') | |
| if isinstance(created_at, str): | |
| try: | |
| # Parse and format date | |
| dt = datetime.fromisoformat(created_at.replace('Z', '+00:00')) | |
| formatted_date = dt.strftime('%b %d, %Y %H:%M') | |
| except: | |
| formatted_date = created_at | |
| elif hasattr(created_at, 'strftime'): | |
| formatted_date = created_at.strftime('%b %d, %Y %H:%M') | |
| else: | |
| formatted_date = str(created_at) | |
| # Get full summary without truncation | |
| full_summary = analysis.get('aggregated_contextual_summary', 'No summary available') | |
| # Get a simple insights summary for the table | |
| marketing_insights = analysis.get('aggregated_marketing_insights', {}) | |
| insights_count = 0 | |
| if isinstance(marketing_insights, dict): | |
| quotes_count = len(marketing_insights.get('quotes', [])) | |
| findings_count = len(marketing_insights.get('key_findings', [])) | |
| insights_count = quotes_count + findings_count | |
| insights_text = f"{insights_count} insights available" if insights_count > 0 else "No insights available" | |
| data.append({ | |
| 'conversation_id': conversation_id, | |
| 'quality_score': round(doc.get('content_quality_score', 0.0), 2), | |
| 'sentiment': primary_sentiment, | |
| 'summary': full_summary, | |
| 'insights': insights_text, | |
| 'date': formatted_date | |
| }) | |
| return pd.DataFrame(data) | |
| except Exception as e: | |
| print(f"β Error loading conversations: {e}") | |
| return pd.DataFrame() | |
| def get_conversation_details(self, conversation_id: str) -> str: | |
| """Get detailed analysis for a specific conversation.""" | |
| try: | |
| doc = self.collection.find_one({ | |
| 'metadata.properties.conversation_id': conversation_id, | |
| 'conversation_analysis': {'$exists': True} | |
| }) | |
| if not doc: | |
| return "<p>β Conversation not found</p>" | |
| analysis = doc.get('conversation_analysis', {}) | |
| insights = analysis.get('aggregated_marketing_insights', {}) | |
| # Format the HTML content | |
| html_content = f""" | |
| <div class="conversation-details" style="background-color: white; color: #333; padding: 20px;"> | |
| <h3 style="color: #333; background-color: white;">π Conversation Analysis: {conversation_id}</h3> | |
| <div class="section" style="background-color: white; color: #333; border: 1px solid #e0e0e0; border-radius: 8px; padding: 15px; margin: 20px 0;"> | |
| <h4 style="color: #333; background-color: white;">π Summary (Contextual Summary)</h4> | |
| <div class="content-box" style="background-color: #f8f9fa; color: #333; padding: 15px; border-radius: 5px; border: 1px solid #dee2e6; margin: 10px 0;"> | |
| <p style="color: #333; background-color: transparent;">{analysis.get('aggregated_contextual_summary', 'No summary available')}</p> | |
| </div> | |
| </div> | |
| <div class="section" style="background-color: white; color: #333; border: 1px solid #e0e0e0; border-radius: 8px; padding: 15px; margin: 20px 0;"> | |
| <h4 style="color: #333; background-color: white;">π‘ Insights</h4> | |
| """ | |
| # Add quotes | |
| quotes = insights.get('quotes', []) | |
| if quotes: | |
| html_content += "<h5 style='color: #333; background-color: white;'>π Key Quotes:</h5><ul style='color: #333; background-color: white;'>" | |
| for i, quote in enumerate(quotes, 1): | |
| sentiment_class = f"sentiment-{quote.get('sentiment', 'neutral').lower()}" | |
| html_content += f""" | |
| <li style='color: #333; background-color: white;'> | |
| <div class="quote-item" style='background-color: #f8f9fa; color: #333; padding: 10px; border-radius: 5px; border-left: 4px solid #007bff; margin: 10px 0;'> | |
| <p style='color: #333; background-color: transparent;'><strong>Quote {i}:</strong> "{quote.get('quote', '')}"</p> | |
| <p style='color: #333; background-color: transparent;'><strong>Context:</strong> {quote.get('context', '')}</p> | |
| <p style='color: #333; background-color: transparent;'><strong>Sentiment:</strong> <span class="{sentiment_class}">{quote.get('sentiment', 'Unknown')}</span></p> | |
| </div> | |
| </li> | |
| """ | |
| html_content += "</ul>" | |
| # Add key findings | |
| findings = insights.get('key_findings', []) | |
| if findings: | |
| html_content += "<h5 style='color: #333; background-color: white;'>π Key Findings:</h5><ul style='color: #333; background-color: white;'>" | |
| for i, finding in enumerate(findings, 1): | |
| impact_class = f"impact-{finding.get('impact', 'medium').lower()}" | |
| html_content += f""" | |
| <li style='color: #333; background-color: white;'> | |
| <div class="finding-item" style='background-color: #f8f9fa; color: #333; padding: 10px; border-radius: 5px; border-left: 4px solid #007bff; margin: 10px 0;'> | |
| <p style='color: #333; background-color: transparent;'><strong>Finding {i}:</strong> {finding.get('finding', '')}</p> | |
| <p style='color: #333; background-color: transparent;'><strong>Evidence:</strong> {finding.get('evidence', '')}</p> | |
| <p style='color: #333; background-color: transparent;'><strong>Impact:</strong> <span class="{impact_class}">{finding.get('impact', 'Unknown')}</span></p> | |
| </div> | |
| </li> | |
| """ | |
| html_content += "</ul>" | |
| # Add follow-up email | |
| follow_up_email = analysis.get('follow_up_email', '') | |
| if follow_up_email: | |
| html_content += f""" | |
| <div class="section" style="background-color: white; color: #333; border: 1px solid #e0e0e0; border-radius: 8px; padding: 15px; margin: 20px 0;"> | |
| <h4 style="color: #333; background-color: white;">π§ Follow-up Email</h4> | |
| <div class="content-box" style="background-color: #f8f9fa; color: #333; padding: 15px; border-radius: 5px; border: 1px solid #dee2e6; margin: 10px 0;"> | |
| <pre style="color: #333; background-color: transparent; white-space: pre-wrap; font-family: monospace;">{follow_up_email}</pre> | |
| </div> | |
| </div> | |
| """ | |
| html_content += "</div>" | |
| return html_content | |
| except Exception as e: | |
| return f"<p>β Error loading conversation details: {e}</p>" | |
| def setup_ui(self): | |
| """Setup the Gradio interface.""" | |
| with gr.Blocks( | |
| title="Conversation Analysis Dashboard", | |
| theme=gr.themes.Soft(), | |
| css=""" | |
| .conversation-details { | |
| max-width: 100%; | |
| padding: 20px; | |
| font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; | |
| background-color: white; | |
| color: #333; | |
| } | |
| .section { | |
| margin: 20px 0; | |
| padding: 15px; | |
| border: 1px solid #e0e0e0; | |
| border-radius: 8px; | |
| background-color: #ffffff; | |
| color: #333; | |
| } | |
| .content-box { | |
| background-color: #f8f9fa; | |
| padding: 15px; | |
| border-radius: 5px; | |
| border: 1px solid #dee2e6; | |
| margin: 10px 0; | |
| color: #333; | |
| } | |
| .quote-item, .finding-item { | |
| margin: 10px 0; | |
| padding: 10px; | |
| background-color: #f8f9fa; | |
| border-radius: 5px; | |
| border-left: 4px solid #007bff; | |
| color: #333; | |
| } | |
| .sentiment-positive { | |
| background-color: #d4edda; | |
| color: #155724; | |
| padding: 2px 8px; | |
| border-radius: 4px; | |
| font-weight: bold; | |
| display: inline-block; | |
| } | |
| .sentiment-negative { | |
| background-color: #f8d7da; | |
| color: #721c24; | |
| padding: 2px 8px; | |
| border-radius: 4px; | |
| font-weight: bold; | |
| display: inline-block; | |
| } | |
| .sentiment-neutral { | |
| background-color: #d1ecf1; | |
| color: #0c5460; | |
| padding: 2px 8px; | |
| border-radius: 4px; | |
| font-weight: bold; | |
| display: inline-block; | |
| } | |
| .sentiment-confused { | |
| background-color: #fff3cd; | |
| color: #856404; | |
| padding: 2px 8px; | |
| border-radius: 4px; | |
| font-weight: bold; | |
| display: inline-block; | |
| } | |
| .impact-high { | |
| background-color: #f8d7da; | |
| color: #721c24; | |
| padding: 2px 8px; | |
| border-radius: 4px; | |
| font-weight: bold; | |
| display: inline-block; | |
| } | |
| .impact-medium { | |
| background-color: #fff3cd; | |
| color: #856404; | |
| padding: 2px 8px; | |
| border-radius: 4px; | |
| font-weight: bold; | |
| display: inline-block; | |
| } | |
| .impact-low { | |
| background-color: #d4edda; | |
| color: #155724; | |
| padding: 2px 8px; | |
| border-radius: 4px; | |
| font-weight: bold; | |
| display: inline-block; | |
| } | |
| .quality-high { color: #28a745; font-weight: bold; } | |
| .quality-medium { color: #ffc107; font-weight: bold; } | |
| .quality-low { color: #dc3545; font-weight: bold; } | |
| """ | |
| ) as self.interface: | |
| gr.Markdown("# π― Conversation Analysis Dashboard") | |
| gr.Markdown("Analyze customer conversations with AI-powered insights, summaries, and follow-up emails.") | |
| # Filters | |
| with gr.Row(): | |
| with gr.Column(scale=2): | |
| quality_range = gr.Slider( | |
| minimum=0.0, maximum=1.0, value=[0.0, 1.0], | |
| label="Quality Score Range", step=0.01 | |
| ) | |
| with gr.Column(scale=1): | |
| sentiment_filter = gr.Dropdown( | |
| choices=["All", "Positive", "Negative", "Neutral", "Confused"], | |
| value="All", label="Sentiment Filter" | |
| ) | |
| with gr.Column(scale=1): | |
| search_text = gr.Textbox( | |
| placeholder="Search conversations...", label="Search" | |
| ) | |
| with gr.Column(scale=1): | |
| refresh_btn = gr.Button("π Refresh", variant="primary") | |
| # Main table | |
| with gr.Row(): | |
| conversations_df = gr.Dataframe( | |
| headers=["Conversation ID", "Quality", "Sentiment", "Summary", "Insights Count", "Date"], | |
| datatype=["str", "number", "str", "str", "str", "str"], | |
| interactive=False, | |
| label="Conversations", | |
| wrap=True, # Enable text wrapping | |
| max_height=600 # Set max height for scrolling | |
| ) | |
| # Detail view | |
| with gr.Row(): | |
| with gr.Column(): | |
| detail_view = gr.HTML( | |
| value="<p>Select a conversation from the table above to view detailed analysis</p>", | |
| label="Conversation Details" | |
| ) | |
| # Event handlers | |
| def refresh_data(quality_range, sentiment, search): | |
| if isinstance(quality_range, (list, tuple)) and len(quality_range) == 2: | |
| quality_min, quality_max = quality_range | |
| else: | |
| quality_min, quality_max = 0.0, 1.0 | |
| df = self.load_conversations(quality_min, quality_max, sentiment, search, limit=1000) | |
| return df | |
| def on_table_select(evt: gr.SelectData): | |
| if evt.index[0] is not None: | |
| try: | |
| # Get the conversation ID from the selected row | |
| # We need to get the current dataframe from the table | |
| current_df = self.load_conversations() | |
| if not current_df.empty and evt.index[0] < len(current_df): | |
| conversation_id = current_df.iloc[evt.index[0]]['conversation_id'] | |
| return self.get_conversation_details(conversation_id) | |
| else: | |
| return "<p>Please refresh the data first</p>" | |
| except Exception as e: | |
| return f"<p>Error: {e}</p>" | |
| return "<p>Please select a conversation from the table</p>" | |
| refresh_btn.click( | |
| fn=refresh_data, | |
| inputs=[quality_range, sentiment_filter, search_text], | |
| outputs=[conversations_df] | |
| ) | |
| conversations_df.select( | |
| fn=on_table_select, | |
| outputs=[detail_view] | |
| ) | |
| # Load initial data when the page loads | |
| def load_initial_data(): | |
| return self.load_conversations(limit=1000) # Load more conversations | |
| # Set initial data using the interface's load event | |
| self.interface.load(load_initial_data, outputs=[conversations_df]) | |
| def launch(self, **kwargs): | |
| """Launch the Gradio interface.""" | |
| self.interface.launch(**kwargs) | |