DataGraph / app.py
rwayz's picture
Update app.py
f91737c verified
"""
AgentGraph - Aplicação principal com interface Gradio e LangGraph
"""
import asyncio
import logging
import gradio as gr
import tempfile
import os
from typing import List, Tuple, Optional, Dict
from PIL import Image
from graphs.main_graph import initialize_graph, get_graph_manager
from utils.config import (
AVAILABLE_MODELS,
REFINEMENT_MODELS,
DEFAULT_MODEL,
GRADIO_SHARE,
GRADIO_PORT,
validate_config,
is_langsmith_enabled,
LANGSMITH_PROJECT
)
from utils.object_manager import get_object_manager
# Configuração de logging
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s - %(levelname)s - %(message)s'
)
# Variáveis globais
graph_manager = None
show_history_flag = False
connection_ready = False # Controla se a conexão está pronta para uso
chat_blocked = False # Controla se o chat está bloqueado durante carregamento
async def initialize_app():
"""Inicializa a aplicação"""
global graph_manager, connection_ready
try:
# Valida configurações
validate_config()
# Inicializa o grafo
graph_manager = await initialize_graph()
# Inicializa como conectado (base padrão já carregada)
connection_ready = True
# Informa sobre o status do LangSmith
if is_langsmith_enabled():
logging.info(f"✅ LangSmith habilitado - Projeto: '{LANGSMITH_PROJECT}'")
logging.info("🔍 Traces serão enviados para LangSmith automaticamente")
else:
logging.info("ℹ️ LangSmith não configurado - Executando sem observabilidade")
logging.info("Aplicação inicializada com sucesso")
return True
except Exception as e:
logging.error(f"Erro ao inicializar aplicação: {e}")
return False
def run_async(coro):
"""Executa corrotina de forma síncrona"""
try:
loop = asyncio.get_event_loop()
except RuntimeError:
loop = asyncio.new_event_loop()
asyncio.set_event_loop(loop)
return loop.run_until_complete(coro)
def chatbot_response(user_input: str, selected_model: str, advanced_mode: bool = False, processing_enabled: bool = False, processing_model: str = "GPT-4o-mini", connection_type: str = "csv", postgresql_config: Optional[Dict] = None, selected_table: str = None, single_table_mode: bool = False) -> Tuple[str, Optional[str]]:
"""
Processa resposta do chatbot usando LangGraph
Args:
user_input: Entrada do usuário
selected_model: Modelo LLM selecionado
advanced_mode: Se deve usar refinamento avançado
processing_enabled: Se o Processing Agent está habilitado
processing_model: Modelo para o Processing Agent
connection_type: Tipo de conexão ("csv" ou "postgresql")
postgresql_config: Configuração postgresql (se aplicável)
selected_table: Tabela selecionada (para postgresql)
single_table_mode: Se deve usar apenas uma tabela (postgresql)
Returns:
Tupla com (resposta_texto, caminho_imagem_grafico)
"""
global graph_manager
if not graph_manager:
return "❌ Sistema não inicializado. Tente recarregar a página.", None
try:
# Processa query através do LangGraph
result = run_async(graph_manager.process_query(
user_input=user_input,
selected_model=selected_model,
advanced_mode=advanced_mode,
processing_enabled=processing_enabled,
processing_model=processing_model,
connection_type=connection_type,
postgresql_config=postgresql_config,
selected_table=selected_table,
single_table_mode=single_table_mode
))
response_text = result.get("response", "Erro ao processar resposta")
graph_image_path = None
# Verifica se foi gerado um gráfico
if result.get("graph_generated", False) and result.get("graph_image_id"):
graph_image_path = save_graph_image_to_temp(result["graph_image_id"])
# Adiciona informação sobre o gráfico na resposta
if graph_image_path:
graph_type = result.get("graph_type", "gráfico")
response_text += f"\n\n📊 **Gráfico gerado**: {graph_type.replace('_', ' ').title()}"
return response_text, graph_image_path
except Exception as e:
error_msg = f"Erro no chatbot: {e}"
logging.error(error_msg)
logging.error(f"Detalhes do erro: {type(e).__name__}: {str(e)}")
return error_msg, None
def save_graph_image_to_temp(graph_image_id: str) -> Optional[str]:
"""
Salva imagem do gráfico em arquivo temporário para exibição no Gradio
Args:
graph_image_id: ID da imagem no ObjectManager
Returns:
Caminho do arquivo temporário ou None se falhar
"""
try:
obj_manager = get_object_manager()
graph_image = obj_manager.get_object(graph_image_id)
if graph_image and isinstance(graph_image, Image.Image):
# Cria arquivo temporário
temp_file = tempfile.NamedTemporaryFile(delete=False, suffix='.png')
graph_image.save(temp_file.name, format='PNG')
temp_file.close()
logging.info(f"[GRADIO] Gráfico salvo em: {temp_file.name}")
return temp_file.name
except Exception as e:
logging.error(f"[GRADIO] Erro ao salvar gráfico: {e}")
return None
def handle_csv_upload(file) -> str:
"""
Processa upload de arquivo csv
Args:
file: Arquivo enviado pelo Gradio
Returns:
Mensagem de feedback
"""
global graph_manager
if not graph_manager:
return "❌ Sistema não inicializado."
if not file:
return "❌ Nenhum arquivo selecionado."
try:
# Log detalhado do arquivo recebido
logging.info(f"[UPLOAD] Arquivo recebido: {file}")
logging.info(f"[UPLOAD] Nome do arquivo: {file.name}")
logging.info(f"[UPLOAD] Tipo do arquivo: {type(file)}")
# Verifica se o arquivo existe
import os
if not os.path.exists(file.name):
return f"❌ Arquivo não encontrado: {file.name}"
# Verifica se é um arquivo csv
if not file.name.lower().endswith('.csv'):
return "❌ Por favor, selecione um arquivo csv válido."
# Verifica o tamanho do arquivo
file_size = os.path.getsize(file.name)
file_size_mb = file_size / (1024 * 1024)
file_size_gb = file_size / (1024 * 1024 * 1024)
if file_size_gb >= 1:
size_str = f"{file_size_gb:.2f} GB"
else:
size_str = f"{file_size_mb:.2f} MB"
logging.info(f"[UPLOAD] Tamanho do arquivo: {file_size} bytes ({size_str})")
if file_size == 0:
return "❌ O arquivo está vazio."
if file_size > 5 * 1024 * 1024 * 1024: # 5GB
return "❌ Arquivo muito grande. Máximo permitido: 5GB."
# Aviso para arquivos grandes
if file_size_mb > 100:
logging.info(f"[UPLOAD] Arquivo grande detectado ({size_str}). Processamento pode demorar...")
return f"⏳ Processando arquivo grande ({size_str}). Aguarde..."
# Processa upload através do LangGraph
logging.info(f"[UPLOAD] Iniciando processamento do arquivo: {file.name}")
result = run_async(graph_manager.handle_csv_upload(file.name))
logging.info(f"[UPLOAD] Resultado do processamento: {result}")
return result.get("message", "Erro no upload")
except Exception as e:
error_msg = f"❌ Erro ao processar upload: {e}"
logging.error(error_msg)
logging.error(f"[UPLOAD] Detalhes do erro: {type(e).__name__}: {str(e)}")
import traceback
logging.error(f"[UPLOAD] Traceback: {traceback.format_exc()}")
return error_msg
def reset_system() -> str:
"""
Reseta o sistema ao estado inicial
Returns:
Mensagem de feedback
"""
global graph_manager
if not graph_manager:
return "❌ Sistema não inicializado."
try:
# Reseta sistema através do LangGraph
result = run_async(graph_manager.reset_system())
return result.get("message", "Erro no reset")
except Exception as e:
error_msg = f"❌ Erro ao resetar sistema: {e}"
logging.error(error_msg)
return error_msg
def handle_postgresql_connection(host: str, port: str, database: str, username: str, password: str) -> str:
"""
Processa conexão postgresql
Args:
host: Host do postgresql
port: Porta do postgresql
database: Nome do banco
username: Nome de usuário
password: Senha
Returns:
Mensagem de feedback
"""
global graph_manager
if not graph_manager:
return "❌ Sistema não inicializado."
try:
# Valida campos obrigatórios
if not all([host, port, database, username, password]):
return "❌ Todos os campos são obrigatórios para conexão postgresql."
# Valida porta
try:
port_int = int(port)
if port_int < 1 or port_int > 65535:
return "❌ Porta deve estar entre 1 e 65535."
except ValueError:
return "❌ Porta deve ser um número válido."
# Prepara configuração postgresql
postgresql_config = {
"host": host.strip(),
"port": port_int,
"database": database.strip(),
"username": username.strip(),
"password": password
}
# Cria estado inicial para a conexão
initial_state = {
"user_input": "Conectar postgresql",
"selected_model": "gpt-4o-mini",
"advanced_mode": False,
"processing_enabled": False,
"processing_model": "gpt-4o-mini",
"connection_type": "postgresql",
"postgresql_config": postgresql_config,
"selected_table": None,
"single_table_mode": False
}
# Processa conexão através do LangGraph
logging.info(f"[POSTGRESQL] Iniciando conexão: {host}:{port}/{database}")
result = run_async(graph_manager.handle_postgresql_connection(initial_state))
logging.info(f"[POSTGRESQL] Resultado da conexão: {result}")
return result.get("message", "Erro na conexão postgresql")
except Exception as e:
error_msg = f"❌ Erro ao conectar postgresql: {e}"
logging.error(error_msg)
logging.error(f"[POSTGRESQL] Detalhes do erro: {type(e).__name__}: {str(e)}")
return error_msg
def toggle_advanced_mode(enabled: bool) -> str:
"""
Alterna modo avançado
Args:
enabled: Se deve habilitar modo avançado
Returns:
Mensagem de status
"""
global graph_manager
if not graph_manager:
return "❌ Sistema não inicializado."
return graph_manager.toggle_advanced_mode(enabled)
def toggle_history():
"""Alterna exibição do histórico"""
global show_history_flag, graph_manager
show_history_flag = not show_history_flag
if show_history_flag and graph_manager:
return graph_manager.get_history()
else:
return {}
def respond(message: str, chat_history: List[Dict[str, str]], selected_model: str, advanced_mode: bool, processing_enabled: bool = False, processing_model: str = "GPT-4o-mini", connection_type: str = "csv", postgresql_config: Optional[Dict] = None, selected_table: str = None, single_table_mode: bool = False):
"""
Função de resposta para o chatbot Gradio
Args:
message: Mensagem do usuário
chat_history: Histórico do chat (formato messages)
selected_model: Modelo selecionado
advanced_mode: Modo avançado habilitado
processing_enabled: Se o Processing Agent está habilitado
processing_model: Modelo para o Processing Agent
connection_type: Tipo de conexão ("csv" ou "postgresql")
postgresql_config: Configuração postgresql (se aplicável)
selected_table: Tabela selecionada (para postgresql)
single_table_mode: Se deve usar apenas uma tabela (postgresql)
Returns:
Tupla com (mensagem_vazia, histórico_atualizado, imagem_grafico)
"""
import logging
logging.info(f"[GRADIO RESPOND] ===== NOVA REQUISIÇÃO =====")
logging.info(f"[GRADIO RESPOND] Message: {message}")
logging.info(f"[GRADIO RESPOND] Selected model: {selected_model}")
logging.info(f"[GRADIO RESPOND] Advanced mode: {advanced_mode}")
logging.info(f"[GRADIO RESPOND] Processing enabled: {processing_enabled}")
logging.info(f"[GRADIO RESPOND] Processing model: {processing_model}")
if not message.strip():
return "", chat_history, None
# Processa resposta
response, graph_image_path = chatbot_response(message, selected_model, advanced_mode, processing_enabled, processing_model, connection_type, postgresql_config, selected_table, single_table_mode)
# Atualiza histórico no formato messages
chat_history.append({"role": "user", "content": message})
chat_history.append({"role": "assistant", "content": response})
return "", chat_history, graph_image_path
def handle_csv_and_clear_chat(file):
"""
Processa csv e limpa chat com indicador de carregamento melhorado
Args:
file: Arquivo csv
Returns:
Tupla com (feedback, chat_limpo, grafico_limpo, status)
"""
global connection_ready
if file is None:
connection_ready = False
return "", [], gr.update(visible=False), "**Status**: <span class='status-error'>Nenhum arquivo selecionado</span>"
# Indica carregamento
connection_ready = False
# Processa arquivo
feedback = handle_csv_upload(file)
# Status final baseado no resultado
if "✅" in feedback:
connection_ready = True
final_status = "**Status**: <span class='status-connected'>csv processado com sucesso</span>"
else:
connection_ready = False
final_status = "**Status**: <span class='status-error'>Erro no processamento do csv</span>"
return feedback, [], gr.update(visible=False), final_status
def is_connection_ready(conn_type, pg_host=None, pg_port=None, pg_db=None, pg_user=None, pg_pass=None):
"""
Verifica se há uma conexão de dados ativa e pronta para uso
Args:
conn_type: Tipo de conexão ("csv" ou "postgresql")
pg_host, pg_port, pg_db, pg_user, pg_pass: Credenciais postgresql
Returns:
True se conexão está pronta, False caso contrário
"""
global connection_ready, chat_blocked
return connection_ready and not chat_blocked
def show_loading_in_chat(message):
"""
Mostra mensagem de carregamento apenas no chat
Args:
message: Mensagem de carregamento
Returns:
Histórico atualizado com mensagem de carregamento
"""
global chat_blocked
chat_blocked = True
return [
{"role": "user", "content": "Alterando tipo de conexão..."},
{"role": "assistant", "content": f"🔄 {message}"}
]
def clear_loading_from_chat():
"""
Remove carregamento do chat
"""
global chat_blocked
chat_blocked = False
def load_default_csv_and_cleanup_postgresql():
"""
Carrega a base csv padrão e limpa conexões postgresql ativas
Returns:
Mensagem de feedback sobre a operação
"""
global connection_ready
try:
from utils.config import DEFAULT_CSV_PATH
from utils.object_manager import get_object_manager
import os
# Verifica se o arquivo padrão existe
if not os.path.exists(DEFAULT_CSV_PATH):
connection_ready = False
return "Arquivo csv padrão (tabela.csv) não encontrado"
# Limpa conexões postgresql ativas
obj_manager = get_object_manager()
# Fecha engines postgresql (SQLAlchemy engines têm método dispose)
for engine_id, engine in obj_manager._engines.items():
try:
if hasattr(engine, 'dispose'):
engine.dispose()
logging.info(f"[CLEANUP] Engine postgresql {engine_id} fechada")
except Exception as e:
logging.warning(f"[CLEANUP] Erro ao fechar engine {engine_id}: {e}")
# Limpa objetos postgresql do ObjectManager
obj_manager.clear_all()
logging.info("[CLEANUP] Objetos postgresql limpos do ObjectManager")
# Carrega csv padrão através do LangGraph
logging.info(f"[CSV_DEFAULT] Carregando arquivo padrão: {DEFAULT_CSV_PATH}")
result = run_async(graph_manager.handle_csv_upload(DEFAULT_CSV_PATH))
if result.get("success", False):
connection_ready = True
return f"✅ Base padrão carregada: {os.path.basename(DEFAULT_CSV_PATH)}"
else:
connection_ready = False
return f"Erro ao carregar base padrão: {result.get('message', 'Erro desconhecido')}"
except Exception as e:
connection_ready = False
error_msg = f"Erro ao carregar base padrão: {e}"
logging.error(f"[CSV_DEFAULT] {error_msg}")
return error_msg
def reset_all():
"""
Reseta tudo e limpa interface
Returns:
Tupla com (feedback, chat_limpo, arquivo_limpo, grafico_limpo)
"""
feedback = reset_system()
return feedback, [], None, gr.update(visible=False)
# Interface Gradio
def create_interface():
"""Cria interface Gradio"""
# CSS customizado para interface limpa e moderna
custom_css = """
.gradio-container {
padding: 20px 30px !important;
}
/* Seções de configuração */
.config-section {
background: #f8f9fa;
border-radius: 15px;
padding: 0;
margin: 16px 0;
overflow: hidden;
}
/* Headers dos containers com espaçamento adequado */
.gradio-container h3 {
margin: 0 !important;
color: #f1f3f4 !important;
font-size: 16px !important;
font-weight: 600 !important;
}
/* Espaçamento para status e informações nos containers */
.config-section .status-connected,
.config-section .status-loading,
.config-section .status-error,
.config-section .status-waiting {
padding: 8px 20px !important;
display: block !important;
}
.prose.svelte-lag733 {
padding: 12px 20px !important;
margin: 0 !important;
}
/* Conteúdo dos containers */
.config-content {
padding: 20px;
}
/* Status indicators melhorados */
.status-connected {
color: #28a745;
font-weight: 600;
display: inline-flex;
align-items: center;
gap: 8px;
}
.status-loading {
color: #ffc107;
font-weight: 600;
display: inline-flex;
align-items: center;
gap: 8px;
}
.status-loading::before {
content: "⏳";
animation: pulse 1.5s infinite;
}
.status-error {
color: #dc3545;
font-weight: 600;
display: inline-flex;
align-items: center;
gap: 8px;
}
.status-waiting {
color: #6c757d;
font-weight: 500;
display: inline-flex;
align-items: center;
gap: 8px;
}
/* Animação de carregamento */
@keyframes pulse {
0%, 100% { opacity: 1; }
50% { opacity: 0.5; }
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
/* Espaçamentos internos */
.gr-form {
padding: 16px;
}
.gr-box {
padding: 16px;
margin: 12px 0;
}
/* Melhorias para seção postgresql */
.pg-section {
background: #f8f9fa;
border-radius: 8px;
padding: 16px;
margin: 12px 0;
}
.pg-feedback {
padding: 12px;
margin: 8px 0;
border-radius: 6px;
background: #f1f3f4;
}
"""
with gr.Blocks(theme=gr.themes.Soft(), css=custom_css) as demo:
with gr.Row():
with gr.Column(scale=1):
gr.Markdown("## Configurações")
# 1. CONEXÃO DE DADOS
with gr.Group():
gr.Markdown("### Conexão de Dados")
with gr.Group():
connection_type = gr.Radio(
choices=[("CSV", "csv"), ("PostgreSQL", "postgresql")],
value="csv",
label="Tipo de Conexão"
)
# Status da conexão
connection_status = gr.Markdown("**Status**: <span class='status-connected'>Base padrão carregada</span>")
# Seção csv
with gr.Group(visible=True) as csv_section:
csv_file = gr.File(
file_types=[".csv"],
label="Arquivo csv"
)
upload_feedback = gr.Markdown()
# Seção postgresql
with gr.Group(visible=False) as postgresql_section:
with gr.Group():
with gr.Row():
pg_host = gr.Textbox(
label="Host",
placeholder="localhost",
scale=2
)
pg_port = gr.Textbox(
label="Porta",
value="5432",
placeholder="5432",
scale=1
)
pg_database = gr.Textbox(
label="Banco de Dados",
placeholder="nome_do_banco"
)
with gr.Row():
pg_username = gr.Textbox(
label="Usuário",
placeholder="usuario",
scale=1
)
pg_password = gr.Textbox(
label="Senha",
type="password",
placeholder="senha",
scale=1
)
pg_connect_btn = gr.Button(
"Conectar postgresql",
variant="primary",
size="lg"
)
pg_feedback = gr.Markdown()
# Configuração de tabelas (visível após conexão)
with gr.Group(visible=False) as pg_table_section:
gr.Markdown("#### Configuração de Tabelas")
with gr.Group():
pg_single_table_mode = gr.Checkbox(
label="Modo Tabela Única",
value=False
)
# Seletor de tabela
with gr.Group(visible=False) as pg_table_selector_group:
pg_table_selector = gr.Dropdown(
choices=[],
label="Selecionar Tabela",
interactive=True
)
pg_table_info = gr.Markdown()
# 2. CONFIGURAÇÃO DE MODELOS
with gr.Group():
gr.Markdown("### Configuração de Agentes")
with gr.Group():
# Processing Agent
processing_checkbox = gr.Checkbox(
label="Processing Agent",
value=False
)
processing_model_selector = gr.Dropdown(
choices=list(AVAILABLE_MODELS.keys()) + list(REFINEMENT_MODELS.keys()),
value="GPT-4o-mini",
label="Modelo do Processing Agent",
visible=False
)
# Modelo principal SQL
model_selector = gr.Dropdown(
list(AVAILABLE_MODELS.keys()),
value=DEFAULT_MODEL,
label="Modelo SQL Principal"
)
# 3. CONFIGURAÇÕES AVANÇADAS
with gr.Group():
gr.Markdown("### Configurações Avançadas")
with gr.Group():
advanced_checkbox = gr.Checkbox(
label="Refinar Resposta"
)
# 4. STATUS E CONTROLES
with gr.Group():
gr.Markdown("### Status do Sistema")
with gr.Group():
# Status do LangSmith
if is_langsmith_enabled():
gr.Markdown(f"**LangSmith**: Ativo")
else:
gr.Markdown("**LangSmith**: Desabilitado")
reset_btn = gr.Button(
"Resetar Sistema",
variant="secondary"
)
with gr.Column(scale=4):
gr.Markdown("## Agente Seara")
chatbot = gr.Chatbot(
height=600,
show_label=False,
container=True,
type="messages"
)
msg = gr.Textbox(placeholder="Digite sua pergunta aqui...", lines=1, label="")
btn = gr.Button("Enviar", variant="primary")
history_btn = gr.Button("Histórico", variant="secondary")
history_output = gr.JSON()
# Componente para exibir gráficos - posicionado após histórico
graph_image = gr.Image(
label="📊 Visualização de Dados",
visible=False,
height=500, # Altura maior para ocupar mais espaço
show_label=True,
container=True,
interactive=False,
show_download_button=True
)
download_file = gr.File(visible=False)
# Função para mostrar carregamento de transição no chat
def show_transition_loading(conn_type):
"""Mostra carregamento de transição apenas no chat"""
if conn_type == "csv":
loading_chat = show_loading_in_chat("Fechando postgresql e carregando base csv padrão...")
return "", loading_chat, gr.update(visible=False)
else:
return "", [], gr.update(visible=False)
# Event handlers (usando as funções originais do sistema)
def handle_response_with_graph(message, chat_history, model, advanced, processing_enabled, processing_model, conn_type, pg_host, pg_port, pg_db, pg_user, pg_pass, pg_table, pg_single_mode):
"""Wrapper para lidar com resposta e gráfico"""
# Verifica se há conexão ativa antes de processar
if not is_connection_ready(conn_type, pg_host, pg_port, pg_db, pg_user, pg_pass):
error_msg = "⚠️ **Aguarde**: Configure e conecte a uma fonte de dados antes de fazer perguntas."
chat_history.append({"role": "user", "content": message})
chat_history.append({"role": "assistant", "content": error_msg})
return "", chat_history, gr.update(visible=False)
# Prepara configuração postgresql se necessário
postgresql_config = None
if conn_type == "postgresql":
postgresql_config = {
"host": pg_host,
"port": pg_port,
"database": pg_db,
"username": pg_user,
"password": pg_pass
}
empty_msg, updated_history, graph_path = respond(message, chat_history, model, advanced, processing_enabled, processing_model, conn_type, postgresql_config, pg_table, pg_single_mode)
# Controla visibilidade do componente de gráfico
if graph_path:
return empty_msg, updated_history, gr.update(value=graph_path, visible=True)
else:
return empty_msg, updated_history, gr.update(visible=False)
def toggle_processing_agent(enabled):
"""Controla visibilidade do seletor de modelo do Processing Agent"""
return gr.update(visible=enabled)
def toggle_connection_type(conn_type):
"""Controla visibilidade das seções de conexão - FECHA POSTGRES IMEDIATAMENTE"""
global connection_ready
if conn_type == "csv":
# PRIMEIRO: Fecha container postgresql imediatamente
# SEGUNDO: Executa transição em background
feedback_msg = load_default_csv_and_cleanup_postgresql()
if "✅" in feedback_msg:
connection_ready = True
status_msg = "**Status**: <span class='status-connected'>Base padrão carregada</span>"
else:
connection_ready = False
status_msg = "**Status**: <span class='status-error'>Erro na conexão</span>"
return (
gr.update(visible=True), # csv_section - MOSTRA IMEDIATAMENTE
gr.update(visible=False), # postgresql_section - FECHA IMEDIATAMENTE
feedback_msg, # upload_feedback
status_msg, # connection_status
# Limpa campos postgresql IMEDIATAMENTE
gr.update(value=""), # pg_host
gr.update(value="5432"), # pg_port
gr.update(value=""), # pg_database
gr.update(value=""), # pg_username
gr.update(value=""), # pg_password
gr.update(value=""), # pg_feedback
gr.update(visible=False), # pg_table_section
gr.update(value=False), # pg_single_table_mode
gr.update(visible=False), # pg_table_selector_group
gr.update(choices=[], value=None), # pg_table_selector
gr.update(value="") # pg_table_info
)
else: # postgresql
connection_ready = False
status_msg = "**Status**: <span class='status-waiting'>Aguardando configuração postgresql</span>"
return (
gr.update(visible=False), # csv_section
gr.update(visible=True), # postgresql_section
"", # upload_feedback
status_msg, # connection_status
# Mantém campos postgresql como estão
gr.update(), # pg_host
gr.update(), # pg_port
gr.update(), # pg_database
gr.update(), # pg_username
gr.update(), # pg_password
gr.update(), # pg_feedback
gr.update(), # pg_table_section
gr.update(), # pg_single_table_mode
gr.update(), # pg_table_selector_group
gr.update(), # pg_table_selector
gr.update() # pg_table_info
)
def handle_postgresql_connect(host, port, database, username, password):
"""Wrapper para conexão postgresql"""
global connection_ready
# Executa conexão
connection_ready = False
result = handle_postgresql_connection(host, port, database, username, password)
# Se conexão foi bem-sucedida, retorna tabelas disponíveis
if "✅" in result:
connection_ready = True
try:
# Obtém tabelas do ObjectManager
from utils.object_manager import get_object_manager
obj_manager = get_object_manager()
# Busca metadados de conexão mais recente
all_metadata = obj_manager.get_all_connection_metadata()
if all_metadata:
latest_metadata = list(all_metadata.values())[-1]
tables = latest_metadata.get("tables", [])
# Status de sucesso
success_status = "**Status**: <span class='status-connected'>postgresql conectado com sucesso</span>"
table_info = f"**Modo Multi-Tabela ativo** - {len(tables)} tabelas disponíveis"
# Retorna resultado + atualização do seletor
return (
f"✅ **Conectado com sucesso!** {len(tables)} tabelas encontradas", # feedback
gr.update(visible=True), # pg_table_section
False, # pg_single_table_mode (padrão desativado)
gr.update(visible=False), # pg_table_selector_group (oculto por padrão)
gr.update(choices=tables, value=tables[0] if tables else None), # pg_table_selector
table_info, # pg_table_info
success_status # connection_status
)
except Exception as e:
logging.error(f"Erro ao obter tabelas: {e}")
# Se falhou, mantém seção de tabela oculta
connection_ready = False
error_status = "**Status**: <span class='status-error'>Falha na conexão postgresql</span>"
return (
result, # feedback
gr.update(visible=False), # pg_table_section
False, # pg_single_table_mode
gr.update(visible=False), # pg_table_selector_group
gr.update(choices=[], value=None), # pg_table_selector
"", # pg_table_info
error_status # connection_status
)
def toggle_table_mode(single_mode_enabled, current_table):
"""Alterna entre modo multi-tabela e tabela única"""
if single_mode_enabled:
# Modo tabela única ativado
return (
gr.update(visible=True), # pg_table_selector_group
f"**Modo Tabela Única ativo** - Usando: {current_table or 'Selecione uma tabela'}"
)
else:
# Modo multi-tabela ativado
return (
gr.update(visible=False), # pg_table_selector_group
"**Modo Multi-Tabela ativo** - Pode usar todas as tabelas e fazer JOINs"
)
msg.submit(
handle_response_with_graph,
inputs=[msg, chatbot, model_selector, advanced_checkbox, processing_checkbox, processing_model_selector, connection_type, pg_host, pg_port, pg_database, pg_username, pg_password, pg_table_selector, pg_single_table_mode],
outputs=[msg, chatbot, graph_image],
show_progress=True # Mostra carregamento no input do chat
)
btn.click(
handle_response_with_graph,
inputs=[msg, chatbot, model_selector, advanced_checkbox, processing_checkbox, processing_model_selector, connection_type, pg_host, pg_port, pg_database, pg_username, pg_password, pg_table_selector, pg_single_table_mode],
outputs=[msg, chatbot, graph_image]
)
csv_file.change(
handle_csv_and_clear_chat,
inputs=csv_file,
outputs=[upload_feedback, chatbot, graph_image, connection_status],
show_progress="minimal" # Mostra carregamento mínimo
)
reset_btn.click(
reset_all,
outputs=[upload_feedback, chatbot, csv_file, graph_image]
)
advanced_checkbox.change(
toggle_advanced_mode,
inputs=advanced_checkbox,
outputs=[]
)
history_btn.click(
toggle_history,
outputs=history_output
)
processing_checkbox.change(
toggle_processing_agent,
inputs=processing_checkbox,
outputs=processing_model_selector
)
# Executa toggle imediatamente (sem carregamento nos campos)
connection_type.change(
toggle_connection_type,
inputs=connection_type,
outputs=[
csv_section, postgresql_section, upload_feedback, connection_status,
pg_host, pg_port, pg_database, pg_username, pg_password, pg_feedback,
pg_table_section, pg_single_table_mode, pg_table_selector_group,
pg_table_selector, pg_table_info
],
show_progress=False # Não mostra carregamento nos campos
)
pg_connect_btn.click(
handle_postgresql_connect,
inputs=[pg_host, pg_port, pg_database, pg_username, pg_password],
outputs=[pg_feedback, pg_table_section, pg_single_table_mode, pg_table_selector_group, pg_table_selector, pg_table_info, connection_status],
show_progress="minimal" # Mostra carregamento mínimo
)
# Event handler para toggle de modo de tabela
pg_single_table_mode.change(
toggle_table_mode,
inputs=[pg_single_table_mode, pg_table_selector],
outputs=[pg_table_selector_group, pg_table_info]
)
return demo
async def main():
"""Função principal"""
# Inicializa aplicação
success = await initialize_app()
if not success:
logging.error("Falha na inicialização. Encerrando aplicação.")
return
# Cria e lança interface
demo = create_interface()
# Tenta diferentes portas se a padrão estiver ocupada
ports_to_try = [GRADIO_PORT, 7861, 7862, 7863, 7864, 0] # 0 = porta automática
for port in ports_to_try:
try:
logging.info(f"Tentando iniciar interface Gradio na porta {port}")
# Configurações para Docker
server_name = "0.0.0.0" if GRADIO_SHARE else "127.0.0.1"
if GRADIO_SHARE:
logging.info("🌐 Configurando link público do Gradio...")
demo.launch()
break # Se chegou aqui, deu certo
except OSError as e:
if "Cannot find empty port" in str(e) and port != ports_to_try[-1]:
logging.warning(f"Porta {port} ocupada, tentando próxima...")
continue
else:
logging.error(f"Erro ao iniciar servidor: {e}")
raise
except Exception as e:
logging.error(f"Erro inesperado ao iniciar interface: {e}")
raise
if __name__ == "__main__":
run_async(main())