| | 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() |
| |
|