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