|
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 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 |
|
|
|
|
|
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 _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() |
|
|
|
if self.app_instance: |
|
|
|
logger.info("Shutting down Gradio application") |
|
import os |
|
os._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): |
|
super().__init__() |
|
self.logs = deque(maxlen=1000) |
|
self.session_logs = {} |
|
|
|
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=500) |
|
self.session_logs[session_id].append(log_entry) |
|
|
|
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) |
|
|
|
|
|
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 tempfile |
|
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" |
|
|
|
|
|
|
|
temp_dir = tempfile.gettempdir() |
|
zip_path = Path(temp_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.absolute()) |
|
logger.info(f"Returning zip file path for download: {abs_path}") |
|
logger.info(f"File size: {zip_path.stat().st_size} bytes") |
|
|
|
|
|
os.chmod(abs_path, 0o644) |
|
|
|
|
|
|
|
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 "", "", "", None, 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 "", "", "", None, 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)) |
|
|
|
time.sleep(1) |
|
|
|
|
|
logger.info("=" * 60) |
|
logger.info("π STEP 1/4: DATA EXTRACTION PHASE") |
|
logger.info("=" * 60) |
|
logger.info("π Initializing financial data extraction agent...") |
|
progress_html = "π <strong>Step 1/4: Extracting financial data from document...</strong>" |
|
yield (progress_html, create_step_html("extraction"), "", gr.Column(visible=False)) |
|
|
|
|
|
if "extracted_data" in ui.workflow.session_state: |
|
logger.info("πΎ Using cached extraction data from previous run") |
|
logger.info("β© Skipping extraction step - data already available") |
|
time.sleep(0.5) |
|
else: |
|
logger.info(f"π Starting fresh data extraction from document: {temp_path}") |
|
logger.info("π Creating document object for analysis...") |
|
|
|
document = File(filepath=temp_path) |
|
logger.info("β
Document object created successfully") |
|
|
|
extraction_prompt = f""" |
|
Analyze this financial document and extract all relevant financial data points. |
|
|
|
Focus on: |
|
- Company identification and reporting period |
|
- Revenue, expenses, profits, and losses |
|
- Assets, liabilities, and equity |
|
- Cash flows and financial ratios |
|
- Any other key financial metrics |
|
|
|
Document path: {temp_path} |
|
""" |
|
|
|
logger.info("π€ Calling data extractor agent with financial analysis prompt") |
|
logger.info("β³ This may take 30-60 seconds depending on document complexity...") |
|
|
|
extraction_response = ui.workflow.data_extractor.run( |
|
extraction_prompt, |
|
files=[document] |
|
) |
|
extracted_data = extraction_response.content |
|
|
|
logger.info("π Data extraction agent completed successfully!") |
|
logger.info(f"π Extracted {len(extracted_data.data_points)} financial data points") |
|
|
|
|
|
ui.workflow.session_state["extracted_data"] = extracted_data.model_dump() |
|
logger.info(f"πΎ Cached extraction results for session {ui.session_id}") |
|
logger.info("β
Step 1 COMPLETED - Data extraction successful") |
|
|
|
|
|
logger.info("=" * 60) |
|
logger.info("π STEP 2/4: DATA ORGANIZATION PHASE") |
|
logger.info("=" * 60) |
|
progress_html = "π <strong>Step 2/4: Organizing and analyzing financial data...</strong>" |
|
yield (progress_html, create_step_html("arrangement"), "", gr.Column(visible=False)) |
|
|
|
if "arrangement_response" in ui.workflow.session_state: |
|
logger.info("πΎ Using cached data arrangement from previous run") |
|
logger.info("β© Skipping organization step - data already structured") |
|
time.sleep(0.5) |
|
else: |
|
logger.info("π Starting fresh data organization and analysis") |
|
|
|
extracted_data_dict = ui.workflow.session_state["extracted_data"] |
|
logger.info(f"π Retrieved {len(extracted_data_dict.get('data_points', []))} data points for organization") |
|
logger.info("ποΈ Preparing to organize data into 12 financial categories...") |
|
|
|
arrangement_prompt = f""" |
|
You are given raw, extracted financial data. Your task is to reorganize it and prepare it for Excel-based reporting. |
|
|
|
========== WHAT TO DELIVER ========== |
|
β’ A single JSON object saved as arranged_financial_data.json |
|
β’ Fields required: categories, key_metrics, insights, summary |
|
|
|
========== HOW TO ORGANIZE ========== |
|
Create 12 distinct, Excel-ready categories (one worksheet each): |
|
1. Executive Summary & Key Metrics |
|
2. Income Statement / P&L |
|
3. Balance Sheet β Assets |
|
4. Balance Sheet β Liabilities & Equity |
|
5. Cash-Flow Statement |
|
6. Financial Ratios & Analysis |
|
7. Revenue Analysis |
|
8. Expense Analysis |
|
9. Profitability Analysis |
|
10. Liquidity & Solvency |
|
11. Operational Metrics |
|
12. Risk Assessment & Notes |
|
|
|
========== STEP-BY-STEP ========== |
|
1. Map every data point into the most appropriate category above. |
|
2. Calculate or aggregate key financial metrics where possible. |
|
3. Add concise insights for trends, anomalies, or red flags. |
|
4. Write an executive summary that highlights the most important findings. |
|
5. Assemble everything into the JSON schema described under "WHAT TO DELIVER." |
|
6. Save the JSON as arranged_financial_data.json via save_file. |
|
7. Use list_files to confirm the file exists, then read_file to validate its content. |
|
8. If the file is missing or malformed, fix the issue and repeat steps 6 β 7. |
|
9. Only report success after the file passes both existence and content checks. |
|
10. Conclude with a short, plain-language summary of what was organized. |
|
|
|
Extracted Data: {json.dumps(extracted_data_dict, indent=2)} |
|
""" |
|
|
|
logger.info("Calling data arranger to organize financial data into 12 categories") |
|
arrangement_response = ui.workflow.data_arranger.run(arrangement_prompt) |
|
arrangement_content = arrangement_response.content |
|
|
|
|
|
ui.workflow.session_state["arrangement_response"] = arrangement_content |
|
logger.info("Data organization completed successfully - financial data categorized") |
|
logger.info(f"Cached arrangement results for session {ui.session_id}") |
|
|
|
|
|
logger.info("Step 3: Starting code generation...") |
|
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)) |
|
|
|
if "code_generation_response" in ui.workflow.session_state: |
|
logger.info("Using cached code generation results from previous run") |
|
code_generation_content = ui.workflow.session_state["code_generation_response"] |
|
execution_success = ui.workflow.session_state.get("execution_success", False) |
|
logger.info(f"Previous execution status: {'Success' if execution_success else 'Failed'}") |
|
time.sleep(0.5) |
|
else: |
|
logger.info("Starting fresh Python code generation for Excel report creation") |
|
code_prompt = f""" |
|
Your objective: Turn the organized JSON data into a polished, multi-sheet Excel reportβand prove that it works. |
|
|
|
========== INPUT ========== |
|
File: arranged_financial_data.json |
|
Tool to read it: read_file |
|
|
|
========== WHAT THE PYTHON SCRIPT MUST DO ========== |
|
1. Load arranged_financial_data.json and parse its contents. |
|
2. For each category in the JSON, create a dedicated worksheet using openpyxl. |
|
3. Apply professional touches: |
|
β’ Bold, centered headers |
|
β’ Appropriate number formats |
|
β’ Column-width auto-sizing |
|
β’ Borders, cell styles, and freeze panes |
|
4. Insert charts (bar, line, or pie) wherever the data lends itself to visualisation. |
|
5. Embed key metrics and summary notes prominently in the Executive Summary sheet. |
|
6. Name the workbook: Financial_Report_<YYYYMMDD_HHMMSS>.xlsx. |
|
7. Wrap every file and workbook operation in robust try/except blocks. |
|
8. Log all major steps and any exceptions for easy debugging. |
|
9. Save the script via save_to_file_and_run and execute it immediately. |
|
10. After execution, use list_files to ensure the Excel file was created. |
|
11. Optionally inspect the file (e.g., size or first bytes via read_file) to confirm it is not empty. |
|
12. If the workbook is missing or corrupted, refine the code, re-save, and re-run until success. |
|
|
|
========== OUTPUT ========== |
|
β’ A fully formatted Excel workbook in the working directory. |
|
β’ A concise summary of what ran, any issues encountered, and confirmation that the file exists and opens without error. |
|
""" |
|
|
|
logger.info("Calling code generator to create Python Excel generation script") |
|
code_response = ui.workflow.code_generator.run(code_prompt) |
|
code_generation_content = code_response.content |
|
|
|
|
|
execution_success = ( |
|
"error" not in code_generation_content.lower() or |
|
"success" in code_generation_content.lower() or |
|
"completed" in code_generation_content.lower() |
|
) |
|
|
|
|
|
ui.workflow.session_state["code_generation_response"] = code_generation_content |
|
ui.workflow.session_state["execution_success"] = execution_success |
|
|
|
logger.info(f"Code generation and execution completed: {'β
Success' if execution_success else 'β Failed'}") |
|
logger.info(f"Cached code generation results for session {ui.session_id}") |
|
|
|
|
|
logger.info("Step 4: Preparing final results...") |
|
progress_html = "π <strong>Step 4/4: Creating final Excel report...</strong>" |
|
yield (progress_html, create_step_html("execution"), "", gr.Column(visible=False)) |
|
|
|
time.sleep(1) |
|
|
|
|
|
logger.info("Scanning output directory for generated files") |
|
output_files = [] |
|
if ui.workflow.session_output_dir.exists(): |
|
output_files = [f.name for f in ui.workflow.session_output_dir.iterdir() if f.is_file()] |
|
logger.info(f"Found {len(output_files)} generated files: {', '.join(output_files)}") |
|
else: |
|
logger.warning(f"Output directory does not exist: {ui.workflow.session_output_dir}") |
|
|
|
|
|
extracted_data_dict = ui.workflow.session_state["extracted_data"] |
|
arrangement_content = ui.workflow.session_state["arrangement_response"] |
|
code_generation_content = ui.workflow.session_state["code_generation_response"] |
|
execution_success = ui.workflow.session_state.get("execution_success", False) |
|
|
|
results_summary = f""" |
|
# Financial Document Analysis Complete |
|
|
|
## Document Information |
|
- **Company**: {extracted_data_dict.get('company_name', 'Not specified') if extracted_data_dict else 'Not specified'} |
|
- **Document Type**: {extracted_data_dict.get('document_type', 'Unknown') if extracted_data_dict else 'Unknown'} |
|
- **Reporting Period**: {extracted_data_dict.get('reporting_period', 'Not specified') if extracted_data_dict else 'Not specified'} |
|
|
|
## Processing Summary |
|
- **Data Points Extracted**: {len(extracted_data_dict.get('data_points', [])) if extracted_data_dict else 0} |
|
- **Data Organization**: {'β
Completed' if arrangement_content else 'β Failed'} |
|
- **Excel Creation**: {'β
Success' if execution_success else 'β Failed'} |
|
|
|
## Data Organization Results |
|
{arrangement_content[:500] + '...' if arrangement_content and len(arrangement_content) > 500 else arrangement_content or 'No arrangement data available'} |
|
|
|
## Tool Execution Summary |
|
**Data Arranger**: Used FileTools to save organized data to JSON |
|
**Code Generator**: Used PythonTools and FileTools for Excel generation |
|
|
|
## Code Generation Results |
|
{code_generation_content[:500] + '...' if code_generation_content and len(code_generation_content) > 500 else code_generation_content or 'No code generation results available'} |
|
|
|
## Generated Files ({len(output_files)} files) |
|
{chr(10).join(f"- **{file}**" for file in output_files) if output_files else "- No files generated"} |
|
|
|
## Output Directory |
|
π `{ui.workflow.session_output_dir}` |
|
|
|
--- |
|
*Generated using Agno Workflows with step-by-step execution* |
|
*Note: Each step was executed individually with progress updates* |
|
""" |
|
|
|
|
|
ui.workflow.session_state["final_results"] = results_summary |
|
logger.info("Final results compiled and cached successfully") |
|
logger.info(f"Processing workflow completed 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' if execution_success else 'Partial') + '''</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.""" |
|
|
|
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) |
|
|
|
|
|
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 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.""" |
|
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") |
|
|
|
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() |
|
|