| import gradio as gr |
| import asyncio |
| import json |
| import time |
| import os |
| |
| 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 |
|
|
| |
| |
| import tempfile |
| import os |
|
|
| try: |
| |
| 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): |
| |
| logging.basicConfig( |
| level=logging.INFO, |
| format="%(asctime)s - %(name)s - %(levelname)s - %(message)s", |
| handlers=[logging.StreamHandler()], |
| ) |
|
|
| |
| 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__) |
|
|
| |
| INACTIVITY_TIMEOUT_MINUTES = 30 |
| CHECK_INTERVAL_SECONDS = 60 |
|
|
| 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 |
| |
| |
| signal.signal(signal.SIGINT, self._signal_handler) |
| signal.signal(signal.SIGTERM, self._signal_handler) |
| |
| |
| 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: |
| |
| 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() |
| |
| |
| try: |
| |
| 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: |
| |
| 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}") |
| |
| |
| import time |
| time.sleep(1) |
| |
| |
| 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) |
|
|
| |
| shutdown_manager = AutoShutdownManager() |
|
|
| |
| 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) |
| self.session_logs = {} |
| self.max_session_logs = max_session_logs |
| self.cleanup_counter = 0 |
| |
| def emit(self, record): |
| """Emit a log record.""" |
| try: |
| |
| if record.levelname in ['DEBUG'] and record.name in ['httpcore', 'urllib3', 'requests']: |
| return |
| |
| |
| message = record.getMessage() |
| |
| |
| 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', '') |
| } |
| |
| |
| self.logs.append(log_entry) |
| |
| |
| 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) |
| |
| |
| self.cleanup_counter += 1 |
| if self.cleanup_counter % 100 == 0: |
| self.cleanup_old_sessions() |
| |
| except Exception as e: |
| |
| 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''' |
| <div class="terminal-line {level_class}"> |
| <span class="timestamp">{log['timestamp']}</span> |
| <span>[{log['level']}] {log['logger']}: {log['message']}</span> |
| </div> |
| ''') |
| |
| 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: |
| |
| sessions_by_activity = [] |
| current_time = datetime.now() |
| |
| for session_id, logs in self.session_logs.items(): |
| if logs: |
| |
| last_log_time = logs[-1].get('timestamp', '00:00:00') |
| try: |
| |
| 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: |
| |
| sessions_by_activity.append((session_id, current_time - timedelta(hours=24))) |
| else: |
| |
| sessions_by_activity.append((session_id, current_time - timedelta(hours=24))) |
| |
| |
| sessions_by_activity.sort(key=lambda x: x[1], reverse=True) |
| |
| |
| sessions_to_keep = set(session_id for session_id, _ in sessions_by_activity[:max_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 |
| } |
|
|
| |
| terminal_log_handler = TerminalLogHandler() |
|
|
| |
| terminal_log_handler.setLevel(logging.DEBUG) |
|
|
| |
| root_logger = logging.getLogger() |
| root_logger.addHandler(terminal_log_handler) |
| root_logger.setLevel(logging.DEBUG) |
|
|
| |
| 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) |
|
|
| |
| 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) |
|
|
| |
| 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 |
|
|
| |
| prompt_gallery = PromptGallery() |
|
|
| |
| 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] |
| |
| |
| 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 |
|
|
| |
| 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: |
| 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: |
| 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.""" |
| |
| shutdown_manager.update_activity() |
| |
| try: |
| import zipfile |
| import os |
| import shutil |
| from datetime import datetime |
| |
| |
| 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 |
| |
| |
| timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") |
| zip_filename = f"processed_files_{self.session_id}_{timestamp}.zip" |
| |
| |
| |
| downloads_dir = Path(settings.TEMP_DIR) / "downloads" |
| downloads_dir.mkdir(parents=True, exist_ok=True) |
| |
| |
| try: |
| import time |
| current_time = time.time() |
| for old_file in downloads_dir.glob("*.zip"): |
| if current_time - old_file.stat().st_mtime > 3600: |
| 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: |
| |
| file_count = 0 |
| for file_path in session_output_dir.rglob('*'): |
| if file_path.is_file(): |
| |
| 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") |
| |
| 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}") |
| |
| if zip_path.exists(): |
| zip_path.unlink() |
| return None |
| |
| logger.info(f"Created zip file with {file_count} files: {zip_path}") |
| |
| |
| if zip_path.exists() and zip_path.stat().st_size > 0: |
| |
| 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") |
| |
| |
| try: |
| os.chmod(abs_path, 0o644) |
| except (OSError, PermissionError) as e: |
| logger.warning(f"Could not set file permissions: {e}") |
| |
| |
| |
| 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.""" |
| |
| |
| 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.""" |
| |
| 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}") |
| |
| |
| shutdown_manager.update_activity() |
|
|
| if not file: |
| logger.warning("Missing file") |
| return "", "", "", gr.Column(visible=False), session_state |
|
|
| |
| 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 |
|
|
| |
| 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 = '<div style="display: flex; gap: 10px; margin-top: 15px;">' |
| |
| for step in steps: |
| if step["key"] == current_step: |
| |
| step_html += f''' |
| <div style="padding: 10px; border-radius: 6px; background: rgba(59, 130, 246, 0.2); border: 2px solid #3b82f6; position: relative; overflow: hidden;"> |
| <div style="position: absolute; top: 0; left: -100%; width: 100%; height: 100%; background: linear-gradient(90deg, transparent, rgba(255,255,255,0.3), transparent); animation: shimmer 2s infinite;"></div> |
| {step["icon"]} {step["name"]} ⚡ |
| </div> |
| ''' |
| 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): |
| |
| step_html += f''' |
| <div style="padding: 10px; border-radius: 6px; background: rgba(16, 185, 129, 0.1); border: 1px solid #10b981;"> |
| ✅ {step["name"]} |
| </div> |
| ''' |
| else: |
| |
| step_html += f''' |
| <div style="padding: 10px; border-radius: 6px; background: rgba(107, 114, 128, 0.1); border: 1px solid #6b7280;"> |
| {step["icon"]} {step["name"]} |
| </div> |
| ''' |
| |
| step_html += '</div>' |
| |
| return f''' |
| <div style="padding: 20px; background: var(--background-fill-secondary); border-radius: 8px;"> |
| <h3>📊 Financial Document Analysis Workflow</h3> |
| {step_html} |
| <p style="margin-top: 15px; color: var(--body-text-color-subdued);"> |
| Current step: <strong>{next(s["name"] for s in steps if s["key"] == current_step)}</strong> |
| </p> |
| <style> |
| @keyframes shimmer {{ |
| 0% {{ transform: translateX(-100%); }} |
| 100% {{ transform: translateX(200%); }} |
| }} |
| </style> |
| </div> |
| ''' |
|
|
| try: |
| import time |
| from pathlib import Path |
| from agno.media import File |
| |
| |
| progress_html = "🚀 <strong>Initializing financial document processing...</strong>" |
| 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) |
| |
| |
| logger.info("=" * 60) |
| logger.info("🚀 STARTING FINANCIAL WORKFLOW") |
| logger.info("=" * 60) |
| progress_html = "🚀 <strong>Running complete financial analysis workflow...</strong>" |
| 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") |
| |
| |
| |
| |
| progress_html = "🔍 <strong>Step 1/4: Extracting financial data from document...</strong>" |
| yield (progress_html, create_step_html("extraction"), "", gr.Column(visible=False), session_state) |
| |
| |
| ui.workflow.file_path = temp_path |
| |
| |
| |
| import threading |
| import time |
| |
| |
| progress_state = { |
| 'current_step': 1, |
| 'step_completed': threading.Event(), |
| 'workflow_completed': threading.Event(), |
| 'result': [None], |
| 'error': [None] |
| } |
| |
| def run_workflow_with_progress(): |
| try: |
| |
| logger.info("Backend: Starting Step 1 - Data Extraction") |
| |
| |
| result = list(ui.workflow.run(file_path=ui.workflow.file_path)) |
| progress_state['result'][0] = result |
| |
| |
| 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() |
| |
| |
| workflow_thread = threading.Thread(target=run_workflow_with_progress) |
| workflow_thread.start() |
| |
| |
| step_shown = {2: False, 3: False, 4: False} |
| |
| while not progress_state['workflow_completed'].is_set(): |
| time.sleep(2) |
| |
| |
| if not step_shown[2] and "extracted_data" in ui.workflow.session_state: |
| progress_html = "📊 <strong>Step 2/4: Organizing and analyzing financial data...</strong>" |
| 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)") |
| |
| |
| elif not step_shown[3] and "arrangement_response" in ui.workflow.session_state: |
| progress_html = "💻 <strong>Step 3/4: Generating Python code for Excel reports...</strong>" |
| 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)") |
| |
| |
| elif not step_shown[4] and "code_response" in ui.workflow.session_state: |
| progress_html = "📊 <strong>Step 4/4: Creating final Excel report...</strong>" |
| 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)") |
| |
| |
| workflow_thread.join() |
| |
| |
| if progress_state['error'][0]: |
| raise progress_state['error'][0] |
| |
| workflow_responses = progress_state['result'][0] |
| |
| workflow_results = "\n".join([response.content for response in workflow_responses]) |
| |
| |
| 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}") |
| |
| |
| final_progress_html = "✅ <strong>All steps completed successfully!</strong>" |
| final_steps_html = ''' |
| <div style="padding: 20px; background: var(--background-fill-secondary); border-radius: 8px;"> |
| <h3>✅ Workflow Completed Successfully</h3> |
| <div style="display: flex; gap: 10px; margin-top: 15px;"> |
| <div style="padding: 10px; border-radius: 6px; background: rgba(16, 185, 129, 0.1); border: 1px solid #10b981;"> |
| ✅ Data Extraction |
| </div> |
| <div style="padding: 10px; border-radius: 6px; background: rgba(16, 185, 129, 0.1); border: 1px solid #10b981;"> |
| ✅ Organization |
| </div> |
| <div style="padding: 10px; border-radius: 6px; background: rgba(16, 185, 129, 0.1); border: 1px solid #10b981;"> |
| ✅ Code Generation |
| </div> |
| <div style="padding: 10px; border-radius: 6px; background: rgba(16, 185, 129, 0.1); border: 1px solid #10b981;"> |
| ✅ Excel Creation |
| </div> |
| </div> |
| <div style="margin-top: 15px; padding: 10px; background: rgba(16, 185, 129, 0.05); border-radius: 4px;"> |
| <strong>All steps executed successfully!</strong> |
| <ul style="margin: 5px 0;"> |
| <li><strong>Data Extraction:</strong> Completed</li> |
| <li><strong>Organization:</strong> Completed</li> |
| <li><strong>Code Generation:</strong> Completed</li> |
| <li><strong>Excel Creation:</strong> Completed</li> |
| </ul> |
| </div> |
| </div> |
| ''' |
| |
| logger.info("Financial document processing completed successfully") |
| if verbose_print: |
| logger.info("Final workflow response:\n" + results_summary) |
| |
| |
| 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"❌ <strong>Processing failed: {str(e)}</strong>" |
| error_steps = f""" |
| <div style="padding: 20px; background: rgba(239, 68, 68, 0.1); border: 1px solid #ef4444; border-radius: 8px;"> |
| <h3>❌ Processing Failed</h3> |
| <p><strong>Error:</strong> {str(e)}</p> |
| <p>Please check the file and try again. If the problem persists, check the logs for more details.</p> |
| </div> |
| """ |
| 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: |
| |
| session_id = session_state.session_id if session_state else None |
| logs = terminal_log_handler.get_logs(session_id=session_id, limit=25) |
| |
| |
| if not logs: |
| logs = terminal_log_handler.get_logs(session_id=None, limit=25) |
| |
| log_lines = [] |
| |
| |
| if not logs: |
| log_lines = [ |
| f'<div class="terminal-line system-line"><span class="timestamp">{datetime.now().strftime("%H:%M:%S")}</span><span>🎯 Terminal initialized - Monitoring backend logs</span></div>', |
| f'<div class="terminal-line system-line"><span class="timestamp">{datetime.now().strftime("%H:%M:%S")}</span><span>💡 Backend processing logs will appear here in real-time</span></div>', |
| f'<div class="terminal-line system-line"><span class="timestamp">{datetime.now().strftime("%H:%M:%S")}</span><span>📚 Session ID: {session_id or "Not initialized"}</span></div>' |
| ] |
| 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') |
| |
| |
| message = log['message'].replace('<', '<').replace('>', '>') |
| logger_name = log['logger'].replace('<', '<').replace('>', '>') |
| |
| log_lines.append(f'<div class="terminal-line {level_class}"><span class="timestamp">{log["timestamp"]}</span><span>[{log["level"]}] {logger_name}: {message}</span></div>') |
| |
| |
| terminal_html = f""" |
| <div class="terminal-container"> |
| <div class="terminal-header"> |
| <div class="terminal-title"> |
| <div class="terminal-icon"></div> |
| <span>Terminal</span> |
| </div> |
| <div class="terminal-controls"> |
| <button class="control-btn close" onclick="clearTerminal()"></button> |
| <button class="control-btn minimize" onclick="minimizeTerminal()"></button> |
| <button class="control-btn maximize" onclick="maximizeTerminal()"></button> |
| </div> |
| </div> |
| |
| <div class="terminal-body"> |
| <div class="terminal-output" id="terminalOutput"> |
| {''.join(log_lines)} |
| </div> |
| </div> |
| </div> |
| |
| <script> |
| // Simple read-only terminal for backend log display |
| class LogTerminal {{ |
| constructor() {{ |
| this.output = document.getElementById('terminalOutput'); |
| this.autoScroll = true; |
| this.userScrolled = false; |
| |
| this.init(); |
| }} |
| |
| init() {{ |
| // Add scroll event listener to detect manual scrolling |
| if (this.output) {{ |
| this.output.addEventListener('scroll', (e) => this.handleScroll(e)); |
| }} |
| |
| this.scrollToBottom(); |
| }} |
| |
| handleScroll(e) {{ |
| const element = e.target; |
| const isScrolledToBottom = element.scrollHeight - element.clientHeight <= element.scrollTop + 1; |
| |
| // If user scrolled away from bottom, disable auto-scroll |
| if (!isScrolledToBottom && this.autoScroll) {{ |
| this.userScrolled = true; |
| this.autoScroll = false; |
| }} else if (isScrolledToBottom && !this.autoScroll) {{ |
| // If user scrolled back to bottom, re-enable auto-scroll |
| this.userScrolled = false; |
| this.autoScroll = true; |
| }} |
| }} |
| |
| scrollToBottom() {{ |
| if (this.output && this.autoScroll) {{ |
| this.output.scrollTop = this.output.scrollHeight; |
| }} |
| }} |
| |
| clear() {{ |
| if (this.output) {{ |
| this.output.innerHTML = ''; |
| this.autoScroll = true; |
| this.userScrolled = false; |
| }} |
| }} |
| }} |
| |
| // Initialize terminal with auto-scroll preservation |
| function initTerminal() {{ |
| if (window.logTerminal) {{ |
| // Preserve scroll state if terminal exists |
| window.logTerminal.init(); |
| }} else {{ |
| window.logTerminal = new LogTerminal(); |
| }} |
| |
| // Enable auto-scroll for new content |
| if (window.logTerminal && window.logTerminal.autoScroll) {{ |
| setTimeout(() => {{ |
| window.logTerminal.scrollToBottom(); |
| }}, 100); |
| }} |
| }} |
| |
| // Initialize immediately and on DOM changes |
| initTerminal(); |
| |
| // Reinitialize when terminal content updates |
| setTimeout(initTerminal, 200); |
| |
| // Terminal control functions |
| function clearTerminal() {{ |
| if (window.logTerminal) {{ |
| window.logTerminal.clear(); |
| }} |
| }} |
| |
| function minimizeTerminal() {{ |
| console.log('Minimize terminal'); |
| }} |
| |
| function maximizeTerminal() {{ |
| console.log('Maximize terminal'); |
| }} |
| </script> |
| """ |
| |
| return terminal_html |
| |
| except Exception as e: |
| logger.error(f"Error creating terminal with logs: {e}") |
| return f""" |
| <div class="terminal-container"> |
| <div class="terminal-line error-line"> |
| <span class="timestamp">{datetime.now().strftime('%H:%M:%S')}</span> |
| <span>Error loading terminal: {str(e)}</span> |
| </div> |
| </div> |
| """ |
|
|
| def reset_session(session_state): |
| """Reset the current session.""" |
| |
| if session_state is not None: |
| try: |
| |
| if hasattr(session_state, 'workflow'): |
| session_state.workflow.clear_cache() |
| logger.info(f"Cleared workflow cache for session: {session_state.session_id}") |
| |
| |
| 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}") |
| |
| |
| new_session = WorkflowUI() |
| logger.info(f"Session reset - New session ID: {new_session.session_id}") |
| |
| |
| 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 |
|
|
| |
| with gr.Blocks(css=custom_css, title="📊 Data Extractor Using Gemini") as app: |
| |
| session_state = gr.State() |
| |
| |
| gr.HTML(""" |
| <div class="header-title"> |
| 📊 Data Extractor Using Gemini |
| </div> |
| """) |
| |
| |
| with gr.Row(): |
| |
| with gr.Column(scale=2): |
| |
| gr.Markdown("## ⚙️ Configuration") |
|
|
| |
| session_info = gr.Textbox( |
| label="Session ID", value="Initializing...", interactive=False |
| ) |
|
|
| |
| gr.Markdown("### 📄 Upload Document") |
| file_input = gr.File( |
| label="Choose a file", |
| file_types=[f".{ext}" for ext in settings.SUPPORTED_FILE_TYPES], |
| ) |
| |
|
|
| |
| 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!") |
|
|
| |
| 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) |
|
|
| |
| gr.Markdown("## ⚡ Processing Status") |
|
|
| |
| progress_display = gr.HTML(label="Progress") |
|
|
| |
| steps_display = gr.HTML(label="Processing Steps") |
|
|
| |
| verbose_checkbox = gr.Checkbox(label="Print model response", value=False) |
| |
| |
| results_section = gr.Column(visible=False) |
| with results_section: |
| gr.Markdown("### 📊 Results") |
| results_display = gr.Code( |
| label="Final Results", language="markdown", lines=10 |
| ) |
| |
| |
| 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 |
| ) |
| |
| |
| with gr.Column(scale=3): |
| gr.Markdown("## 💻 Terminal") |
| |
| |
| terminal_html = gr.HTML() |
| |
|
|
| |
| 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)], |
| ) |
| |
| |
| |
| 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], |
| ) |
| |
| |
| refresh_timer = gr.Timer(value=3.0, active=True) |
| |
| |
| refresh_timer.tick( |
| fn=get_terminal_with_logs, |
| inputs=[session_state], |
| outputs=[terminal_html], |
| ) |
|
|
| return app |
|
|
|
|
| def main(): |
| """Main application entry point.""" |
| try: |
| |
| logger.info("Validating configuration...") |
| settings.validate_config() |
| logger.info("Configuration validation successful") |
| |
| |
| 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() |
| |
| |
| 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: |
| |
| 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() |
|
|