import gradio as gr
import asyncio
import json
import time
import os
# Silence Matplotlib cache warnings on read-only filesystems
os.environ.setdefault("MPLCONFIGDIR", "/tmp/mpl_cache")
import logging
from pathlib import Path
import uuid
from workflow.financial_workflow_working import FinancialDocumentWorkflow
from agno.storage.sqlite import SqliteStorage
from utils.file_handler import FileHandler
from config.settings import settings
import threading
from queue import Queue
import signal
import sys
import atexit
from datetime import datetime, timedelta
from terminal_stream import terminal_manager, run_websocket_server
from collections import deque
# Configure logging - Only INFO level and above, no httpcore/debug details
# Use /tmp for file logging on Hugging Face Spaces or disable file logging if not writable
import tempfile
import os
try:
# Try to create log file in /tmp directory (works on Hugging Face Spaces)
log_dir = "/tmp"
log_file = os.path.join(log_dir, "app.log")
logging.basicConfig(
level=logging.INFO,
format="%(asctime)s - %(name)s - %(levelname)s - %(message)s",
handlers=[logging.FileHandler(log_file), logging.StreamHandler()],
)
except (PermissionError, OSError):
# Fallback to console-only logging if file logging fails
logging.basicConfig(
level=logging.INFO,
format="%(asctime)s - %(name)s - %(levelname)s - %(message)s",
handlers=[logging.StreamHandler()],
)
# Disable httpcore and other verbose loggers
logging.getLogger("httpcore").setLevel(logging.WARNING)
logging.getLogger("urllib3").setLevel(logging.WARNING)
logging.getLogger("requests").setLevel(logging.WARNING)
logging.getLogger("google").setLevel(logging.WARNING)
logging.getLogger("google.auth").setLevel(logging.WARNING)
logging.getLogger("google.api_core").setLevel(logging.WARNING)
logger = logging.getLogger(__name__)
# Auto-shutdown configuration
INACTIVITY_TIMEOUT_MINUTES = 30 # Shutdown after 30 minutes of inactivity
CHECK_INTERVAL_SECONDS = 60 # Check every minute
class AutoShutdownManager:
"""Manages automatic shutdown of the Gradio application."""
def __init__(self, timeout_minutes=INACTIVITY_TIMEOUT_MINUTES):
self.timeout_minutes = timeout_minutes
self.last_activity = datetime.now()
self.shutdown_timer = None
self.app_instance = None
self.is_shutting_down = False
self.manual_shutdown_requested = False
# Setup signal handlers for graceful shutdown
signal.signal(signal.SIGINT, self._signal_handler)
signal.signal(signal.SIGTERM, self._signal_handler)
# Register cleanup function
atexit.register(self._cleanup)
logger.info(f"AutoShutdownManager initialized with {timeout_minutes} minute timeout")
def request_shutdown(self):
"""Request manual shutdown of the application."""
logger.info("Manual shutdown requested")
self.manual_shutdown_requested = True
self._shutdown_server()
def _signal_handler(self, signum, frame):
"""Handle shutdown signals gracefully."""
logger.info(f"Received signal {signum}, initiating graceful shutdown...")
self._shutdown_server()
sys.exit(0)
def _cleanup(self):
"""Cleanup function called on exit."""
if not self.is_shutting_down:
logger.info("Application cleanup initiated")
self._shutdown_server()
def update_activity(self):
"""Update the last activity timestamp."""
self.last_activity = datetime.now()
logger.debug(f"Activity updated: {self.last_activity}")
def start_monitoring(self, app_instance):
"""Start monitoring for inactivity."""
self.app_instance = app_instance
self._start_inactivity_timer()
logger.info("Inactivity monitoring started")
def _start_inactivity_timer(self):
"""Start or restart the inactivity timer."""
if self.shutdown_timer:
self.shutdown_timer.cancel()
def check_inactivity():
if self.is_shutting_down:
return
time_since_activity = datetime.now() - self.last_activity
if time_since_activity > timedelta(minutes=self.timeout_minutes):
logger.info(f"No activity for {self.timeout_minutes} minutes, shutting down...")
self._shutdown_server()
else:
# Schedule next check
self._start_inactivity_timer()
self.shutdown_timer = threading.Timer(CHECK_INTERVAL_SECONDS, check_inactivity)
self.shutdown_timer.start()
def _shutdown_server(self):
"""Shutdown the Gradio server gracefully."""
if self.is_shutting_down:
return
self.is_shutting_down = True
logger.info("Initiating server shutdown...")
try:
if self.shutdown_timer:
self.shutdown_timer.cancel()
# Attempt graceful shutdown of components
try:
# Stop terminal WebSocket server
if hasattr(terminal_manager, 'stop_server'):
terminal_manager.stop_server()
logger.info("Terminal WebSocket server stopped")
except Exception as e:
logger.warning(f"Error stopping terminal server: {e}")
if self.app_instance:
try:
# Try to close Gradio server gracefully
if hasattr(self.app_instance, 'close'):
self.app_instance.close()
logger.info("Gradio application closed gracefully")
elif hasattr(self.app_instance, 'server'):
if hasattr(self.app_instance.server, 'close'):
self.app_instance.server.close()
logger.info("Gradio server closed")
except Exception as e:
logger.warning(f"Could not close Gradio gracefully: {e}")
# Give a moment for graceful shutdown
import time
time.sleep(1)
# If manual shutdown or graceful methods failed, exit
if self.manual_shutdown_requested:
logger.info("Forcing application exit due to manual shutdown request")
import os
os._exit(0)
else:
logger.info("Application shutdown complete")
import sys
sys.exit(0)
except Exception as e:
logger.error(f"Error during shutdown: {e}")
import os
os._exit(1)
# Global shutdown manager instance
shutdown_manager = AutoShutdownManager()
# Terminal Log Handler
class TerminalLogHandler(logging.Handler):
"""Custom logging handler that captures logs for terminal display."""
def __init__(self, max_global_logs=1000, max_session_logs=500):
super().__init__()
self.logs = deque(maxlen=max_global_logs) # Keep last N log entries
self.session_logs = {} # Per-session logs
self.max_session_logs = max_session_logs
self.cleanup_counter = 0
def emit(self, record):
"""Emit a log record."""
try:
# Skip some noisy logs but keep important ones
if record.levelname in ['DEBUG'] and record.name in ['httpcore', 'urllib3', 'requests']:
return
# Format the log message
message = record.getMessage()
# Skip empty or very short messages
if not message or len(message.strip()) < 3:
return
log_entry = {
'timestamp': datetime.fromtimestamp(record.created).strftime('%H:%M:%S'),
'level': record.levelname,
'message': message,
'logger': record.name,
'module': getattr(record, 'module', ''),
'funcName': getattr(record, 'funcName', '')
}
# Add to global logs
self.logs.append(log_entry)
# Add to session-specific logs if available
session_id = getattr(record, 'session_id', None)
if session_id:
if session_id not in self.session_logs:
self.session_logs[session_id] = deque(maxlen=self.max_session_logs)
self.session_logs[session_id].append(log_entry)
# Periodic cleanup of old sessions
self.cleanup_counter += 1
if self.cleanup_counter % 100 == 0: # Every 100 log entries
self.cleanup_old_sessions()
except Exception as e:
# Prevent logging errors from breaking the application
print(f"TerminalLogHandler error: {e}")
pass
def get_logs(self, session_id=None, limit=50):
"""Get recent logs, optionally filtered by session."""
if session_id and session_id in self.session_logs:
logs = list(self.session_logs[session_id])[-limit:]
else:
logs = list(self.logs)[-limit:]
return logs
def get_logs_as_html(self, session_id=None, limit=50):
"""Get logs formatted as HTML for terminal display."""
logs = self.get_logs(session_id, limit)
html_lines = []
for log in logs:
level_class = {
'DEBUG': 'system-line',
'INFO': 'output-line',
'WARNING': 'system-line',
'ERROR': 'error-line',
'CRITICAL': 'error-line'
}.get(log['level'], 'output-line')
html_lines.append(f'''
{log['timestamp']}
[{log['level']}] {log['logger']}: {log['message']}
''')
return ''.join(html_lines)
def cleanup_old_sessions(self, max_sessions=10):
"""Clean up old session logs to prevent memory buildup."""
if len(self.session_logs) > max_sessions:
# Keep only the most recent sessions
sessions_by_activity = []
current_time = datetime.now()
for session_id, logs in self.session_logs.items():
if logs:
# Get the timestamp of the last log entry
last_log_time = logs[-1].get('timestamp', '00:00:00')
try:
# Convert to datetime for comparison (assume today)
log_time = datetime.strptime(last_log_time, '%H:%M:%S').replace(
year=current_time.year,
month=current_time.month,
day=current_time.day
)
sessions_by_activity.append((session_id, log_time))
except:
# If parsing fails, assume it's old
sessions_by_activity.append((session_id, current_time - timedelta(hours=24)))
else:
# Empty logs are old
sessions_by_activity.append((session_id, current_time - timedelta(hours=24)))
# Sort by activity time (most recent first)
sessions_by_activity.sort(key=lambda x: x[1], reverse=True)
# Keep only the most recent sessions
sessions_to_keep = set(session_id for session_id, _ in sessions_by_activity[:max_sessions])
# Remove old sessions
removed_count = 0
for session_id in list(self.session_logs.keys()):
if session_id not in sessions_to_keep:
del self.session_logs[session_id]
removed_count += 1
if removed_count > 0:
print(f"Cleaned up {removed_count} old session logs")
def get_memory_usage(self):
"""Get memory usage statistics for the log handler."""
total_logs = len(self.logs)
total_session_logs = sum(len(logs) for logs in self.session_logs.values())
return {
'global_logs': total_logs,
'session_count': len(self.session_logs),
'total_session_logs': total_session_logs,
'total_logs': total_logs + total_session_logs
}
# Global terminal log handler
terminal_log_handler = TerminalLogHandler()
# Configure the terminal handler log level
terminal_log_handler.setLevel(logging.DEBUG)
# Add the terminal handler to the root logger and specific loggers
root_logger = logging.getLogger()
root_logger.addHandler(terminal_log_handler)
root_logger.setLevel(logging.DEBUG) # Capture more logs
# Also add to specific workflow loggers
workflow_logger = logging.getLogger('workflow')
workflow_logger.addHandler(terminal_log_handler)
workflow_logger.setLevel(logging.DEBUG)
agno_logger = logging.getLogger('agno')
agno_logger.addHandler(terminal_log_handler)
agno_logger.setLevel(logging.DEBUG)
utils_logger = logging.getLogger('utils')
utils_logger.addHandler(terminal_log_handler)
utils_logger.setLevel(logging.DEBUG)
# Keep httpx at INFO level to avoid spam
httpx_logger = logging.getLogger('httpx')
httpx_logger.addHandler(terminal_log_handler)
httpx_logger.setLevel(logging.INFO)
google_logger = logging.getLogger('google')
google_logger.addHandler(terminal_log_handler)
google_logger.setLevel(logging.INFO)
# Prompt Gallery Loader
class PromptGallery:
"""Manages loading and accessing prompt gallery from JSON configuration."""
def __init__(self):
self.prompts = {}
self.load_prompts()
def load_prompts(self):
"""Load prompts from JSON configuration file."""
try:
prompt_file = Path(settings.TEMP_DIR).parent / "config" / "prompt_gallery.json"
if prompt_file.exists():
with open(prompt_file, 'r', encoding='utf-8') as f:
self.prompts = json.load(f)
logger.info(f"Loaded prompt gallery with {len(self.prompts.get('categories', {}))} categories")
else:
logger.warning(f"Prompt gallery file not found: {prompt_file}")
self.prompts = {"categories": {}}
except Exception as e:
logger.error(f"Error loading prompt gallery: {e}")
self.prompts = {"categories": {}}
def get_categories(self):
"""Get all available prompt categories."""
return self.prompts.get('categories', {})
def get_prompts_for_category(self, category_id):
"""Get all prompts for a specific category."""
return self.prompts.get('categories', {}).get(category_id, {}).get('prompts', [])
def get_prompt_by_id(self, category_id, prompt_id):
"""Get a specific prompt by category and prompt ID."""
prompts = self.get_prompts_for_category(category_id)
for prompt in prompts:
if prompt.get('id') == prompt_id:
return prompt
return None
# Global prompt gallery instance
prompt_gallery = PromptGallery()
# Custom CSS for beautiful multi-agent streaming interface
custom_css = """
/* Main container styling */
.main-container {
max-width: 1400px;
margin: 0 auto;
}
/* Dynamic Single-Panel Workflow Layout */
.workflow-progress-nav {
display: flex;
justify-content: space-between;
align-items: center;
background: var(--background-fill-secondary);
border: 1px solid var(--border-color-primary);
border-radius: 12px;
padding: 16px;
margin: 16px 0;
gap: 8px;
}
.progress-nav-item {
display: flex;
flex-direction: column;
align-items: center;
padding: 12px 16px;
border-radius: 8px;
cursor: pointer;
transition: all 0.3s ease;
flex: 1;
text-align: center;
position: relative;
}
.progress-nav-item.pending {
background: rgba(107, 114, 128, 0.1);
color: var(--body-text-color-subdued);
}
.progress-nav-item.active {
background: rgba(59, 130, 246, 0.1);
color: #3b82f6;
border: 2px solid #3b82f6;
}
.progress-nav-item.current {
background: rgba(102, 126, 234, 0.2);
color: #667eea;
border: 2px solid #667eea;
transform: scale(1.05);
}
.progress-nav-item.completed {
background: rgba(16, 185, 129, 0.1);
color: #10b981;
border: 2px solid #10b981;
}
.progress-nav-item.clickable:hover {
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
}
.nav-icon {
font-size: 24px;
margin-bottom: 8px;
}
.nav-label {
font-size: 12px;
font-weight: 600;
margin-bottom: 4px;
}
.nav-status {
font-size: 10px;
opacity: 0.7;
}
.active-agent-panel {
background: var(--background-fill-secondary);
border: 2px solid var(--border-color-primary);
border-radius: 16px;
margin: 16px 0;
overflow: hidden;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.1);
transition: all 0.3s ease;
}
.agent-panel-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 20px 24px;
background: linear-gradient(135deg, var(--background-fill-primary) 0%, var(--background-fill-secondary) 100%);
border-bottom: 1px solid var(--border-color-primary);
}
.agent-info {
display: flex;
align-items: center;
gap: 16px;
}
.agent-icon-large {
font-size: 32px;
padding: 12px;
background: var(--background-fill-primary);
border-radius: 12px;
border: 2px solid var(--border-color-accent);
}
.agent-details h3.agent-title {
margin: 0 0 4px 0;
font-size: 20px;
font-weight: 700;
color: var(--body-text-color);
}
.agent-details p.agent-description {
margin: 0;
font-size: 14px;
color: var(--body-text-color-subdued);
}
.agent-status-badge {
padding: 8px 16px;
border-radius: 20px;
color: white;
font-weight: 600;
font-size: 12px;
text-transform: uppercase;
letter-spacing: 0.5px;
}
.agent-content-area {
padding: 24px;
min-height: 200px;
max-height: 400px;
overflow-y: auto;
}
.agent-content {
font-family: var(--font-mono);
font-size: 14px;
line-height: 1.6;
color: var(--body-text-color);
white-space: pre-wrap;
word-wrap: break-word;
}
.agent-content.streaming {
border-left: 3px solid #3b82f6;
padding-left: 12px;
background: rgba(59, 130, 246, 0.02);
}
.agent-waiting,
.agent-starting,
.agent-empty {
display: flex;
align-items: center;
justify-content: center;
height: 120px;
color: var(--body-text-color-subdued);
font-style: italic;
font-size: 16px;
}
.typing-cursor {
animation: blink 1s infinite;
color: #3b82f6;
font-weight: bold;
}
/* Legacy Multi-Agent Workflow Layout (kept for compatibility) */
.workflow-container {
display: grid;
grid-template-columns: 1fr;
gap: 12px;
margin: 16px 0;
}
.agent-panel {
background: var(--background-fill-secondary);
border: 2px solid var(--border-color-primary);
border-radius: 12px;
padding: 16px;
margin: 8px 0;
transition: all 0.3s ease;
position: relative;
overflow: hidden;
}
.agent-panel.active {
border-color: var(--color-accent);
box-shadow: 0 4px 20px rgba(102, 126, 234, 0.2);
transform: translateY(-2px);
}
.agent-panel.completed {
border-color: var(--color-success);
background: rgba(17, 153, 142, 0.05);
}
.agent-panel.streaming {
border-color: var(--color-accent);
background: rgba(102, 126, 234, 0.05);
}
.agent-header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 12px;
padding-bottom: 8px;
border-bottom: 1px solid var(--border-color-primary);
}
.agent-info {
display: flex;
align-items: center;
gap: 12px;
}
.agent-icon {
font-size: 24px;
animation: pulse 2s infinite;
}
.agent-icon.active {
animation: bounce 1s infinite;
}
.agent-name {
font-size: 18px;
font-weight: 600;
color: var(--body-text-color);
}
.agent-description {
font-size: 14px;
color: var(--body-text-color-subdued);
margin-top: 4px;
}
.agent-status {
display: flex;
align-items: center;
gap: 8px;
font-size: 14px;
font-weight: 500;
}
.status-indicator {
width: 12px;
height: 12px;
border-radius: 50%;
animation: pulse 2s infinite;
}
.status-indicator.pending {
background: var(--color-neutral);
}
.status-indicator.starting {
background: var(--color-warning);
animation: flash 1s infinite;
}
.status-indicator.streaming {
background: var(--color-accent);
animation: pulse 1s infinite;
}
.status-indicator.completed {
background: var(--color-success);
animation: none;
}
.agent-thinking {
background: var(--background-fill-primary);
border: 1px solid var(--border-color-primary);
border-radius: 8px;
padding: 12px;
min-height: 120px;
max-height: 300px;
overflow-y: auto;
font-family: var(--font-mono);
font-size: 13px;
line-height: 1.5;
color: var(--body-text-color);
white-space: pre-wrap;
word-wrap: break-word;
}
.agent-thinking.streaming {
border-color: var(--color-accent);
background: rgba(102, 126, 234, 0.02);
}
.agent-thinking.empty {
display: flex;
align-items: center;
justify-content: center;
color: var(--body-text-color-subdued);
font-style: italic;
}
.thinking-cursor {
display: inline-block;
width: 2px;
height: 16px;
background: var(--color-accent);
margin-left: 2px;
animation: blink 1s infinite;
}
/* Workflow Progress Overview */
.workflow-progress {
display: flex;
justify-content: space-between;
align-items: center;
background: var(--background-fill-secondary);
border: 1px solid var(--border-color-primary);
border-radius: 8px;
padding: 16px;
margin: 16px 0;
}
.progress-step-mini {
display: flex;
flex-direction: column;
align-items: center;
gap: 8px;
flex: 1;
position: relative;
}
.progress-step-mini::after {
content: '';
position: absolute;
top: 12px;
right: -50%;
width: 100%;
height: 2px;
background: var(--border-color-primary);
z-index: 1;
}
.progress-step-mini:last-child::after {
display: none;
}
.mini-icon {
font-size: 20px;
padding: 8px;
border-radius: 50%;
background: var(--background-fill-primary);
border: 2px solid var(--border-color-primary);
z-index: 2;
position: relative;
}
.mini-icon.active {
border-color: var(--color-accent);
background: var(--color-accent);
color: white;
animation: pulse 1s infinite;
}
.mini-icon.completed {
border-color: var(--color-success);
background: var(--color-success);
color: white;
}
.mini-label {
font-size: 12px;
font-weight: 500;
color: var(--body-text-color);
text-align: center;
}
/* Animations */
@keyframes bounce {
0%, 20%, 50%, 80%, 100% { transform: translateY(0); }
40% { transform: translateY(-10px); }
60% { transform: translateY(-5px); }
}
@keyframes flash {
0%, 50%, 100% { opacity: 1; }
25%, 75% { opacity: 0.5; }
}
@keyframes blink {
0%, 50% { opacity: 1; }
51%, 100% { opacity: 0; }
}
@keyframes typewriter {
from { width: 0; }
to { width: 100%; }
}
/* Single step container styling */
.single-step-container {
background: var(--background-fill-secondary);
border: 1px solid var(--border-color-primary);
border-radius: 8px;
padding: 16px;
margin: 8px 0;
font-family: var(--font-mono);
}
.steps-overview {
display: flex;
flex-wrap: wrap;
gap: 8px;
margin-bottom: 16px;
padding-bottom: 12px;
border-bottom: 1px solid var(--border-color-primary);
}
.step-overview-item {
padding: 4px 8px;
border-radius: 4px;
font-size: 12px;
font-weight: 500;
background: var(--background-fill-primary);
border: 1px solid var(--border-color-primary);
}
.step-overview-item.current-step {
background: var(--color-accent);
color: white;
border-color: var(--color-accent);
}
.step-overview-item.completed-step {
background: var(--color-success);
color: white;
border-color: var(--color-success);
cursor: pointer;
transition: all 0.2s ease;
}
.step-overview-item.completed-step:hover {
transform: translateY(-1px);
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
}
.step-overview-item.clickable {
cursor: pointer;
user-select: none;
}
.step-overview-item.other-step {
opacity: 0.7;
}
/* Content formatting styles */
.code-content, .json-content, .text-content {
background: var(--background-fill-primary);
border: 1px solid var(--border-color-primary);
border-radius: 4px;
margin: 8px 0;
}
.code-header, .content-header {
display: flex;
justify-content: space-between;
align-items: center;
background: var(--background-fill-secondary);
padding: 8px 12px;
border-bottom: 1px solid var(--border-color-primary);
font-size: 12px;
font-weight: 600;
}
.code-label, .content-label {
color: var(--body-text-color);
}
.code-language, .content-type {
background: var(--color-accent);
color: white;
padding: 2px 6px;
border-radius: 3px;
font-size: 10px;
}
.code-block, .json-block, .text-block {
margin: 0;
padding: 12px;
font-family: var(--font-mono);
font-size: 12px;
line-height: 1.4;
overflow-x: auto;
background: var(--background-fill-primary);
color: var(--body-text-color);
}
.empty-content {
padding: 20px;
text-align: center;
color: var(--body-text-color-subdued);
font-style: italic;
}
/* New step content wrapper styles */
.step-content-wrapper {
background: var(--background-fill-primary);
border: 1px solid var(--border-color-primary);
border-radius: 8px;
margin: 12px 0;
overflow: hidden;
}
.step-content-header {
background: var(--background-fill-secondary);
padding: 12px 16px;
border-bottom: 1px solid var(--border-color-primary);
display: flex;
align-items: center;
gap: 8px;
font-weight: 600;
font-size: 14px;
}
.step-icon {
font-size: 18px;
}
.step-label {
color: var(--body-text-color);
}
.step-content-body {
padding: 16px;
line-height: 1.6;
}
.markdown-content {
font-family: var(--font-sans);
color: var(--body-text-color);
}
.markdown-content h1, .markdown-content h2, .markdown-content h3,
.markdown-content h4, .markdown-content h5, .markdown-content h6 {
margin: 16px 0 8px 0;
font-weight: 600;
color: var(--body-text-color);
}
.markdown-content h1 { font-size: 24px; }
.markdown-content h2 { font-size: 20px; }
.markdown-content h3 { font-size: 18px; }
.markdown-content h4 { font-size: 16px; }
.markdown-content h5 { font-size: 14px; }
.markdown-content h6 { font-size: 12px; }
.markdown-content p {
margin: 8px 0;
color: var(--body-text-color);
}
.markdown-content li {
margin: 4px 0;
padding-left: 8px;
list-style-type: disc;
color: var(--body-text-color);
}
.markdown-content ul {
margin: 8px 0;
padding-left: 20px;
}
.markdown-content ol {
margin: 8px 0;
padding-left: 20px;
}
.markdown-content strong {
font-weight: 600;
color: var(--body-text-color);
}
.markdown-content em {
font-style: italic;
color: var(--body-text-color-subdued);
}
.markdown-content code {
background: var(--background-fill-secondary);
padding: 2px 4px;
border-radius: 3px;
font-family: var(--font-mono);
font-size: 13px;
color: var(--body-text-color);
}
.formatted-content {
font-family: var(--font-sans);
line-height: 1.6;
color: var(--body-text-color);
}
.error-content {
background: #fee;
border: 1px solid #fcc;
border-radius: 4px;
padding: 12px;
color: #c33;
font-family: var(--font-mono);
font-size: 12px;
}
/* Step type specific styling */
.code-step .step-content-header {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
}
.data-step .step-content-header {
background: linear-gradient(135deg, #11998e 0%, #38ef7d 100%);
color: white;
}
.prompts-step .step-content-header {
background: linear-gradient(135deg, #ff6b6b 0%, #feca57 100%);
color: white;
}
.default-step .step-content-header {
background: linear-gradient(135deg, #74b9ff 0%, #0984e3 100%);
color: white;
}
.current-step-details {
background: var(--background-fill-primary);
border: 1px solid var(--border-color-primary);
border-radius: 4px;
padding: 12px;
}
.step-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 8px;
padding-bottom: 8px;
border-bottom: 1px solid var(--border-color-primary);
}
.step-title {
font-weight: 600;
font-size: 14px;
color: var(--body-text-color);
}
.step-progress {
font-size: 12px;
font-weight: 500;
color: var(--body-text-color-subdued);
}
.step-description {
font-size: 12px;
color: var(--body-text-color-subdued);
margin-bottom: 8px;
font-style: italic;
}
.step-content {
background: var(--background-fill-secondary);
border: 1px solid var(--border-color-primary);
border-radius: 4px;
padding: 12px;
margin-top: 8px;
max-height: 200px;
overflow-y: auto;
}
.step-content pre {
margin: 0;
font-family: var(--font-mono);
font-size: 12px;
line-height: 1.4;
color: var(--body-text-color);
white-space: pre-wrap;
word-wrap: break-word;
}
/* Progress bar styling */
.progress-container {
margin: 20px 0;
}
.progress-step {
display: flex;
align-items: center;
margin: 10px 0;
padding: 10px;
border-radius: 10px;
background: rgba(255, 255, 255, 0.05);
transition: all 0.3s ease;
}
.progress-step.active {
background: rgba(102, 126, 234, 0.2);
transform: scale(1.02);
}
.progress-step.completed {
background: rgba(17, 153, 142, 0.2);
}
.step-icon {
font-size: 24px;
margin-right: 15px;
animation: pulse 2s infinite;
}
@keyframes pulse {
0% { transform: scale(1); }
50% { transform: scale(1.1); }
100% { transform: scale(1); }
}
/* Fade in animation */
.fade-in {
animation: fadeIn 0.5s ease-in;
}
@keyframes fadeIn {
from { opacity: 0; transform: translateY(20px); }
to { opacity: 1; transform: translateY(0); }
}
/* Typing indicator */
.typing-indicator {
display: inline-block;
width: 20px;
height: 10px;
}
.typing-indicator span {
display: inline-block;
width: 8px;
height: 8px;
border-radius: 50%;
background: #667eea;
margin: 0 2px;
animation: typing 1.4s infinite ease-in-out;
}
.typing-indicator span:nth-child(1) { animation-delay: -0.32s; }
.typing-indicator span:nth-child(2) { animation-delay: -0.16s; }
@keyframes typing {
0%, 80%, 100% { transform: scale(0.8); opacity: 0.5; }
40% { transform: scale(1); opacity: 1; }
}
/* Header styling */
.header-title {
font-size: 1.2rem;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
margin: 0;
text-align: left;
padding: 0.5rem 0;
}
/* Status indicators */
.status-success {
color: #38ef7d;
font-weight: bold;
}
.status-error {
color: #ff6b6b;
font-weight: bold;
}
.status-processing {
color: #667eea;
font-weight: bold;
}
/* Download button styling */
.download-section {
text-align: center;
margin: 20px 0;
}
.download-btn {
background: linear-gradient(135deg, #38ef7d, #11998e);
color: white;
border: none;
padding: 12px 24px;
border-radius: 8px;
font-size: 16px;
font-weight: 600;
cursor: pointer;
transition: all 0.3s ease;
box-shadow: 0 4px 12px rgba(56, 239, 125, 0.3);
}
.download-btn:hover {
transform: translateY(-2px);
box-shadow: 0 6px 16px rgba(56, 239, 125, 0.4);
}
.download-btn:active {
transform: translateY(2px);
box-shadow: 0 2px 6px rgba(56, 239, 125, 0.2);
}
/* Terminal Component Styling */
.terminal-container {
display: flex;
flex-direction: column;
height: 750px;
background: linear-gradient(135deg, #0d1117 0%, #161b22 100%);
border: 1px solid #30363d;
border-radius: 8px;
font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
overflow: hidden;
margin: 0;
}
.terminal-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 12px 16px;
background: #161b22;
border-bottom: 1px solid #30363d;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3);
}
.terminal-title {
display: flex;
align-items: center;
gap: 8px;
font-size: 14px;
font-weight: 600;
color: #f0f6fc;
}
.terminal-icon {
width: 16px;
height: 16px;
background: #238636;
border-radius: 50%;
position: relative;
}
.terminal-icon::after {
content: '>';
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
font-size: 10px;
color: white;
font-weight: bold;
}
.terminal-controls {
display: flex;
gap: 8px;
}
.control-btn {
width: 12px;
height: 12px;
border-radius: 50%;
border: none;
cursor: pointer;
transition: opacity 0.2s;
}
.control-btn:hover {
opacity: 0.8;
}
.close { background: #ff5f56; }
.minimize { background: #ffbd2e; }
.maximize { background: #27ca3f; }
.terminal-body {
flex: 1;
display: flex;
flex-direction: column;
overflow: hidden;
}
.terminal-output {
flex: 1;
padding: 8px;
overflow-y: auto;
font-size: 10px;
line-height: 1.2;
background: #0d1117;
color: #c9d1d9;
scrollbar-width: thin;
scrollbar-color: #30363d #0d1117;
height: 100%;
word-wrap: break-word;
white-space: pre-wrap;
}
.terminal-output::-webkit-scrollbar {
width: 8px;
}
.terminal-output::-webkit-scrollbar-track {
background: #0d1117;
}
.terminal-output::-webkit-scrollbar-thumb {
background: #30363d;
border-radius: 4px;
}
.terminal-output::-webkit-scrollbar-thumb:hover {
background: #484f58;
}
.terminal-line {
margin-bottom: 1px;
white-space: pre-wrap;
word-wrap: break-word;
display: block;
width: 100%;
}
.command-line {
color: #58a6ff;
font-weight: 600;
}
.output-line {
color: #c9d1d9;
}
.error-line {
color: #f85149;
}
.success-line {
color: #56d364;
}
.system-line {
color: #ffa657;
font-style: italic;
}
.timestamp {
color: #7d8590;
font-size: 8px;
margin-right: 4px;
display: inline-block;
min-width: 60px;
}
.terminal-input {
display: flex;
align-items: center;
padding: 12px 16px;
background: #161b22;
border-top: 1px solid #30363d;
}
.prompt {
color: #58a6ff;
margin-right: 8px;
font-weight: 600;
}
.input-field {
flex: 1;
background: transparent;
border: none;
color: #c9d1d9;
font-family: inherit;
font-size: 11px;
outline: none;
}
.input-field::placeholder {
color: #7d8590;
}
.status-indicator {
display: flex;
align-items: center;
gap: 8px;
margin-left: 12px;
}
.status-dot {
width: 8px;
height: 8px;
border-radius: 50%;
background: #7d8590;
transition: background-color 0.3s;
}
.status-dot.connected {
background: #56d364;
box-shadow: 0 0 8px rgba(86, 211, 100, 0.5);
}
.status-dot.running {
background: #ffa657;
animation: pulse 1.5s infinite;
}
.status-dot.error {
background: #f85149;
}
@keyframes terminal-pulse {
0%, 100% { opacity: 1; }
50% { opacity: 0.5; }
}
/* Prompt Gallery Styling */
.prompt-gallery {
background: var(--background-fill-secondary);
border: 1px solid var(--border-color-primary);
border-radius: 8px;
padding: 16px;
margin: 8px 0;
}
.prompt-card {
background: var(--background-fill-primary);
border: 1px solid var(--border-color-accent);
border-radius: 6px;
padding: 12px;
margin: 8px 0;
cursor: pointer;
transition: all 0.3s ease;
}
.prompt-card:hover {
background: var(--background-fill-secondary);
border-color: var(--color-accent);
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
}
.prompt-card-header {
display: flex;
align-items: center;
gap: 8px;
margin-bottom: 8px;
}
.prompt-card-title {
font-weight: 600;
color: var(--body-text-color);
margin: 0;
}
.prompt-card-description {
color: var(--body-text-color-subdued);
font-size: 0.9em;
margin: 0;
}
.prompt-preview {
background: var(--background-fill-secondary);
border: 1px solid var(--border-color-primary);
border-radius: 4px;
padding: 8px;
margin-top: 8px;
font-size: 0.85em;
color: var(--body-text-color-subdued);
max-height: 100px;
overflow-y: auto;
}
.gallery-category {
margin-bottom: 16px;
}
.category-header {
display: flex;
align-items: center;
gap: 8px;
margin-bottom: 12px;
padding-bottom: 8px;
border-bottom: 2px solid var(--border-color-accent);
}
.category-title {
font-size: 1.1em;
font-weight: 600;
color: var(--body-text-color);
margin: 0;
}
.use-prompt-btn {
background: linear-gradient(135deg, #667eea, #764ba2);
color: white;
border: none;
padding: 6px 12px;
border-radius: 4px;
font-size: 0.85em;
cursor: pointer;
transition: all 0.3s ease;
margin-top: 8px;
}
.use-prompt-btn:hover {
background: linear-gradient(135deg, #764ba2, #667eea);
transform: translateY(-1px);
box-shadow: 0 2px 8px rgba(102, 126, 234, 0.3);
}
"""
class WorkflowUI:
def __init__(self):
self.file_handler = FileHandler()
self.session_id = str(uuid.uuid4())[:8] # Generate our own session ID
# Create workflow with database storage for caching
self.workflow = FinancialDocumentWorkflow(
session_id=self.session_id,
storage=SqliteStorage(
table_name="financial_workflows",
db_file=str(Path(settings.TEMP_DIR) / "workflows.db")
)
)
self.processing_started = False
self.selected_prompt = None
# Simple step configuration for UI display
self.steps_config = {
"extraction": {
"name": "Financial Data Extraction",
"description": "Extracting financial data points from document",
"icon": "š"
},
"arrangement": {
"name": "Data Analysis & Organization",
"description": "Organizing and analyzing extracted financial data",
"icon": "š"
},
"code_generation": {
"name": "Excel Code Generation",
"description": "Generating Python code for Excel reports",
"icon": "š»"
},
"execution": {
"name": "Excel Report Creation",
"description": "Executing code to create Excel workbook",
"icon": "š"
}
}
def validate_file(self, file_path):
"""Validate uploaded file."""
logger.info(f"Validating file: {file_path}")
if not file_path:
logger.warning("No file uploaded")
return {"valid": False, "error": "No file uploaded"}
path = Path(file_path)
if not path.exists():
logger.error(f"File does not exist: {file_path}")
return {"valid": False, "error": "File does not exist"}
file_extension = path.suffix.lower().lstrip(".")
if file_extension not in settings.SUPPORTED_FILE_TYPES:
logger.error(f"Unsupported file type: {file_extension}")
return {
"valid": False,
"error": f"Unsupported file type. Supported: {', '.join(settings.SUPPORTED_FILE_TYPES)}",
}
file_size_mb = path.stat().st_size / (1024 * 1024)
if file_size_mb > 50: # 50MB limit
logger.error(f"File too large: {file_size_mb}MB")
return {"valid": False, "error": "File too large (max 50MB)"}
logger.info(
f"File validation successful: {path.name} ({file_extension}, {file_size_mb}MB)"
)
return {
"valid": True,
"file_info": {
"name": path.name,
"type": file_extension,
"size_mb": round(file_size_mb, 2),
},
}
file_size_mb = path.stat().st_size / (1024 * 1024)
if file_size_mb > 50: # 50MB limit
return {"valid": False, "error": "File too large (max 50MB)"}
return {
"valid": True,
"file_info": {
"name": path.name,
"type": file_extension,
"size_mb": round(file_size_mb, 2),
},
}
def get_file_preview(self, file_path):
"""Get file preview."""
try:
path = Path(file_path)
if path.suffix.lower() in [".txt", ".md", ".py", ".json"]:
with open(path, "r", encoding="utf-8") as f:
content = f.read()
return content[:1000] + "..." if len(content) > 1000 else content
else:
return f"Binary file: {path.name} ({path.suffix})"
except Exception as e:
return f"Error reading file: {str(e)}"
def get_prompt_text(self, category_id, prompt_id):
"""Get the full text of a specific prompt."""
prompt = prompt_gallery.get_prompt_by_id(category_id, prompt_id)
return prompt.get('prompt', '') if prompt else ''
def download_processed_files(self):
"""Create a zip file of all processed files and return for download."""
# Update activity for auto-shutdown monitoring
shutdown_manager.update_activity()
try:
import zipfile
import os
import shutil
from datetime import datetime
# Get session output directory - now using workflow's output directory
session_output_dir = self.workflow.session_output_dir
if not session_output_dir.exists():
logger.warning(f"Output directory does not exist: {session_output_dir}")
return None
# Create a properly named zip file in a location that Gradio can access
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
zip_filename = f"processed_files_{self.session_id}_{timestamp}.zip"
# Use application's TEMP_DIR instead of system temp for cross-platform compatibility
# This ensures Gradio can access it properly across all systems
downloads_dir = Path(settings.TEMP_DIR) / "downloads"
downloads_dir.mkdir(parents=True, exist_ok=True)
# Clean up old download files (older than 1 hour) to prevent disk space issues
try:
import time
current_time = time.time()
for old_file in downloads_dir.glob("*.zip"):
if current_time - old_file.stat().st_mtime > 3600: # 1 hour
old_file.unlink()
logger.debug(f"Cleaned up old download file: {old_file.name}")
except Exception as cleanup_error:
logger.warning(f"Could not clean up old download files: {cleanup_error}")
zip_path = downloads_dir / zip_filename
with zipfile.ZipFile(zip_path, 'w', zipfile.ZIP_DEFLATED) as zipf:
# Add all files from output directory
file_count = 0
for file_path in session_output_dir.rglob('*'):
if file_path.is_file():
# Calculate relative path for zip
arcname = file_path.relative_to(session_output_dir)
zipf.write(file_path, arcname)
file_count += 1
logger.debug(f"Added to zip: {arcname}")
if file_count == 0:
logger.warning("No files found to download")
# Debug: List all files in session directory
session_dir = Path(settings.TEMP_DIR) / self.session_id
if session_dir.exists():
logger.info(f"Session directory exists: {session_dir}")
for subdir in ['input', 'output', 'temp']:
subdir_path = session_dir / subdir
if subdir_path.exists():
files = list(subdir_path.glob('*'))
logger.info(f"{subdir} directory has {len(files)} files: {[f.name for f in files]}")
else:
logger.info(f"{subdir} directory does not exist")
else:
logger.warning(f"Session directory does not exist: {session_dir}")
# Clean up empty zip file
if zip_path.exists():
zip_path.unlink()
return None
logger.info(f"Created zip file with {file_count} files: {zip_path}")
# Ensure the file exists and has content
if zip_path.exists() and zip_path.stat().st_size > 0:
# For cross-platform compatibility, use forward slashes and resolve path
abs_path = str(zip_path.resolve())
logger.info(f"Returning zip file path for download: {abs_path}")
logger.info(f"File size: {zip_path.stat().st_size} bytes")
# Set proper permissions for cross-platform access
try:
os.chmod(abs_path, 0o644)
except (OSError, PermissionError) as e:
logger.warning(f"Could not set file permissions: {e}")
# Return the file path for Gradio to handle
# Use resolved absolute path for better cross-platform compatibility
return abs_path
else:
logger.error("Zip file was created but is empty or doesn't exist")
return None
except Exception as e:
logger.error(f"Error creating download: {str(e)}")
import traceback
logger.error(f"Traceback: {traceback.format_exc()}")
return None
def create_gradio_app():
"""Create the main Gradio application."""
# Start WebSocket server for terminal streaming
try:
run_websocket_server()
logger.info("Terminal WebSocket server started on port 8765")
except Exception as e:
logger.error(f"Failed to start terminal WebSocket server: {e}")
def initialize_session():
"""Initialize a new session with fresh WorkflowUI instance."""
return WorkflowUI()
def process_file(file, verbose_print, session_state, progress=gr.Progress()):
"""Process uploaded file with step-by-step execution and progress updates."""
# Get or create session-specific UI instance
if session_state is None:
session_state = WorkflowUI()
ui = session_state
logger.info(f"š PROCESSING STARTED - File: {file.name if file else 'None'}, Verbose: {verbose_print}")
logger.info(f"š Session ID: {ui.session_id}")
# Update activity for auto-shutdown monitoring
shutdown_manager.update_activity()
if not file:
logger.warning("Missing file")
return "", "", "", gr.Column(visible=False), session_state
# Validate file (file.name contains Gradio's temp path)
logger.info(f"š VALIDATING FILE: {file.name}")
validation = ui.validate_file(file.name)
logger.info(f"ā
File validation result: {validation}")
if not validation["valid"]:
logger.error(f"ā FILE VALIDATION FAILED: {validation['error']}")
return "", "", "", gr.Column(visible=False), session_state
# Save file to our session directory
logger.info("š¾ Saving uploaded file to session directory...")
temp_path = ui.file_handler.save_uploaded_file(file, ui.session_id)
logger.info(f"ā
File saved to: {temp_path}")
logger.info(f"š File size: {validation.get('file_info', {}).get('size_mb', 'Unknown')} MB")
def create_step_html(current_step):
"""Create HTML for step progress display"""
steps = [
{"key": "extraction", "name": "Data Extraction", "icon": "š"},
{"key": "arrangement", "name": "Organization", "icon": "š"},
{"key": "code_generation", "name": "Code Generation", "icon": "š»"},
{"key": "execution", "name": "Excel Creation", "icon": "š"}
]
step_html = ''
for step in steps:
if step["key"] == current_step:
# Current step - blue with animation
step_html += f'''
{step["icon"]} {step["name"]} ā”
'''
elif any(s["key"] == step["key"] and steps.index(s) < steps.index(next(s for s in steps if s["key"] == current_step)) for s in steps):
# Completed step - green
step_html += f'''
ā
{step["name"]}
'''
else:
# Pending step - gray
step_html += f'''
{step["icon"]} {step["name"]}
'''
step_html += '
'
return f'''
š Financial Document Analysis Workflow
{step_html}
Current step: {next(s["name"] for s in steps if s["key"] == current_step)}
'''
try:
import time
from pathlib import Path
from agno.media import File
# Step 0: Initialize
progress_html = "š Initializing financial document processing..."
logger.info(f"šÆ WORKFLOW INITIALIZATION - Session: {ui.session_id}")
logger.info(f"š Document: {temp_path}")
logger.info("ā” Starting multi-step financial analysis workflow...")
yield (progress_html, create_step_html("extraction"), "", gr.Column(visible=False), session_state)
time.sleep(1) # Brief pause for UI update
# Run the complete workflow - it handles all steps internally
logger.info("=" * 60)
logger.info("š STARTING FINANCIAL WORKFLOW")
logger.info("=" * 60)
progress_html = "š Running complete financial analysis workflow..."
yield (progress_html, create_step_html("extraction"), "", gr.Column(visible=False), session_state)
logger.info(f"š Processing document: {temp_path}")
logger.info("š§ Workflow will handle: extraction ā arrangement ā code generation ā execution")
# Execute workflow with step-by-step UI updates
# Step 1: Data Extraction
progress_html = "š Step 1/4: Extracting financial data from document..."
yield (progress_html, create_step_html("extraction"), "", gr.Column(visible=False), session_state)
# Set the file path
ui.workflow.file_path = temp_path
# Run the workflow - it will execute all steps internally
# We'll show UI progression during execution
import threading
import time
# Create shared progress tracking
progress_state = {
'current_step': 1,
'step_completed': threading.Event(),
'workflow_completed': threading.Event(),
'result': [None],
'error': [None]
}
def run_workflow_with_progress():
try:
# Step 1: Data Extraction (already shown)
logger.info("Backend: Starting Step 1 - Data Extraction")
# Run the workflow and track progress
result = list(ui.workflow.run(file_path=ui.workflow.file_path))
progress_state['result'][0] = result
# Signal completion
progress_state['workflow_completed'].set()
logger.info("Backend: All steps completed")
except Exception as e:
progress_state['error'][0] = e
progress_state['workflow_completed'].set()
# Start workflow in background
workflow_thread = threading.Thread(target=run_workflow_with_progress)
workflow_thread.start()
# Monitor workflow progress by checking logs and session state
step_shown = {2: False, 3: False, 4: False}
while not progress_state['workflow_completed'].is_set():
time.sleep(2) # Check every 2 seconds
# Check if step 2 (arrangement) has started by looking at session state
if not step_shown[2] and "extracted_data" in ui.workflow.session_state:
progress_html = "š Step 2/4: Organizing and analyzing financial data..."
yield (progress_html, create_step_html("arrangement"), "", gr.Column(visible=False), session_state)
step_shown[2] = True
logger.info("UI: Advanced to step 2 (arrangement started)")
# Check if step 3 (code generation) has started
elif not step_shown[3] and "arrangement_response" in ui.workflow.session_state:
progress_html = "š» Step 3/4: Generating Python code for Excel reports..."
yield (progress_html, create_step_html("code_generation"), "", gr.Column(visible=False), session_state)
step_shown[3] = True
logger.info("UI: Advanced to step 3 (code generation started)")
# Check if step 4 (execution) has started
elif not step_shown[4] and "code_response" in ui.workflow.session_state:
progress_html = "š Step 4/4: Creating final Excel report..."
yield (progress_html, create_step_html("execution"), "", gr.Column(visible=False), session_state)
step_shown[4] = True
logger.info("UI: Advanced to step 4 (execution started)")
# Wait for thread to complete
workflow_thread.join()
# Check for errors
if progress_state['error'][0]:
raise progress_state['error'][0]
workflow_responses = progress_state['result'][0]
# Extract content from all responses and join them
workflow_results = "\n".join([response.content for response in workflow_responses])
# The workflow has completed all steps - just display the results
logger.info("š Displaying workflow results")
results_summary = workflow_results
logger.info("ā
Processing workflow completed successfully")
logger.info(f"š Results ready for session {ui.session_id}")
# Create completion HTML
final_progress_html = "ā
All steps completed successfully!"
final_steps_html = '''
ā
Workflow Completed Successfully
ā
Data Extraction
ā
Organization
ā
Code Generation
ā
Excel Creation
All steps executed successfully!
- Data Extraction: Completed
- Organization: Completed
- Code Generation: Completed
- Excel Creation: Completed
'''
logger.info("Financial document processing completed successfully")
if verbose_print:
logger.info("Final workflow response:\n" + results_summary)
# Return final results with updated session state
yield (final_progress_html, final_steps_html, results_summary, gr.Column(visible=True), session_state)
except Exception as e:
logger.error(f"Processing failed: {str(e)}", exc_info=True)
error_progress = f"ā Processing failed: {str(e)}"
error_steps = f"""
ā Processing Failed
Error: {str(e)}
Please check the file and try again. If the problem persists, check the logs for more details.
"""
error_markdown = f"# ā Processing Error\n\n**Error:** {str(e)}\n\nPlease try again or check the logs for more details."
yield (error_progress, error_steps, error_markdown, gr.Column(visible=True), session_state)
def get_terminal_with_logs(session_state):
"""Get the complete terminal HTML with real backend logs."""
try:
# Get session-specific logs if session exists
session_id = session_state.session_id if session_state else None
logs = terminal_log_handler.get_logs(session_id=session_id, limit=25)
# If no session-specific logs, get general logs
if not logs:
logs = terminal_log_handler.get_logs(session_id=None, limit=25)
log_lines = []
# Add initial messages if no logs
if not logs:
log_lines = [
f'{datetime.now().strftime("%H:%M:%S")}šÆ Terminal initialized - Monitoring backend logs
',
f'{datetime.now().strftime("%H:%M:%S")}š” Backend processing logs will appear here in real-time
',
f'{datetime.now().strftime("%H:%M:%S")}š Session ID: {session_id or "Not initialized"}
'
]
else:
for log in logs:
level_class = {
'DEBUG': 'system-line',
'INFO': 'output-line',
'WARNING': 'system-line',
'ERROR': 'error-line',
'CRITICAL': 'error-line'
}.get(log['level'], 'output-line')
# Escape HTML and preserve formatting
message = log['message'].replace('<', '<').replace('>', '>')
logger_name = log['logger'].replace('<', '<').replace('>', '>')
log_lines.append(f'{log["timestamp"]}[{log["level"]}] {logger_name}: {message}
')
# Create the complete terminal HTML
terminal_html = f"""
"""
return terminal_html
except Exception as e:
logger.error(f"Error creating terminal with logs: {e}")
return f"""
{datetime.now().strftime('%H:%M:%S')}
Error loading terminal: {str(e)}
"""
def reset_session(session_state):
"""Reset the current session."""
# Clean up old session if it exists
if session_state is not None:
try:
# Clear workflow cache and session state using the new method
if hasattr(session_state, 'workflow'):
session_state.workflow.clear_cache()
logger.info(f"Cleared workflow cache for session: {session_state.session_id}")
# Clear terminal log handler session logs
if session_state.session_id in terminal_log_handler.session_logs:
terminal_log_handler.session_logs.pop(session_state.session_id, None)
logger.info(f"Cleared terminal logs for session: {session_state.session_id}")
except Exception as e:
logger.warning(f"Error during session cleanup: {e}")
# Create completely new WorkflowUI instance
new_session = WorkflowUI()
logger.info(f"Session reset - New session ID: {new_session.session_id}")
# Clear all displays and return fresh state
return ("", "", "", None, new_session, new_session.session_id)
def update_session_display(session_state):
"""Update session display with current session ID."""
if session_state is None:
session_state = WorkflowUI()
return session_state.session_id, session_state
# Create Gradio interface
with gr.Blocks(css=custom_css, title="š Data Extractor Using Gemini") as app:
# Session state to maintain per-user data
session_state = gr.State()
# Header
gr.HTML("""
""")
# Main interface with integrated terminal (Manus AI style)
with gr.Row():
# Left side - Main processing interface
with gr.Column(scale=2):
# Configuration Panel
gr.Markdown("## āļø Configuration")
# Session info - will be updated when session initializes
session_info = gr.Textbox(
label="Session ID", value="Initializing...", interactive=False
)
# File upload
gr.Markdown("### š Upload Document")
file_input = gr.File(
label="Choose a file",
file_types=[f".{ext}" for ext in settings.SUPPORTED_FILE_TYPES],
)
# Info about automated processing
gr.Markdown("### šÆ Automated Financial Data Extraction")
gr.Markdown("This application automatically extracts financial data points from uploaded documents and generates comprehensive analysis reports. No additional input required!")
# Control buttons
with gr.Row():
process_btn = gr.Button(
"š Start Processing", variant="primary", scale=2
)
reset_btn = gr.Button("š Reset Session", scale=1)
stop_btn = gr.Button("š Stop Backend", variant="stop", scale=1)
# Processing Panel
gr.Markdown("## ā” Processing Status")
# Progress bar
progress_display = gr.HTML(label="Progress")
# Steps display
steps_display = gr.HTML(label="Processing Steps")
# Results - Hidden initially, shown when processing completes
verbose_checkbox = gr.Checkbox(label="Print model response", value=False)
# Results section
results_section = gr.Column(visible=False)
with results_section:
gr.Markdown("### š Results")
results_display = gr.Code(
label="Final Results", language="markdown", lines=10
)
# Download section
gr.Markdown("### ā¬ļø Download Processed Files")
download_btn = gr.Button("š„ Download All Files", variant="primary")
download_output = gr.File(
label="Download Files",
file_count="single",
file_types=[".zip"],
interactive=False,
visible=True
)
# Right side - Integrated Terminal Panel
with gr.Column(scale=3):
gr.Markdown("## š» Terminal")
# Terminal component with real backend logs
terminal_html = gr.HTML()
# Event handlers
process_btn.click(
fn=process_file,
inputs=[file_input, verbose_checkbox, session_state],
outputs=[progress_display, steps_display, results_display, results_section, session_state],
)
def session_download(session_state):
"""Session-aware download function."""
if session_state is None:
return None
return session_state.download_processed_files()
download_btn.click(
fn=session_download,
inputs=[session_state],
outputs=[download_output],
show_progress=True
)
reset_btn.click(
fn=reset_session,
inputs=[session_state],
outputs=[progress_display, steps_display, results_display, download_output, session_state, session_info],
)
def stop_backend():
"""Stop the backend server."""
logger.info("Backend stop requested by user")
shutdown_manager.request_shutdown()
return "š Backend shutdown initiated..."
stop_btn.click(
fn=stop_backend,
outputs=[gr.Textbox(label="Shutdown Status", visible=True)],
)
# Initialize session and terminal on load
def initialize_app():
"""Initialize app with fresh session."""
new_session = WorkflowUI()
terminal_html_content = get_terminal_with_logs(new_session)
return new_session, new_session.session_id, terminal_html_content
app.load(
fn=initialize_app,
outputs=[session_state, session_info, terminal_html],
)
# Auto-refresh timer component (hidden)
refresh_timer = gr.Timer(value=3.0, active=True) # Refresh every 3 seconds
# Timer event to auto-refresh terminal with session awareness
refresh_timer.tick(
fn=get_terminal_with_logs,
inputs=[session_state],
outputs=[terminal_html],
)
return app
def main():
"""Main application entry point."""
try:
# Validate configuration before starting
logger.info("Validating configuration...")
settings.validate_config()
logger.info("Configuration validation successful")
# Log debug info
debug_info = settings.get_debug_info()
logger.info(f"System info: Python {debug_info['python_version'].split()[0]}, {debug_info['platform']}")
logger.info(f"Temp directory: {debug_info['temp_dir']} (exists: {debug_info['temp_dir_exists']})")
logger.info(f"Models: {debug_info['models']['data_extractor']}, {debug_info['models']['data_arranger']}, {debug_info['models']['code_generator']}")
except ValueError as e:
logger.error(f"Configuration error: {e}")
print(f"\nā Configuration Error:\n{e}\n")
print("Please fix the configuration issues and try again.")
return
except Exception as e:
logger.error(f"Unexpected error during validation: {e}")
print(f"\nā Unexpected error: {e}\n")
return
try:
app = create_gradio_app()
# Start auto-shutdown monitoring
shutdown_manager.start_monitoring(app)
logger.info("Starting Gradio application with auto-shutdown enabled")
logger.info(f"Auto-shutdown timeout: {INACTIVITY_TIMEOUT_MINUTES} minutes")
logger.info("Press Ctrl+C to stop the server manually")
except Exception as e:
logger.error(f"Error creating Gradio app: {e}")
print(f"\nā Error creating application: {e}\n")
return
try:
# Launch the app
app.launch(
server_name="0.0.0.0",
server_port=7860,
share=True,
debug=False,
show_error=True,
)
except KeyboardInterrupt:
logger.info("Received keyboard interrupt, shutting down...")
shutdown_manager._shutdown_server()
except Exception as e:
logger.error(f"Error during app launch: {e}")
shutdown_manager._shutdown_server()
if __name__ == "__main__":
main()