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!
''' 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"""
Terminal
{''.join(log_lines)}
""" 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("""
šŸ“Š Data Extractor Using Gemini
""") # 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()