faerazo's picture
Update ui.py
189e3b7 verified
# ui.py - Gradio User Interface
import gradio as gr
import pandas as pd
import asyncio
from datetime import datetime
import pytz
import re
from config import APP_TITLE, EXAMPLE_QUERIES
from database import get_sample_data
def format_timestamp_to_cet(iso_timestamp):
"""Format ISO timestamp to readable CET date and hour."""
try:
if not iso_timestamp or iso_timestamp == '':
return 'N/A'
# Handle different ISO timestamp formats
timestamp_str = str(iso_timestamp)
# Replace 'Z' with '+00:00' for proper ISO format
if timestamp_str.endswith('Z'):
timestamp_str = timestamp_str[:-1] + '+00:00'
# Handle timestamps with more than 6 microsecond digits
# Python's fromisoformat() only supports up to 6 digits
# Match pattern: YYYY-MM-DDTHH:MM:SS.microseconds+timezone
match = re.match(r'(\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2})\.(\d+)(.*)', timestamp_str)
if match:
date_time_part, microseconds, timezone_part = match.groups()
# Truncate microseconds to 6 digits max
microseconds = microseconds[:6]
timestamp_str = f"{date_time_part}.{microseconds}{timezone_part}"
# Parse ISO timestamp
dt = datetime.fromisoformat(timestamp_str)
# Convert to CET timezone
cet = pytz.timezone('CET')
dt_cet = dt.astimezone(cet)
# Format as readable string: "2025-09-13 14:30 CET"
return dt_cet.strftime('%Y-%m-%d %H:%M CET')
except (ValueError, AttributeError, TypeError) as e:
# Fallback to original timestamp if parsing fails
print(f"[DEBUG] Failed to format timestamp '{iso_timestamp}': {e}")
return str(iso_timestamp) if iso_timestamp else 'N/A'
def create_interface(agent):
"""Create Gradio interface for the AR Collection Agent."""
with gr.Blocks(
title=APP_TITLE,
theme=gr.themes.Soft(),
# Add viewport meta tag for better mobile/iframe handling
head="""<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">""",
css="""
/* Theme tokens */
.gradio-container {
--bg: #0b1220;
--panel: #0f172a;
--panel-elevated: #111827;
--border: #1f2937;
--text: #e5e7eb;
--muted: #9ca3af;
--accent: #6366f1;
--accent-2: #7c3aed;
--bot-accent: #06b6d4;
--bot-accent-2: #3b82f6;
}
/* Critical HF Spaces fixes - Force proper container behavior */
.gradio-container,
.gradio-container > *,
body {
max-width: 100% !important;
width: 100% !important;
overflow-x: hidden !important;
}
/* Force iframe content to be scrollable */
#root,
.gradio-container {
position: relative !important;
height: auto !important;
min-height: 100vh !important;
max-height: 100vh !important; /* Constrain to viewport to prevent infinite scrolling */
}
/* App background */
.gradio-container {
background: var(--bg) !important;
color: var(--text) !important;
font-family: 'Inter', sans-serif;
}
/* Header with same gradient as send button */
.header {
background: #f50082 !important;
color: #ffffff !important;
text-align: center;
padding: 1rem;
margin-bottom: 1rem;
border-radius: 16px !important;
border: 1px solid var(--border) !important;
}
.header h1 { margin: 0; font-size: 1.4rem; font-weight: 600; }
.header p { margin: .5rem 0 0 0; color: rgba(255,255,255,0.9); font-size: .95rem; }
/* Sidebar styling */
.sidebar {
background: var(--panel) !important;
border-radius: 16px !important;
border: 1px solid var(--border) !important;
padding: 16px !important;
margin-right: 16px !important;
min-width: 280px !important;
}
/* Main chat area */
.chat-main {
display: flex !important;
flex-direction: column !important;
}
/* Chat container - flexible height for HF Spaces */
.chat-container {
min-height: 1000px !important;
max-height: 1200px !important;
height: auto !important;
background: var(--panel) !important;
border-radius: 16px !important;
border: 1px solid var(--border) !important;
box-shadow: 0 2px 12px rgba(0,0,0,.35) !important;
overflow-y: auto !important;
flex: 1 !important;
}
/* Message row alignment */
.gradio-container .message-row {
display: flex !important;
width: 100% !important;
margin: 12px 0 !important;
padding: 0 16px !important;
}
/* BOT messages - left aligned */
.gradio-container [data-testid="bot"] {
display: flex !important;
justify-content: flex-start !important;
align-items: flex-start !important;
gap: 12px !important;
width: 100% !important;
}
/* USER messages - right aligned */
.gradio-container [data-testid="user"] {
display: flex !important;
justify-content: flex-end !important;
align-items: flex-start !important;
flex-direction: row-reverse !important;
gap: 12px !important;
width: 100% !important;
}
/* Message bubble base */
.gradio-container [data-testid="bot"] .message,
.gradio-container [data-testid="user"] .message {
background: transparent !important;
border: none !important;
padding: 0 !important;
margin: 0 !important;
display: block !important;
}
/* BOT bubble */
.gradio-container [data-testid="bot"] .message > * {
display: inline-block !important;
background: linear-gradient(135deg, var(--bot-accent) 0%, var(--bot-accent-2) 100%) !important;
color: #ffffff !important;
border-radius: 18px 18px 18px 4px !important;
padding: 12px 16px !important;
max-width: 70% !important;
word-wrap: break-word !important;
word-break: break-word !important;
white-space: pre-wrap !important;
box-shadow: 0 2px 8px rgba(0,0,0,.3) !important;
}
/* USER bubble */
.gradio-container [data-testid="user"] .message > * {
display: inline-block !important;
background: linear-gradient(135deg, var(--accent) 0%, var(--accent-2) 100%) !important;
color: #ffffff !important;
border-radius: 18px 18px 4px 18px !important;
padding: 12px 16px !important;
max-width: 70% !important;
word-wrap: break-word !important;
word-break: break-word !important;
white-space: pre-wrap !important;
box-shadow: 0 2px 8px rgba(0,0,0,.3) !important;
}
/* Text inside bubbles */
.gradio-container .message p,
.gradio-container .message div,
.gradio-container .message span {
margin: 0 !important;
padding: 0 !important;
color: inherit !important;
background: transparent !important;
word-wrap: break-word !important;
white-space: pre-wrap !important;
}
/* Avatar styling */
.gradio-container img[alt="user"],
.gradio-container img[alt="assistant"],
.gradio-container .avatar img {
width: 36px !important;
height: 36px !important;
border-radius: 50% !important;
border: 2px solid rgba(255,255,255,0.2) !important;
box-shadow: 0 2px 8px rgba(0,0,0,.3) !important;
flex-shrink: 0 !important;
}
/* Hide duplicate wrappers */
.gradio-container .message-row .message .message,
.gradio-container .prose,
[class*="markdown"] {
background: transparent !important;
border: none !important;
padding: 0 !important;
margin: 0 !important;
max-width: 100% !important;
}
/* Input area */
.gradio-container input[type="text"],
.gradio-container textarea {
background: var(--panel-elevated) !important;
color: var(--text) !important;
border: 1px solid var(--border) !important;
border-radius: 12px !important;
padding: 12px 16px !important;
font-size: 15px !important;
}
.gradio-container input[type="text"]:focus,
.gradio-container textarea:focus {
outline: none !important;
border-color: var(--accent) !important;
box-shadow: 0 0 0 3px rgba(99,102,241,0.15) !important;
}
/* Query Section Buttons - Base Styling */
.gradio-container .gr-button {
border-radius: 12px !important;
padding: 14px 20px !important;
font-weight: 600 !important;
font-size: 14px !important;
cursor: pointer !important;
transition: all 0.2s ease-in-out !important;
margin: 4px !important;
box-shadow: 0 2px 8px rgba(0,0,0,.15) !important;
border: none !important;
color: #fff !important;
}
.gradio-container .gr-button:hover {
transform: translateY(-2px) !important;
}
/* Overdue Analysis Buttons (Blue) - Force all buttons to have proper styling */
.gradio-container .gr-button[variant="primary"],
.gradio-container button[data-variant="primary"],
.gradio-container button.primary {
background: linear-gradient(135deg, #6366f1 0%, #7c3aed 100%) !important;
color: #fff !important;
border: none !important;
}
.gradio-container .gr-button[variant="primary"]:hover,
.gradio-container button[data-variant="primary"]:hover,
.gradio-container button.primary:hover {
box-shadow: 0 8px 20px rgba(99,102,241,0.4) !important;
}
/* Customer Segmentation Buttons (Teal) - Force styling */
.gradio-container .gr-button[variant="secondary"],
.gradio-container button[data-variant="secondary"],
.gradio-container button.secondary {
background: linear-gradient(135deg, #06b6d4 0%, #3b82f6 100%) !important;
color: #fff !important;
border: none !important;
}
.gradio-container .gr-button[variant="secondary"]:hover,
.gradio-container button[data-variant="secondary"]:hover,
.gradio-container button.secondary:hover {
box-shadow: 0 8px 20px rgba(6,182,212,0.4) !important;
}
/* Action Buttons (Red) - Force styling */
.gradio-container .gr-button[variant="stop"],
.gradio-container button[data-variant="stop"],
.gradio-container button.stop {
background: linear-gradient(135deg, #dc2626 0%, #b91c1c 100%) !important;
color: #fff !important;
border: none !important;
}
.gradio-container .gr-button[variant="stop"]:hover,
.gradio-container button[data-variant="stop"]:hover,
.gradio-container button.stop:hover {
box-shadow: 0 8px 20px rgba(220,38,38,0.4) !important;
}
/* Custom class-based styling */
.gradio-container .overdue-btn {
background: linear-gradient(135deg, #6366f1 0%, #7c3aed 100%) !important;
color: #fff !important;
border: none !important;
}
.gradio-container .overdue-btn:hover {
box-shadow: 0 8px 20px rgba(99,102,241,0.4) !important;
}
.gradio-container .segment-btn {
background: linear-gradient(135deg, #06b6d4 0%, #3b82f6 100%) !important;
color: #fff !important;
border: none !important;
}
.gradio-container .segment-btn:hover {
box-shadow: 0 8px 20px rgba(6,182,212,0.4) !important;
}
.gradio-container .action-btn {
background: linear-gradient(135deg, #dc2626 0%, #b91c1c 100%) !important;
color: #fff !important;
border: none !important;
}
.gradio-container .action-btn:hover {
box-shadow: 0 8px 20px rgba(220,38,38,0.4) !important;
}
/* Section Headers */
.gradio-container h3 {
color: var(--text) !important;
font-size: 1.3rem !important;
font-weight: 700 !important;
margin: 24px 0 8px 0 !important;
border-bottom: 2px solid var(--border) !important;
padding-bottom: 8px !important;
}
/* Section Descriptions */
.gradio-container p em {
color: var(--muted) !important;
font-style: italic !important;
font-size: 0.95rem !important;
margin-bottom: 16px !important;
}
/* Clear Button */
.gradio-container .gr-button:has-text("Clear Chat") {
background: linear-gradient(135deg, #6b7280 0%, #4b5563 100%) !important;
color: #fff !important;
margin-bottom: 20px !important;
}
/* Mock email styling (preserved from original) */
.mock-email {
background-color: #fffbeb;
border: 2px dashed #f59e0b;
padding: 1rem;
border-radius: 0.5rem;
}
/* Scrollbar */
::-webkit-scrollbar {
width: 8px;
}
::-webkit-scrollbar-track {
background: transparent;
}
::-webkit-scrollbar-thumb {
background: var(--border);
border-radius: 8px;
}
::-webkit-scrollbar-thumb:hover {
background: var(--muted);
}
/* Responsive tables */
.gradio-container .dataframe {
overflow-x: auto !important;
overflow-y: auto !important;
max-width: 100% !important;
max-height: calc(100vh - 300px) !important; /* Constrain DataFrame height */
}
.gradio-container table {
min-width: 600px !important; /* Ensure minimum width for readability */
font-size: 14px !important;
}
/* Specific constraints for tab DataFrames */
.gradio-container .tabitem .dataframe {
max-height: 400px !important; /* Fixed max height for DataFrames */
overflow-y: auto !important;
}
/* Constrain textboxes in tabs */
.gradio-container .tabitem textarea,
.gradio-container .tabitem .textbox textarea {
max-height: 300px !important; /* Fixed max height for textboxes */
overflow-y: auto !important;
}
/* Prevent tab content from expanding */
.gradio-container .tabitem [role="tabpanel"] > div {
max-height: none !important; /* Reset any inherited max-height */
height: auto !important; /* Allow natural sizing */
}
@media (max-width: 768px) {
.gradio-container table {
font-size: 12px !important;
}
/* Tighter constraints for mobile */
.gradio-container .tabitem .dataframe {
max-height: 300px !important; /* Fixed height on mobile */
}
.gradio-container .tabitem textarea,
.gradio-container .tabitem .textbox textarea {
max-height: 200px !important; /* Fixed height on mobile */
}
}
/* Hugging Face Spaces specific fixes */
.gradio-container {
min-height: 100vh !important;
overflow-y: visible !important; /* Let tab content handle its own scrolling */
}
/* Fix for HF iframe container */
body, html {
height: auto !important;
min-height: 100% !important;
overflow-x: hidden !important;
overflow-y: auto !important;
}
/* Ensure proper scrolling in tabs - Critical for HF Spaces */
.gradio-container .tabitem,
.gradio-container [role="tabpanel"] {
min-height: 0 !important; /* Allow natural height */
max-height: calc(100vh - 150px) !important; /* Prevent infinite expansion */
height: auto !important; /* Let content determine height */
overflow-y: auto !important; /* Enable internal scrolling */
overflow-x: hidden !important;
position: relative !important;
}
/* Make sure tab content can expand */
.gradio-container .tabs,
.gradio-container [role="tablist"] + div {
height: auto !important;
min-height: auto !important; /* Don't force full height */
}
/* Responsive Design */
@media (max-width: 1024px) {
/* Tablet and small laptop adjustments */
.sidebar {
min-width: 240px !important;
margin-right: 12px !important;
padding: 12px !important;
}
.chat-container {
min-height: 350px !important;
max-height: 500px !important;
}
.header h1 {
font-size: 1.3rem !important;
}
}
@media (max-width: 768px) {
/* Mobile landscape and small tablets */
.gradio-container .tabitem [role="tabpanel"] {
flex-direction: column !important;
min-height: calc(100vh - 200px) !important; /* Adjust for mobile header space */
max-height: calc(100vh - 200px) !important;
height: calc(100vh - 200px) !important;
}
.sidebar {
width: 100% !important;
margin-right: 0 !important;
margin-bottom: 16px !important;
order: 2 !important; /* Chat first on mobile */
}
.chat-main {
order: 1 !important;
width: 100% !important;
}
.chat-container {
min-height: 300px !important;
max-height: 400px !important;
}
.gradio-container [data-testid="bot"] .message > *,
.gradio-container [data-testid="user"] .message > * {
max-width: 85% !important;
}
.header h1 {
font-size: 1.2rem !important;
}
.gradio-container .gr-button {
font-size: 13px !important;
padding: 12px 16px !important;
}
/* Email Activity tab: stack columns vertically on mobile */
.email-activity-row {
flex-direction: column !important;
}
.email-activity-row .gradio-column {
width: 100% !important;
}
}
@media (max-width: 480px) {
/* Mobile portrait */
.chat-container {
min-height: 250px !important;
max-height: 350px !important;
}
.gradio-container .tabitem,
.gradio-container [role="tabpanel"] {
min-height: calc(100vh - 220px) !important; /* More space for mobile UI */
max-height: calc(100vh - 220px) !important;
height: calc(100vh - 220px) !important;
}
.gradio-container [data-testid="bot"] .message > *,
.gradio-container [data-testid="user"] .message > * {
max-width: 90% !important;
padding: 10px 12px !important;
}
.sidebar {
padding: 8px !important;
}
.gradio-container .gr-button {
font-size: 12px !important;
padding: 10px 14px !important;
margin: 2px !important;
}
.header h1 {
font-size: 1.1rem !important;
}
.header p {
font-size: 0.9rem !important;
}
}
"""
) as demo:
# Header
gr.HTML("""
<div class="header">
<h1>🏢 AR Collection Agent Demo</h1>
<p>Educational demonstration of an AI agent for accounts receivable collections</p>
</div>
""")
# Main Chat Tab
with gr.Tab("💬 Chat with Agent"):
with gr.Row():
# Left Sidebar - Buttons
with gr.Column(scale=1, elem_classes=["sidebar"]):
# Clear chat button
clear_btn = gr.Button("🗑️ Clear Chat", variant="secondary", scale=1)
# Organized Query Sections
gr.Markdown("### 📊 Overdue Analysis")
gr.Markdown("*Analyze overdue accounts*")
overdue_btn1 = gr.Button("Show me all late-payment customers", elem_classes=["overdue-btn"], scale=1)
overdue_btn2 = gr.Button("Which invoices are more than 30 days overdue?", elem_classes=["overdue-btn"], scale=1)
overdue_btn3 = gr.Button("Top 5 customers at risk of default", elem_classes=["overdue-btn"], scale=1)
overdue_btn4 = gr.Button("What's the most overdue account?", elem_classes=["overdue-btn"], scale=1)
gr.Markdown("### 👥 Customer Segmentation")
gr.Markdown("*Explore customer segments*")
segment_btn1 = gr.Button("Who are the VIPs with unpaid invoices?", elem_classes=["segment-btn"], scale=1)
segment_btn2 = gr.Button("Show me all Swedish customers with overdue invoices", elem_classes=["segment-btn"], scale=1)
segment_btn3 = gr.Button("Which customers are repeat late-payers in the last 12 months?", elem_classes=["segment-btn"], scale=1)
segment_btn4 = gr.Button("How much total money is outstanding?", elem_classes=["segment-btn"], scale=1)
gr.Markdown("### ⚡ Action Examples")
gr.Markdown("*AI Agent Email Campaigns*")
action_btn1 = gr.Button("Send collection emails to all overdue customers", elem_classes=["action-btn"], scale=1)
action_btn2 = gr.Button("Generate bulk emails for VIP customers only", elem_classes=["action-btn"], scale=1)
action_btn3 = gr.Button("Create targeted collection campaign for Swedish customers", elem_classes=["action-btn"], scale=1)
action_btn4 = gr.Button("Send emails to high-risk accounts only", elem_classes=["action-btn"], scale=1)
# Right Side - Chat Interface
with gr.Column(scale=2, elem_classes=["chat-main"]):
chatbot = gr.Chatbot(
height=None,
show_label=False,
bubble_full_width=False,
type='messages',
elem_classes=["chat-container"],
container=True
)
# Email Activity Log Tab
with gr.Tab("📧 Email Activity"):
gr.Markdown("""
### Simulated Email History
All emails shown here are **mock emails** generated for demonstration purposes.
""")
with gr.Row(elem_classes=["email-activity-row"]):
# Left side - Email log
with gr.Column(scale=1):
email_log = gr.DataFrame(
headers=["Timestamp", "Recipient", "Subject", "Status", "Invoice ID"],
label="Mock Emails Generated (Not Sent)",
wrap=True
)
with gr.Row():
refresh_email_btn = gr.Button("🔄 Refresh Log", scale=1)
export_btn = gr.Button("📥 Export to CSV", scale=1)
# Right side - Email preview
with gr.Column(scale=1):
email_preview = gr.Textbox(
label="Email Preview (Click on a row to view)",
lines=15,
max_lines=25,
interactive=False
)
# Database Explorer Tab
with gr.Tab("📊 Database Explorer"):
gr.Markdown("### AR Collection Data")
with gr.Row():
with gr.Column(scale=4):
search_box = gr.Textbox(
placeholder="Search by company name, email, invoice ID, country...",
label="Search AR Data"
)
with gr.Column(scale=1):
search_btn = gr.Button("🔍 Search", variant="primary")
refresh_db_btn = gr.Button("🔄 Refresh")
# Main data display
database_table = gr.DataFrame(
label="Database Records",
wrap=True,
interactive=False
)
# Export functionality
with gr.Row():
export_db_btn = gr.Button("📥 Export Current View to CSV")
exported_file = gr.File(label="Downloaded File", visible=False)
# How It Works Tab
with gr.Tab("ℹ️ How It Works"):
gr.Markdown("""
## Understanding AI Agents: The Perceive-Think-Act Pattern
This demo showcases how modern AI agents operate through an intelligent cycle:
### 1. 📊 **PERCEIVE** - Data Gathering
- **Database Queries**: The agent uses SQL to query customer and invoice data
- **Context Awareness**: Understands current date for calculating overdue periods
- **Information Synthesis**: Combines multiple data sources for complete picture
### 2. 🧠 **THINK** - Analysis & Decision Making
- **Pattern Recognition**: Identifies payment patterns and risk factors
- **Priority Assessment**: Determines which accounts need immediate attention
- **Strategy Selection**: Chooses appropriate collection approach based on:
- Days overdue
- Customer segment (VIP status)
- Payment history
- Outstanding amount
### 3. ⚡ **ACT** - Execute Actions
- **Email Generation**: Creates personalized collection emails
- **Tone Adjustment**: Varies communication based on severity
- **Activity Logging**: Records all actions for audit trail
""")
# Event Handlers
def handle_button_message(button_text, history):
"""Handle button click by processing message and returning complete history."""
if not button_text:
return history or []
# Initialize history if needed
history = history or []
# Add user message to history
history.append({"role": "user", "content": button_text})
# Get agent response (handle async call)
import asyncio
try:
response = asyncio.run(agent.process_message(button_text))
# Add assistant response to history
history.append({"role": "assistant", "content": response})
except Exception as e:
# Add error message if agent fails
error_msg = f"I apologize, but I encountered an error: {str(e)}. Please try again."
history.append({"role": "assistant", "content": error_msg})
return history
def add_user_message_from_button(button_text, history):
"""Step 1: Add user message immediately and return updated history."""
if not button_text:
return history or []
# Initialize history if needed
history = history or []
# Add user message immediately
history.append({"role": "user", "content": button_text})
# Add placeholder for assistant response
history.append({"role": "assistant", "content": "🤔 Processing your request..."})
return history
def stream_agent_response(history):
"""Step 2: Stream agent response using generator for the last user message."""
if not history or len(history) < 2:
yield history
return
# Get the last user message (second to last in history)
user_message = history[-2]["content"] if history[-2]["role"] == "user" else ""
if not user_message:
yield history
return
# Process agent response with streaming
import asyncio
try:
response = asyncio.run(agent.process_message(user_message))
# Update the assistant message progressively using line-by-line streaming
# This preserves markdown formatting (bullet points, etc.)
lines = response.split('\n')
current_response = ""
for i, line in enumerate(lines):
current_response += line
if i < len(lines) - 1: # Add newline except for the last line
current_response += "\n"
# Update the last message (assistant response)
history[-1] = {"role": "assistant", "content": current_response}
yield history
# Small delay to show streaming effect (only for first few lines)
if i < 5: # Only delay for first 5 lines to show streaming effect
import time
time.sleep(0.1) # Slightly longer delay for line-by-line
except Exception as e:
# Replace placeholder with error message
error_msg = f"I apologize, but I encountered an error: {str(e)}. Please try again."
history[-1] = {"role": "assistant", "content": error_msg}
yield history
def get_email_log():
"""Refresh email log display using database storage."""
from database import get_email_activity
# Get emails from database (persistent storage)
result = get_email_activity(page=0, page_size=100) # Get latest 100 emails
if result["success"] and result["data"]:
df = pd.DataFrame(result["data"])
# Ensure we have the required columns
required_columns = ["timestamp", "recipient", "subject", "status", "invoice_id"]
for col in required_columns:
if col not in df.columns:
df[col] = ""
# Format timestamps to readable CET format
if not df.empty and "timestamp" in df.columns:
df["timestamp"] = df["timestamp"].apply(format_timestamp_to_cet)
return df[required_columns]
else:
# Fallback to in-memory storage if database fails
email_history = agent.get_email_history()
if email_history:
df = pd.DataFrame(email_history)
# Format timestamps for fallback data too
if not df.empty and "timestamp" in df.columns:
df["timestamp"] = df["timestamp"].apply(format_timestamp_to_cet)
return df[["timestamp", "recipient", "subject", "status", "invoice_id"]]
return pd.DataFrame(columns=["timestamp", "recipient", "subject", "status", "invoice_id"])
def export_emails():
"""Export email log to CSV."""
df = get_email_log()
if not df.empty:
return gr.File.update(value=df.to_csv(index=False), visible=True)
return None
def preview_email(evt: gr.SelectData, log_data):
"""Preview selected email from database storage."""
from database import get_email_activity
try:
# Get email data from database
result = get_email_activity(page=0, page_size=100)
if result["success"] and result["data"] and evt.index[0] < len(result["data"]):
email = result["data"][evt.index[0]]
body = email.get("body", "No content available")
# Format the email preview with headers
preview_text = f"""From: AR Collection Agent
To: {email.get('recipient', 'N/A')}
Subject: {email.get('subject', 'N/A')}
Date: {email.get('timestamp', 'N/A')}
Status: {email.get('status', 'N/A')}
Tone: {email.get('tone', 'N/A')}
{body}"""
return preview_text
else:
# Fallback to in-memory storage
email_history = agent.get_email_history()
if email_history and evt.index[0] < len(email_history):
email = email_history[evt.index[0]]
return email.get("body", "No content available")
except Exception as e:
return f"Error loading email preview: {str(e)}"
return "Select an email to preview"
def clear_chat():
"""Clear chat and email history."""
agent.clear_history()
return [] # Return empty messages list
# Database Explorer Functions
def load_database_data(search_term=""):
"""Load AR data using simplified direct table approach."""
from database import get_basic_ar_data
try:
print(f"[DEBUG] Loading basic AR data with search: '{search_term}'")
# Use simplified function - direct table queries
result = get_basic_ar_data(page=0, page_size=100, search=search_term)
print(f"[DEBUG] Query success: {result.get('success', False)}")
if result["success"]:
data = result["data"]
print(f"[DEBUG] Retrieved {len(data)} records")
if data:
df = pd.DataFrame(data)
print(f"[DEBUG] DataFrame shape: {df.shape}, columns: {list(df.columns)}")
return df
else:
print("[DEBUG] No data returned - empty result set")
return pd.DataFrame(columns=['Invoice ID', 'Company Name', 'Email', 'Country', 'Amount', 'Due Date', 'Days Overdue', 'VIP', 'Status'])
else:
error_msg = result.get("error", "Unknown error")
print(f"[DEBUG] Query failed: {error_msg}")
return pd.DataFrame([{
'Error': 'Database Query Failed',
'Details': error_msg,
'Action': 'Check database connection and table structure'
}])
except Exception as e:
print(f"[DEBUG] Exception in load_database_data: {str(e)}")
return pd.DataFrame([{
'Error': 'Critical Exception',
'Details': str(e),
'Action': 'Check error logs and database setup'
}])
def export_database_view(search_term):
"""Export current AR data to CSV."""
print(f"[DEBUG] Exporting data with search term: '{search_term}'")
df = load_database_data(search_term)
if not df.empty and 'Error' not in df.columns:
filename = "ar_data_export.csv"
csv_content = df.to_csv(index=False)
print(f"[DEBUG] Exported {len(df)} rows to CSV")
return gr.File.update(value=csv_content, filename=filename, visible=True)
else:
print("[DEBUG] No data to export or error occurred")
return gr.File.update(visible=False)
# Connect event handlers for all buttons
# Overdue Analysis Examples
overdue_btn1.click(lambda h: add_user_message_from_button("Show me all late-payment customers", h), [chatbot], [chatbot]).then(
stream_agent_response, [chatbot], [chatbot]
)
overdue_btn2.click(lambda h: add_user_message_from_button("Which invoices are more than 30 days overdue?", h), [chatbot], [chatbot]).then(
stream_agent_response, [chatbot], [chatbot]
)
overdue_btn3.click(lambda h: add_user_message_from_button("Top 5 customers at risk of default", h), [chatbot], [chatbot]).then(
stream_agent_response, [chatbot], [chatbot]
)
overdue_btn4.click(lambda h: add_user_message_from_button("What's the most overdue account?", h), [chatbot], [chatbot]).then(
stream_agent_response, [chatbot], [chatbot]
)
# Customer Segmentation Examples
segment_btn1.click(lambda h: add_user_message_from_button("Who are the VIPs with unpaid invoices?", h), [chatbot], [chatbot]).then(
stream_agent_response, [chatbot], [chatbot]
)
segment_btn2.click(lambda h: add_user_message_from_button("Show me all Swedish customers with overdue invoices", h), [chatbot], [chatbot]).then(
stream_agent_response, [chatbot], [chatbot]
)
segment_btn3.click(lambda h: add_user_message_from_button("Which customers are repeat late-payers in the last 12 months?", h), [chatbot], [chatbot]).then(
stream_agent_response, [chatbot], [chatbot]
)
segment_btn4.click(lambda h: add_user_message_from_button("How much total money is outstanding?", h), [chatbot], [chatbot]).then(
stream_agent_response, [chatbot], [chatbot]
)
# Action Examples - Bulk Email Campaigns
action_btn1.click(lambda h: add_user_message_from_button("Send collection emails to all overdue customers", h), [chatbot], [chatbot]).then(
stream_agent_response, [chatbot], [chatbot]
)
action_btn2.click(lambda h: add_user_message_from_button("Generate bulk emails for VIP customers only", h), [chatbot], [chatbot]).then(
stream_agent_response, [chatbot], [chatbot]
)
action_btn3.click(lambda h: add_user_message_from_button("Create targeted collection campaign for Swedish customers", h), [chatbot], [chatbot]).then(
stream_agent_response, [chatbot], [chatbot]
)
action_btn4.click(lambda h: add_user_message_from_button("Send emails to high-risk accounts only", h), [chatbot], [chatbot]).then(
stream_agent_response, [chatbot], [chatbot]
)
# Clear chat button
clear_btn.click(clear_chat, outputs=[chatbot])
refresh_email_btn.click(get_email_log, outputs=email_log)
email_log.select(preview_email, inputs=[email_log], outputs=email_preview)
# Database Explorer Event Handlers
# Search functionality
search_btn.click(
load_database_data,
inputs=[search_box],
outputs=[database_table]
)
# Refresh button
refresh_db_btn.click(
load_database_data,
inputs=[search_box],
outputs=[database_table]
)
# Export functionality
export_db_btn.click(
export_database_view,
inputs=[search_box],
outputs=exported_file
)
# Auto-refresh email log on load
demo.load(get_email_log, outputs=email_log)
# Load initial database data with consolidated AR view
demo.load(
lambda: load_database_data(""),
outputs=[database_table]
)
return demo