nextmarte commited on
Commit
845e2dd
·
1 Parent(s): dd273c0
Files changed (6) hide show
  1. .gitignore +16 -0
  2. .python-version +1 -0
  3. main.py +1143 -0
  4. pyproject.toml +21 -0
  5. requirements.txt +12 -0
  6. uv.lock +0 -0
.gitignore ADDED
@@ -0,0 +1,16 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Python-generated files
2
+ __pycache__/
3
+ *.py[oc]
4
+ build/
5
+ dist/
6
+ wheels/
7
+ *.egg-info
8
+
9
+ # Virtual environments
10
+ .venv
11
+ .env
12
+
13
+ sources/
14
+ .github/copilot-instructions.md
15
+
16
+ .github/
.python-version ADDED
@@ -0,0 +1 @@
 
 
1
+ 3.12
main.py ADDED
@@ -0,0 +1,1143 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import gradio as gr
2
+ from sqlalchemy import create_engine, text
3
+ from langchain_community.utilities import SQLDatabase
4
+ from langchain_openai import ChatOpenAI
5
+ from langchain.memory import ConversationBufferMemory
6
+ from langchain_community.agent_toolkits import create_sql_agent
7
+ import os
8
+ import re
9
+ import pandas as pd
10
+ import plotly.express as px
11
+ import json
12
+ from io import StringIO
13
+
14
+ import dotenv
15
+ dotenv.load_dotenv()
16
+
17
+ # Importações adicionais para outros LLMs
18
+ try:
19
+ from langchain_anthropic import ChatAnthropic
20
+ except ImportError:
21
+ ChatAnthropic = None # Define como None se não instalado
22
+ try:
23
+ from langchain_google_genai import ChatGoogleGenerativeAI
24
+ except ImportError:
25
+ ChatGoogleGenerativeAI = None # Define como None se não instalado
26
+
27
+
28
+ # Configurações Iniciais do Banco de Dados e LLM (de variáveis de ambiente como fallback/default)
29
+ DEFAULT_DB_USER = os.getenv("POSTGRES_USER")
30
+ DEFAULT_DB_PASSWORD = os.getenv("POSTGRES_PASSWORD")
31
+ DEFAULT_DB_HOST = os.getenv("POSTGRES_HOST")
32
+ DEFAULT_DB_PORT = os.getenv("POSTGRES_PORT")
33
+ DEFAULT_DB_NAME = os.getenv("POSTGRES_DB")
34
+
35
+ # Chaves de API de variáveis de ambiente
36
+ DEFAULT_OPENAI_API_KEY = os.getenv("OPENAI_API_KEY", "")
37
+ DEFAULT_ANTHROPIC_API_KEY = os.getenv("ANTHROPIC_API_KEY", "")
38
+ DEFAULT_GOOGLE_API_KEY = os.getenv("GOOGLE_API_KEY", "")
39
+ # Usado se DeepSeek não for via OpenAI compat.
40
+ DEFAULT_DEEPSEEK_API_KEY = os.getenv("DEEPSEEK_API_KEY", "")
41
+
42
+ # DATABASE_URL será definido dinamicamente
43
+ # Globais para componentes LangChain e DB
44
+ db_engine = None
45
+ db = None
46
+ llm = None
47
+ # db_chain = None # Substituído por agent_executor
48
+ agent_executor = None
49
+ chat_memory = None
50
+
51
+ # Sistema de tradução - dicionários de idiomas
52
+ TRANSLATIONS = {
53
+ "pt-BR": {
54
+ # Interface principal
55
+ "app_title": "SQLord - Converse com seu Banco de Dados PostgreSQL",
56
+ "chat_tab": "Chat com Banco",
57
+ "config_tab": "Configuração",
58
+ "question_placeholder": "Digite sua pergunta em linguagem natural sobre o banco de dados aqui... \nEx: Quantos usuários existem? Quais tabelas possuem a coluna 'nome'?",
59
+ "question_label": "Sua Pergunta",
60
+ "submit_button": "Enviar Pergunta",
61
+ "sql_output_label": "Query SQL Gerada",
62
+ "result_output_label": "Resultado da Query / Resposta do LLM",
63
+ "viz_title": "### Visualização Automática de Dados",
64
+ "viz_description_label": "Sobre esta visualização",
65
+
66
+ # Configuração
67
+ "db_config_title": "### Configuração do Banco de Dados PostgreSQL",
68
+ "db_user_label": "Usuário do Banco",
69
+ "db_password_label": "Senha do Banco",
70
+ "db_host_label": "Host do Banco",
71
+ "db_port_label": "Porta do Banco",
72
+ "db_name_label": "Nome do Banco de Dados",
73
+ "llm_config_title": "### Configuração do LLM",
74
+ "llm_provider_label": "Provedor LLM",
75
+ "api_key_label": "Chave API do LLM",
76
+ "api_key_placeholder": "Cole sua chave API aqui (opcional, usa var. de ambiente se vazio)",
77
+ "connect_button": "Aplicar Configurações e Conectar",
78
+ "status_title": "### Status",
79
+ "llm_status_label": "Status do LLM",
80
+ "connection_status_label": "Status da Conexão DB",
81
+ "current_config_label": "Configuração DB Ativa",
82
+
83
+ # Mensagens de status
84
+ "no_db_config": "Nenhuma configuração de banco de dados ativa.",
85
+ "waiting_db_config": "Aguardando configuração do banco de dados.",
86
+ "llm_not_configured": "LLM não configurado. Verifique as configurações.",
87
+ "connecting_to": "Tentando conectar a: {user}@{host}:{port}/{db_name}",
88
+ "connected_to": "Conectado a: {user}@{host}:{port}/{db_name}",
89
+ "connection_success": "Conexão com {db_name}@{host} bem-sucedida!",
90
+ "connection_fail": "Falha na conexão com o banco. Verifique os detalhes e tente novamente.",
91
+ "agent_ready": "Agente SQL com memória pronto (kwargs: {kwargs}).",
92
+ "agent_fail": "Agente SQL não pôde ser inicializado: LLM não configurado/disponível. Status LLM: {status}",
93
+ "configuring_llm": "Configurando LLM: {provider}...",
94
+ "llm_success": "LLM ({provider}) inicializado com sucesso.",
95
+
96
+ # Visualização
97
+ "line_chart": "Gráfico de Linha",
98
+ "bar_chart": "Gráfico de Barras",
99
+ "histogram": "Histograma",
100
+ "scatter_plot": "Gráfico de Dispersão",
101
+ "pie_chart": "Gráfico de Pizza",
102
+ "legend": "Legenda",
103
+ "evolution_of": "Evolução de {column} ao longo do tempo",
104
+ "by": "{numeric} por {category}",
105
+ "distribution_of": "Distribuição de {column}",
106
+ "relation_between": "Relação entre {column1} e {column2}",
107
+ "count_of": "Contagem de {column}",
108
+ "too_many_columns": "Muitas colunas ({count}) para visualização gráfica eficaz",
109
+ "data_not_suitable": "Dados não adequados para visualização gráfica automática",
110
+ "insufficient_data": "Dados insuficientes para visualização",
111
+ "too_large_dataset": "Conjunto de dados muito grande para visualização ({rows} linhas). Limite: {limit} linhas.",
112
+ "cannot_convert": "Não foi possível converter o resultado em dados tabulares.",
113
+ "no_viz_data": "Sem dados para visualização",
114
+
115
+ # Mensagens do chat
116
+ "chat_intro": "Faça uma pergunta em linguagem natural. O sistema (agora com memória) tentará gerar uma query SQL, executá-la e mostrar o resultado.",
117
+ "db_config": "DB Config: {config}",
118
+ "llm_status": "LLM Status: {status}",
119
+ "llm_not_operational": "LLM Status: {status} (Não operacional)",
120
+ "config_alerts": "Alertas de Configuração: {alerts}",
121
+ "db_status": "DB Status: {status}",
122
+ "db_connected": "DB Status: Conectado e pronto.",
123
+ "db_connected_agent_fail": "DB Status: Conectado, mas Agente SQL falhou ao inicializar.",
124
+ "db_not_connected": "DB Status: Não conectado. Configure na aba 'Configuração'.",
125
+
126
+ # Mensagens de erros e resultados
127
+ "agent_not_initialized": "O Agente SQL não foi inicializado.",
128
+ "db_not_connected_error": "Banco de dados não conectado.",
129
+ "llm_not_configured_error": "LLM não configurado.",
130
+ "memory_not_configured": "Memória da conversa não configurada.",
131
+ "setup_errors": "Erros de setup LLM: {errors}",
132
+ "llm_status_error": "Status LLM: {status}",
133
+ "db_connection_status": "Status da conexão DB: {status}",
134
+ "check_config": "Verifique a aba 'Configuração' e as variáveis de ambiente.",
135
+ "no_sql_query": "Nenhuma query SQL foi executada por este turno.",
136
+ "no_query_result": "Nenhum resultado de query SQL direto para este turno.",
137
+ "agent_direct_response": "O Agente respondeu diretamente (nenhuma query SQL específica para este turno).",
138
+ "process_error": "Erro ao processar a pergunta com o Agente SQL: {error}",
139
+
140
+ # Erros de LLM
141
+ "api_key_error": "Chave API da {provider} não fornecida.",
142
+ "package_error": "Pacote {package} não instalado. Execute: uv pip install {package}",
143
+ "unknown_provider": "Provedor LLM desconhecido: {provider}",
144
+ "import_error": "Erro de importação para {provider}: {error}. Verifique se o pacote está instalado.",
145
+ "init_error": "Erro ao inicializar LLM com {provider}: {error}.",
146
+ "db_error": "Erro ao conectar/configurar o banco de dados: {error}",
147
+ "agent_init_error": "Erro ao inicializar o Agente SQL: {error}",
148
+ "viz_error": "Erro ao criar visualização: {error}",
149
+ "convert_error": "Erro ao converter resultado para DataFrame: {error}"
150
+ },
151
+ "en-US": {
152
+ # Interface principal
153
+ "app_title": "SQLord - Talk to Your PostgreSQL Database",
154
+ "chat_tab": "Chat with Database",
155
+ "config_tab": "Configuration",
156
+ "question_placeholder": "Type your natural language question about the database here... \nEx: How many users exist? Which tables have the 'name' column?",
157
+ "question_label": "Your Question",
158
+ "submit_button": "Submit Question",
159
+ "sql_output_label": "Generated SQL Query",
160
+ "result_output_label": "Query Result / LLM Response",
161
+ "viz_title": "### Automatic Data Visualization",
162
+ "viz_description_label": "About this visualization",
163
+
164
+ # Configuração
165
+ "db_config_title": "### PostgreSQL Database Configuration",
166
+ "db_user_label": "Database User",
167
+ "db_password_label": "Database Password",
168
+ "db_host_label": "Database Host",
169
+ "db_port_label": "Database Port",
170
+ "db_name_label": "Database Name",
171
+ "llm_config_title": "### LLM Configuration",
172
+ "llm_provider_label": "LLM Provider",
173
+ "api_key_label": "LLM API Key",
174
+ "api_key_placeholder": "Paste your API key here (optional, uses env variable if empty)",
175
+ "connect_button": "Apply Settings and Connect",
176
+ "status_title": "### Status",
177
+ "llm_status_label": "LLM Status",
178
+ "connection_status_label": "DB Connection Status",
179
+ "current_config_label": "Active DB Configuration",
180
+
181
+ # Mensagens de status
182
+ "no_db_config": "No active database configuration.",
183
+ "waiting_db_config": "Waiting for database configuration.",
184
+ "llm_not_configured": "LLM not configured. Check settings.",
185
+ "connecting_to": "Trying to connect to: {user}@{host}:{port}/{db_name}",
186
+ "connected_to": "Connected to: {user}@{host}:{port}/{db_name}",
187
+ "connection_success": "Connection to {db_name}@{host} successful!",
188
+ "connection_fail": "Failed to connect to database. Check details and try again.",
189
+ "agent_ready": "SQL Agent with memory ready (kwargs: {kwargs}).",
190
+ "agent_fail": "SQL Agent could not be initialized: LLM not configured/available. LLM Status: {status}",
191
+ "configuring_llm": "Configuring LLM: {provider}...",
192
+ "llm_success": "LLM ({provider}) initialized successfully.",
193
+
194
+ # Visualização
195
+ "line_chart": "Line Chart",
196
+ "bar_chart": "Bar Chart",
197
+ "histogram": "Histogram",
198
+ "scatter_plot": "Scatter Plot",
199
+ "pie_chart": "Pie Chart",
200
+ "legend": "Legend",
201
+ "evolution_of": "Evolution of {column} over time",
202
+ "by": "{numeric} by {category}",
203
+ "distribution_of": "Distribution of {column}",
204
+ "relation_between": "Relationship between {column1} and {column2}",
205
+ "count_of": "Count of {column}",
206
+ "too_many_columns": "Too many columns ({count}) for effective graph visualization",
207
+ "data_not_suitable": "Data not suitable for automatic graphical visualization",
208
+ "insufficient_data": "Insufficient data for visualization",
209
+ "too_large_dataset": "Dataset too large for visualization ({rows} rows). Limit: {limit} rows.",
210
+ "cannot_convert": "Could not convert result to tabular data.",
211
+ "no_viz_data": "No data for visualization",
212
+
213
+ # Mensagens do chat
214
+ "chat_intro": "Ask a question in natural language. The system (now with memory) will try to generate an SQL query, execute it, and show the result.",
215
+ "db_config": "DB Config: {config}",
216
+ "llm_status": "LLM Status: {status}",
217
+ "llm_not_operational": "LLM Status: {status} (Not operational)",
218
+ "config_alerts": "Configuration Alerts: {alerts}",
219
+ "db_status": "DB Status: {status}",
220
+ "db_connected": "DB Status: Connected and ready.",
221
+ "db_connected_agent_fail": "DB Status: Connected, but SQL Agent failed to initialize.",
222
+ "db_not_connected": "DB Status: Not connected. Configure in the 'Configuration' tab.",
223
+
224
+ # Mensagens de erros e resultados
225
+ "agent_not_initialized": "The SQL Agent was not initialized.",
226
+ "db_not_connected_error": "Database not connected.",
227
+ "llm_not_configured_error": "LLM not configured.",
228
+ "memory_not_configured": "Conversation memory not configured.",
229
+ "setup_errors": "LLM setup errors: {errors}",
230
+ "llm_status_error": "LLM Status: {status}",
231
+ "db_connection_status": "DB connection status: {status}",
232
+ "check_config": "Check the 'Configuration' tab and environment variables.",
233
+ "no_sql_query": "No SQL query was executed for this turn.",
234
+ "no_query_result": "No direct SQL query result for this turn.",
235
+ "agent_direct_response": "The Agent responded directly (no specific SQL query for this turn).",
236
+ "process_error": "Error processing the question with SQL Agent: {error}",
237
+
238
+ # Erros de LLM
239
+ "api_key_error": "{provider} API key not provided.",
240
+ "package_error": "Package {package} not installed. Run: uv pip install {package}",
241
+ "unknown_provider": "Unknown LLM provider: {provider}",
242
+ "import_error": "Import error for {provider}: {error}. Check if the package is installed.",
243
+ "init_error": "Error initializing LLM with {provider}: {error}.",
244
+ "db_error": "Error connecting/configuring the database: {error}",
245
+ "agent_init_error": "Error initializing SQL Agent: {error}",
246
+ "viz_error": "Error creating visualization: {error}",
247
+ "convert_error": "Error converting result to DataFrame: {error}"
248
+ }
249
+ }
250
+
251
+ # Configuração global de idioma
252
+ DEFAULT_LANGUAGE = os.getenv(
253
+ "SQLORD_LANGUAGE", "pt-BR") # Português como padrão
254
+ current_language = DEFAULT_LANGUAGE
255
+
256
+
257
+ def _(key, **kwargs):
258
+ """
259
+ Função de tradução simples. Retorna o texto traduzido para o idioma atual.
260
+ Se a chave não existir, retorna a própria chave.
261
+ Suporta formatação com kwargs.
262
+ """
263
+ global current_language
264
+ translation = TRANSLATIONS.get(current_language, {}).get(key, key)
265
+ if kwargs:
266
+ try:
267
+ return translation.format(**kwargs)
268
+ except:
269
+ return translation
270
+ return translation
271
+
272
+
273
+ def set_language(language):
274
+ """Altera o idioma atual e retorna mensagem de confirmação"""
275
+ global current_language
276
+ if language in TRANSLATIONS:
277
+ current_language = language
278
+ return _("language_changed", language=language)
279
+ return _("language_not_available", language=language)
280
+
281
+
282
+ # Alterar as variáveis globais para usar o sistema de tradução
283
+ current_db_config_display = _("no_db_config")
284
+ setup_error_message_global = ""
285
+ connection_status_message_global = _("waiting_db_config")
286
+ llm_status_message_global = _("llm_not_configured")
287
+
288
+ LLM_PROVIDERS = [
289
+ "OpenAI", "Anthropic (Claude)", "Google (Gemini)", "DeepSeek (OpenAI compatible)"]
290
+
291
+
292
+ def _apply_postgres_interval_fix(sql_query: str) -> str:
293
+ """
294
+ Corrige a sintaxe de INTERVAL no PostgreSQL de 'INTERVAL N UNIT' para 'INTERVAL 'N UNIT''.
295
+ Exemplo: INTERVAL 1 MONTH -> INTERVAL '1 MONTH'
296
+ Funciona para unidades comuns como YEAR, MONTH, DAY, HOUR, MINUTE, SECOND.
297
+ """
298
+ # Padrão para encontrar "INTERVAL <dígitos> <UNIDADE_MAIUSCULA>" que não está já entre aspas.
299
+ # Esta regex é específica para o padrão problemático observado.
300
+ pattern = r"(INTERVAL\s+)(\d+\s+(?:YEAR|MONTH|DAY|HOUR|MINUTE|SECOND))(\s|;|$)"
301
+
302
+ def replacer(match):
303
+ # match.group(1) é "INTERVAL "
304
+ # match.group(2) é "N UNIT" (ex: "1 MONTH")
305
+ # match.group(3) é o caractere de terminação (espaço, ;, ou fim da string)
306
+ return f"{match.group(1)}'{match.group(2)}'{match.group(3)}"
307
+
308
+ corrected_sql = re.sub(pattern, replacer, sql_query)
309
+ if corrected_sql != sql_query:
310
+ print(
311
+ f"DEBUG: Aplicada correção de intervalo PostgreSQL na query exibida: De '{sql_query}' Para '{corrected_sql}'")
312
+ return corrected_sql
313
+
314
+
315
+ def _initialize_llm(api_key_from_ui: str, llm_provider: str):
316
+ global llm, setup_error_message_global, llm_status_message_global
317
+
318
+ # Prioriza a chave da UI, depois a variável de ambiente específica do provedor, depois a genérica OPENAI_API_KEY para compatibilidade
319
+ api_key = api_key_from_ui # Chave da UI tem maior prioridade
320
+
321
+ llm_status_message_global = _("configuring_llm", provider=llm_provider)
322
+ print(llm_status_message_global)
323
+
324
+ try:
325
+ if llm_provider == "OpenAI":
326
+ if not api_key:
327
+ api_key = DEFAULT_OPENAI_API_KEY
328
+ if not api_key:
329
+ raise ValueError(_("api_key_error", provider="OpenAI"))
330
+ llm = ChatOpenAI(temperature=0, openai_api_key=api_key,
331
+ model_name="gpt-3.5-turbo")
332
+ elif llm_provider == "Anthropic (Claude)":
333
+ if not ChatAnthropic:
334
+ raise ImportError(
335
+ _("package_error", package="langchain-anthropic"))
336
+ if not api_key:
337
+ api_key = DEFAULT_ANTHROPIC_API_KEY
338
+ if not api_key:
339
+ raise ValueError(_("api_key_error", provider="Anthropic"))
340
+ llm = ChatAnthropic(
341
+ temperature=0, anthropic_api_key=api_key, model_name="claude-3-haiku-20240307")
342
+ elif llm_provider == "Google (Gemini)":
343
+ if not ChatGoogleGenerativeAI:
344
+ raise ImportError(
345
+ _("package_error", package="langchain-google-genai"))
346
+ if not api_key:
347
+ api_key = DEFAULT_GOOGLE_API_KEY
348
+ if not api_key:
349
+ raise ValueError(_("api_key_error", provider="Google"))
350
+ llm = ChatGoogleGenerativeAI(
351
+ temperature=0, google_api_key=api_key, model="gemini-pro")
352
+ elif llm_provider == "DeepSeek (OpenAI compatible)":
353
+ if not api_key:
354
+ api_key = DEFAULT_DEEPSEEK_API_KEY if DEFAULT_DEEPSEEK_API_KEY else DEFAULT_OPENAI_API_KEY
355
+ if not api_key:
356
+ raise ValueError(_("api_key_error", provider="DeepSeek"))
357
+ openai_api_base = os.getenv("OPENAI_API_BASE")
358
+ if openai_api_base:
359
+ llm = ChatOpenAI(temperature=0, openai_api_key=api_key,
360
+ model_name="deepseek-coder", openai_api_base=openai_api_base)
361
+ else:
362
+ llm = ChatOpenAI(
363
+ temperature=0, openai_api_key=api_key, model_name="deepseek-coder")
364
+ else:
365
+ raise ValueError(_("unknown_provider", provider=llm_provider))
366
+
367
+ llm_status_message_global = _("llm_success", provider=llm_provider)
368
+ print(llm_status_message_global)
369
+ # Limpa erros de LLM anteriores se a inicialização for bem-sucedida
370
+ if llm_provider in setup_error_message_global:
371
+ setup_error_message_global = setup_error_message_global.replace(
372
+ f"Erro ao inicializar LLM com {llm_provider}:", "")
373
+ return True
374
+ except ImportError as e:
375
+ error_msg = _("import_error", provider=llm_provider, error=str(e))
376
+ print(error_msg)
377
+ setup_error_message_global += error_msg + "\n"
378
+ llm_status_message_global = error_msg
379
+ llm = None
380
+ return False
381
+ except Exception as e:
382
+ error_msg = _("init_error", provider=llm_provider, error=str(e))
383
+ print(error_msg)
384
+ setup_error_message_global += error_msg + "\n"
385
+ llm_status_message_global = error_msg
386
+ llm = None
387
+ return False
388
+
389
+
390
+ def connect_db_and_setup_components(db_user, db_password, db_host, db_port, db_name, api_key_ui, llm_provider_ui, language=None):
391
+ global db_engine, db, llm, agent_executor, chat_memory, current_language
392
+ global current_db_config_display, connection_status_message_global, setup_error_message_global, llm_status_message_global
393
+
394
+ # Atualiza o idioma se fornecido
395
+ if language and language in TRANSLATIONS:
396
+ current_language = language
397
+
398
+ connection_status_message_local = ""
399
+ current_db_config_display = _(
400
+ "connecting_to", user=db_user, host=db_host, port=db_port, db_name=db_name)
401
+
402
+ llm_initialized = _initialize_llm(api_key_ui, llm_provider_ui)
403
+ database_url = f"postgresql+psycopg2://{db_user}:{db_password}@{db_host}:{db_port}/{db_name}"
404
+
405
+ try:
406
+ db_engine = create_engine(database_url)
407
+ with db_engine.connect() as connection:
408
+ connection.execute(text("SELECT 1"))
409
+ db = SQLDatabase(engine=db_engine)
410
+ connection_status_message_local = _(
411
+ "connection_success", db_name=db_name, host=db_host)
412
+ print(connection_status_message_local)
413
+
414
+ if llm_initialized and db:
415
+ try:
416
+ # Inicializar a memória da conversa
417
+ chat_memory = ConversationBufferMemory(
418
+ memory_key="chat_history",
419
+ input_key="input",
420
+ return_messages=True
421
+ )
422
+
423
+ # Determinar o agent_type com base no provedor LLM
424
+ agent_type_kwargs = {}
425
+ if llm_provider_ui in ["OpenAI", "DeepSeek (OpenAI compatible)"]:
426
+ agent_type_kwargs['agent_type'] = "openai-tools"
427
+
428
+ # Adicionar informação sobre o idioma no prompt do sistema para o LLM
429
+ system_message = f"You are an SQL agent that helps users query databases. Please respond in {current_language}."
430
+
431
+ # Configurar opções avançadas do agente
432
+ # Número padrão de linhas a retornar
433
+ top_k = int(os.getenv("SQLORD_TOP_K", "50"))
434
+ max_iterations = int(os.getenv("SQLORD_MAX_ITERATIONS", "15"))
435
+ max_execution_time = float(os.getenv("SQLORD_MAX_EXECUTION_TIME", "60")) if os.getenv(
436
+ "SQLORD_MAX_EXECUTION_TIME") else None
437
+
438
+ # Prefixo personalizado para o prompt (opcional)
439
+ custom_prefix = os.getenv("SQLORD_CUSTOM_PREFIX", None)
440
+
441
+ # Configurar callbacks para logging melhorado (opcional)
442
+ agent_executor_kwargs = {
443
+ "handle_parsing_errors": True,
444
+ "return_intermediate_steps": True,
445
+ }
446
+
447
+ # Criar o SQL Agent com configurações avançadas
448
+ agent_executor = create_sql_agent(
449
+ llm=llm,
450
+ db=db,
451
+ **agent_type_kwargs,
452
+ memory=chat_memory,
453
+ verbose=True,
454
+ system_message=system_message,
455
+ top_k=top_k, # Número de linhas a retornar por padrão
456
+ max_iterations=max_iterations, # Evitar loops infinitos
457
+ max_execution_time=max_execution_time, # Timeout em segundos
458
+ prefix=custom_prefix, # Prefix customizado se definido
459
+ agent_executor_kwargs=agent_executor_kwargs
460
+ )
461
+
462
+ print(
463
+ f"Agente SQL com memória inicializado com sucesso (top_k={top_k}, max_iterations={max_iterations}).")
464
+ connection_status_message_local += _(
465
+ "agent_ready", kwargs=f"top_k={top_k}, iterations={max_iterations}")
466
+ except Exception as e:
467
+ error_msg = _("agent_init_error", error=str(e))
468
+ print(error_msg)
469
+ connection_status_message_local += f"\n{error_msg}"
470
+ agent_executor = None
471
+ chat_memory = None
472
+ elif not llm_initialized:
473
+ connection_status_message_local += _(
474
+ "agent_fail", status=llm_status_message_global)
475
+ agent_executor = None
476
+ chat_memory = None
477
+
478
+ current_db_config_display = _(
479
+ "connected_to", user=db_user, host=db_host, port=db_port, db_name=db_name)
480
+
481
+ except Exception as e:
482
+ error_msg = _("db_error", error=str(e))
483
+ print(error_msg)
484
+ db = None
485
+ agent_executor = None
486
+ chat_memory = None
487
+ current_db_config_display = _("connection_fail")
488
+ connection_status_message_local = error_msg
489
+
490
+ connection_status_message_global = connection_status_message_local
491
+ return connection_status_message_local, current_db_config_display, llm_status_message_global, _get_chat_description()
492
+
493
+
494
+ def get_sql_and_result(user_question: str):
495
+ """
496
+ Processa uma pergunta do usuário em linguagem natural usando um agente SQL com memória,
497
+ extrai a consulta SQL gerada e o resultado direto da execução da consulta.
498
+ """
499
+ global agent_executor, connection_status_message_global, setup_error_message_global, db
500
+
501
+ if not agent_executor:
502
+ error_detail = _("agent_not_initialized")
503
+ if not db:
504
+ error_detail += " " + _("db_not_connected_error")
505
+ if not llm:
506
+ error_detail += " " + _("llm_not_configured_error")
507
+ if not chat_memory:
508
+ error_detail += " " + _("memory_not_configured")
509
+ if setup_error_message_global:
510
+ error_detail += "\n" + \
511
+ _("setup_errors", errors=setup_error_message_global)
512
+ if llm_status_message_global and "Erro" in llm_status_message_global:
513
+ error_detail += "\n" + \
514
+ _("llm_status_error", status=llm_status_message_global)
515
+ if connection_status_message_global and ("Falha na conexão" in connection_status_message_global or "Erro" in connection_status_message_global):
516
+ error_detail += "\n" + \
517
+ _("db_connection_status", status=connection_status_message_global)
518
+ return f"Erro: {error_detail}", _("check_config")
519
+
520
+ try:
521
+ response = agent_executor.invoke(
522
+ {"input": user_question, "chat_history": chat_memory.chat_memory.messages})
523
+ print(f"DEBUG: Resposta completa do Agente SQL: {response}")
524
+
525
+ generated_sql = _("no_sql_query")
526
+ query_db_result = _("no_query_result")
527
+
528
+ # Extrair a última query SQL e seu resultado dos intermediate_steps
529
+ if response.get('intermediate_steps') and isinstance(response['intermediate_steps'], list):
530
+ for step in reversed(response['intermediate_steps']):
531
+ action, observation = step
532
+ if hasattr(action, 'tool') and 'sql' in action.tool.lower():
533
+ if hasattr(action, 'tool_input') and action.tool_input:
534
+ if isinstance(action.tool_input, str):
535
+ generated_sql = action.tool_input
536
+ elif isinstance(action.tool_input, dict) and 'query' in action.tool_input:
537
+ generated_sql = action.tool_input['query']
538
+ else:
539
+ generated_sql = str(action.tool_input)
540
+
541
+ query_db_result = str(observation)
542
+ print(
543
+ f"DEBUG: SQL extraído da ação do agente: '{generated_sql}'")
544
+ print(
545
+ f"DEBUG: Resultado da query (observação do agente): '{query_db_result}'")
546
+ break
547
+
548
+ if generated_sql == _("no_sql_query") and 'output' in response:
549
+ query_db_result = response['output']
550
+ generated_sql = _("agent_direct_response")
551
+
552
+ if generated_sql != _("no_sql_query") and generated_sql != _("agent_direct_response"):
553
+ if db and hasattr(db, 'dialect') and db.dialect == "postgresql":
554
+ generated_sql = _apply_postgres_interval_fix(generated_sql)
555
+
556
+ return generated_sql, query_db_result
557
+
558
+ except Exception as e:
559
+ error_msg = _("process_error", error=str(e))
560
+ print(error_msg)
561
+ return error_msg, ""
562
+
563
+
564
+ def _get_chat_description():
565
+ global setup_error_message_global, connection_status_message_global, current_db_config_display, llm_status_message_global, agent_executor, chat_memory
566
+
567
+ description = _("chat_intro") + "\n"
568
+ description += _("db_config", config=current_db_config_display) + "\n"
569
+
570
+ if llm:
571
+ description += _("llm_status", status=llm_status_message_global) + "\n"
572
+ else:
573
+ description += _("llm_not_operational",
574
+ status=llm_status_message_global) + "\n"
575
+
576
+ if setup_error_message_global:
577
+ description += _("config_alerts",
578
+ alerts=setup_error_message_global) + "\n"
579
+
580
+ if "Falha na conexão" in connection_status_message_global or "Aguardando configuração" in connection_status_message_global or "Erro" in connection_status_message_global:
581
+ description += _("db_status",
582
+ status=connection_status_message_global) + "\n"
583
+ elif db and agent_executor:
584
+ description += _("db_connected") + "\n"
585
+ elif db and not agent_executor and llm:
586
+ description += _("db_connected_agent_fail") + "\n"
587
+ elif not db:
588
+ description += _("db_not_connected") + "\n"
589
+
590
+ return description
591
+
592
+
593
+ # Adicionar às traduções
594
+ TRANSLATIONS["pt-BR"].update({
595
+ "chat_header": "Conversa com o Banco de Dados",
596
+ "chat_placeholder": "Digite sua pergunta sobre o banco de dados...",
597
+ "chat_button": "Enviar",
598
+ "chat_example": "Exemplo: Quais tabelas existem neste banco de dados?",
599
+ "sql_prefix": "📊 SQL Gerado: \n```sql\n",
600
+ "sql_suffix": "\n```\n",
601
+ "viz_prefix": "📈 Visualização: ",
602
+ "no_data": "Nenhum dado para exibição",
603
+ "advanced_settings": "Configurações Avançadas",
604
+ "top_k_label": "Linhas a retornar (top_k)",
605
+ "max_iterations_label": "Máximo de iterações do agente",
606
+ "max_execution_time_label": "Tempo máximo de execução (segundos)",
607
+ "custom_prefix_label": "Prefixo personalizado para o prompt",
608
+ "advanced_settings_info": "Configurações avançadas aplicadas: top_k={top_k}, max_iterations={max_iterations}"
609
+ })
610
+
611
+ TRANSLATIONS["en-US"].update({
612
+ "chat_header": "Database Conversation",
613
+ "chat_placeholder": "Type your question about the database...",
614
+ "chat_button": "Send",
615
+ "chat_example": "Example: What tables exist in this database?",
616
+ "sql_prefix": "📊 Generated SQL: \n```sql\n",
617
+ "sql_suffix": "\n```\n",
618
+ "viz_prefix": "📈 Visualization: ",
619
+ "no_data": "No data to display",
620
+ "advanced_settings": "Advanced Settings",
621
+ "top_k_label": "Rows to return (top_k)",
622
+ "max_iterations_label": "Maximum agent iterations",
623
+ "max_execution_time_label": "Maximum execution time (seconds)",
624
+ "custom_prefix_label": "Custom prompt prefix",
625
+ "advanced_settings_info": "Applied advanced settings: top_k={top_k}, max_iterations={max_iterations}"
626
+ })
627
+
628
+ # Histórico de chat como global para persistir entre interações
629
+ chat_history = []
630
+
631
+
632
+ # Configurações para visualização de dados
633
+ MAX_ROWS_FOR_VISUALIZATION = 1000 # Limite de linhas para visualização
634
+ MAX_COLS_FOR_VISUALIZATION = 150 # Limite de colunas para visualização
635
+
636
+
637
+ def determine_visualization_type(df):
638
+ """
639
+ Analisa um DataFrame e determina o tipo de visualização mais adequado.
640
+ """
641
+ # Se não houver dados suficientes para visualização
642
+ if df.empty or len(df.columns) < 1:
643
+ return {"type": "none", "message": _("insufficient_data")}
644
+
645
+ # Se tiver muitas colunas, difícil visualizar bem
646
+ if len(df.columns) > MAX_COLS_FOR_VISUALIZATION:
647
+ return {"type": "table", "message": _("too_many_columns", count=len(df.columns))}
648
+
649
+ # Classificar colunas por tipo
650
+ numeric_cols = df.select_dtypes(include=['number']).columns.tolist()
651
+ categorical_cols = df.select_dtypes(
652
+ include=['object', 'category', 'bool']).columns.tolist()
653
+ datetime_cols = df.select_dtypes(
654
+ include=['datetime', 'datetimetz']).columns.tolist()
655
+
656
+ # Tentar detectar colunas de data/hora em formato string
657
+ for col in categorical_cols:
658
+ if df[col].dtype == 'object':
659
+ # Tenta converter para datetime para ver se é uma coluna de data disfarçada
660
+ try:
661
+ pd.to_datetime(df[col], errors='raise')
662
+ datetime_cols.append(col)
663
+ categorical_cols.remove(col)
664
+ except:
665
+ pass
666
+
667
+ # Determinar o tipo de visualização com base nos tipos de dados
668
+
669
+ # Caso 1: Dados temporais (séries temporais)
670
+ if datetime_cols and numeric_cols:
671
+ return {
672
+ "type": "line",
673
+ "x": datetime_cols[0],
674
+ "y": numeric_cols[0],
675
+ "color": categorical_cols[0] if categorical_cols else None,
676
+ "title": _("evolution_of", column=numeric_cols[0])
677
+ }
678
+
679
+ # Caso 2: Comparação entre categorias (barras)
680
+ elif categorical_cols and numeric_cols:
681
+ return {
682
+ "type": "bar",
683
+ "x": categorical_cols[0],
684
+ "y": numeric_cols[0],
685
+ "color": categorical_cols[1] if len(categorical_cols) > 1 else None,
686
+ "title": _("by", numeric=numeric_cols[0], category=categorical_cols[0])
687
+ }
688
+
689
+ # Caso 3: Distribuição de valores numéricos (histograma)
690
+ elif numeric_cols and len(numeric_cols) >= 1:
691
+ return {
692
+ "type": "histogram",
693
+ "x": numeric_cols[0],
694
+ "color": categorical_cols[0] if categorical_cols else None,
695
+ "title": _("distribution_of", column=numeric_cols[0])
696
+ }
697
+
698
+ # Caso 4: Correlação entre variáveis numéricas (scatter)
699
+ elif len(numeric_cols) >= 2:
700
+ return {
701
+ "type": "scatter",
702
+ "x": numeric_cols[0],
703
+ "y": numeric_cols[1],
704
+ "color": categorical_cols[0] if categorical_cols else None,
705
+ "title": _("relation_between", column1=numeric_cols[0], column2=numeric_cols[1])
706
+ }
707
+
708
+ # Caso 5: Apenas categorias (pizza ou barras)
709
+ elif categorical_cols and len(categorical_cols) >= 1:
710
+ if len(df[categorical_cols[0]].unique()) <= 50: # Alterado de 10 para 50
711
+ return {
712
+ "type": "pie",
713
+ "names": categorical_cols[0],
714
+ "title": _("distribution_of", column=categorical_cols[0])
715
+ }
716
+ else:
717
+ return {
718
+ "type": "bar",
719
+ "x": categorical_cols[0],
720
+ "title": _("count_of", column=categorical_cols[0])
721
+ }
722
+
723
+ # Caso padrão: tabela
724
+ return {"type": "table", "message": _("data_not_suitable")}
725
+
726
+
727
+ def create_visualization(df, viz_config):
728
+ """
729
+ Cria uma visualização Plotly com base na configuração determinada.
730
+ """
731
+ try:
732
+ if viz_config["type"] == "none" or viz_config["type"] == "table":
733
+ return None
734
+
735
+ if viz_config["type"] == "line":
736
+ fig = px.line(
737
+ df,
738
+ x=viz_config["x"],
739
+ y=viz_config["y"],
740
+ color=viz_config.get("color"),
741
+ title=viz_config.get("title", _("line_chart"))
742
+ )
743
+
744
+ elif viz_config["type"] == "bar":
745
+ fig = px.bar(
746
+ df,
747
+ x=viz_config["x"],
748
+ y=viz_config.get("y"), # y pode ser None em alguns casos
749
+ color=viz_config.get("color"),
750
+ title=viz_config.get("title", _("bar_chart"))
751
+ )
752
+
753
+ elif viz_config["type"] == "histogram":
754
+ fig = px.histogram(
755
+ df,
756
+ x=viz_config["x"],
757
+ color=viz_config.get("color"),
758
+ title=viz_config.get("title", _("histogram"))
759
+ )
760
+
761
+ elif viz_config["type"] == "scatter":
762
+ fig = px.scatter(
763
+ df,
764
+ x=viz_config["x"],
765
+ y=viz_config["y"],
766
+ color=viz_config.get("color"),
767
+ title=viz_config.get("title", _("scatter_plot"))
768
+ )
769
+
770
+ elif viz_config["type"] == "pie":
771
+ # Agrupa e conta para criar o gráfico de pizza
772
+ count_df = df[viz_config["names"]].value_counts().reset_index()
773
+ count_df.columns = [viz_config["names"], 'count']
774
+
775
+ fig = px.pie(
776
+ count_df,
777
+ names=viz_config["names"],
778
+ values='count',
779
+ title=viz_config.get("title", _("pie_chart"))
780
+ )
781
+
782
+ # Ajustes gerais da figura
783
+ fig.update_layout(
784
+ template="plotly_white",
785
+ legend_title_text=_("legend")
786
+ )
787
+
788
+ return fig
789
+
790
+ except Exception as e:
791
+ print(_("viz_error", error=str(e)))
792
+ return None
793
+
794
+
795
+ def convert_sql_result_to_dataframe(sql_result):
796
+ """
797
+ Converte o resultado de uma consulta SQL em um DataFrame pandas.
798
+ """
799
+ try:
800
+ # Tenta interpretar como JSON (formato comum para resultados)
801
+ try:
802
+ data = json.loads(sql_result)
803
+ if isinstance(data, list) and len(data) > 0:
804
+ return pd.DataFrame(data)
805
+ except:
806
+ pass
807
+
808
+ # Tenta interpretar como CSV/TSV
809
+ try:
810
+ return pd.read_csv(StringIO(sql_result), sep=None, engine='python')
811
+ except:
812
+ pass
813
+
814
+ # Tenta interpretar como texto tabulado ou espaçado
815
+ lines = sql_result.strip().split('\n')
816
+ if len(lines) >= 2: # Precisa de pelo menos header + uma linha
817
+ # Supõe que a primeira linha é o cabeçalho
818
+ header = lines[0].split()
819
+ data = []
820
+ for line in lines[1:]:
821
+ values = line.split()
822
+ if len(values) == len(header):
823
+ data.append(values)
824
+
825
+ if data:
826
+ return pd.DataFrame(data, columns=header)
827
+
828
+ return None
829
+ except Exception as e:
830
+ print(_("convert_error", error=str(e)))
831
+ return None
832
+
833
+
834
+ def get_sql_and_result_with_viz(user_question: str):
835
+ """
836
+ Processa uma pergunta e gera SQL, resultado e visualização.
837
+
838
+ Args:
839
+ user_question: A pergunta do usuário em linguagem natural
840
+
841
+ Returns:
842
+ Tupla com (sql_gerado, resultado_query, figura_plotly, descrição_visualização)
843
+ """
844
+ # Obter SQL e resultado usando a função existente
845
+ generated_sql, query_db_result = get_sql_and_result(user_question)
846
+
847
+ # Se temos um resultado de query bem-sucedido, tentamos visualizar
848
+ if query_db_result and not query_db_result.startswith("Erro:") and not query_db_result == _("check_config"):
849
+ # Converter o resultado para DataFrame
850
+ df = convert_sql_result_to_dataframe(query_db_result)
851
+
852
+ if df is not None and not df.empty and len(df) <= MAX_ROWS_FOR_VISUALIZATION:
853
+ # Determinar o tipo de visualização apropriado
854
+ viz_config = determine_visualization_type(df)
855
+ # Criar a visualização
856
+ fig = create_visualization(df, viz_config)
857
+
858
+ if fig:
859
+ return generated_sql, query_db_result, fig, viz_config.get("title", _("viz_title"))
860
+ else:
861
+ message = viz_config.get("message", _("cannot_convert"))
862
+ return generated_sql, query_db_result, None, message
863
+ elif df is not None and len(df) > MAX_ROWS_FOR_VISUALIZATION:
864
+ return generated_sql, query_db_result, None, _("too_large_dataset", rows=len(df), limit=MAX_ROWS_FOR_VISUALIZATION)
865
+ else:
866
+ return generated_sql, query_db_result, None, _("cannot_convert")
867
+
868
+ # Se não conseguimos visualizar, retornamos sem figura
869
+ return generated_sql, query_db_result, None, _("no_viz_data")
870
+
871
+
872
+ def process_chat_message(message, history):
873
+ """
874
+ Processa a mensagem do chat, gera a resposta com SQL e resultado integrados.
875
+ Retorna a resposta para exibição no chatbot no formato correto para type='messages'.
876
+ """
877
+ global chat_history
878
+
879
+ if not message.strip():
880
+ return "", history
881
+
882
+ # Obter SQL, resultado e visualização
883
+ sql, result, fig, viz_desc = get_sql_and_result_with_viz(message)
884
+
885
+ # Construir resposta formatada
886
+ response = ""
887
+
888
+ # Adicionar SQL gerado, se disponível
889
+ if sql and sql != _("no_sql_query") and sql != _("agent_direct_response"):
890
+ response += _("sql_prefix") + sql + _("sql_suffix")
891
+
892
+ # Adicionar resultado
893
+ if result:
894
+ response += result
895
+
896
+ # Informações de visualização, se disponível
897
+ if fig:
898
+ # Converter a figura para HTML para exibir no chat
899
+ fig_html = fig.to_html(include_plotlyjs="cdn", full_html=False)
900
+ response += f"\n\n{_('viz_prefix')} {viz_desc}\n\n"
901
+ response += f"<div>{fig_html}</div>"
902
+ elif viz_desc and viz_desc != _("no_viz_data"):
903
+ response += f"\n\n{_('viz_prefix')} {viz_desc}"
904
+
905
+ # Criar mensagens no formato correto para type='messages' (dicionários com 'role' e 'content')
906
+ user_message = {"role": "user", "content": message}
907
+ assistant_message = {"role": "assistant", "content": response}
908
+
909
+ # Atualizar histórico com o formato correto
910
+ updated_history = history + [user_message, assistant_message]
911
+ chat_history = updated_history
912
+
913
+ return "", updated_history
914
+
915
+
916
+ def main():
917
+ print("Iniciando SQLord...")
918
+ global connection_status_message_global, current_db_config_display, llm_status_message_global, current_language, chat_history
919
+
920
+ with gr.Blocks() as iface:
921
+ # Título da aplicação
922
+ app_title = gr.Markdown(_("app_title"))
923
+
924
+ # Seletor de idioma no topo
925
+ with gr.Row():
926
+ language_selector = gr.Dropdown(
927
+ choices=list(TRANSLATIONS.keys()),
928
+ value=current_language,
929
+ label="Idioma / Language",
930
+ interactive=True
931
+ )
932
+
933
+ # Criar as abas - não tentaremos alterar seus nomes após a criação
934
+ with gr.Tabs() as tabs:
935
+ # Interface de chat simplificada
936
+ with gr.TabItem(_("chat_tab")) as chat_tab:
937
+ status_info = gr.Markdown(value=_get_chat_description)
938
+
939
+ # Interface de chatbot com type='messages'
940
+ chatbot = gr.Chatbot(
941
+ value=chat_history,
942
+ show_label=False,
943
+ height=400,
944
+ type='messages'
945
+ )
946
+
947
+ with gr.Row():
948
+ msg = gr.Textbox(
949
+ placeholder=_("chat_placeholder"),
950
+ show_label=False,
951
+ scale=9
952
+ )
953
+ submit = gr.Button(_("chat_button"), scale=1)
954
+
955
+ gr.Examples(
956
+ examples=[
957
+ _("chat_example"),
958
+ "Mostre os usuários criados nos últimos 30 dias",
959
+ ],
960
+ inputs=msg
961
+ )
962
+
963
+ # Botão para limpar conversa - atualizado para retornar lista vazia
964
+ clear = gr.Button("Limpar Conversa")
965
+ clear.click(lambda: [], outputs=[chatbot], show_progress=False)
966
+
967
+ # Processar mensagem
968
+ submit.click(
969
+ process_chat_message,
970
+ inputs=[msg, chatbot],
971
+ outputs=[msg, chatbot]
972
+ )
973
+ msg.submit(
974
+ process_chat_message,
975
+ inputs=[msg, chatbot],
976
+ outputs=[msg, chatbot]
977
+ )
978
+
979
+ # Aba de configuração
980
+ with gr.TabItem(_("config_tab")) as config_tab:
981
+ db_config_title = gr.Markdown(_("db_config_title"))
982
+
983
+ db_user_input = gr.Textbox(
984
+ label=_("db_user_label"), value=DEFAULT_DB_USER)
985
+ db_password_input = gr.Textbox(
986
+ label=_("db_password_label"), type="password", value=DEFAULT_DB_PASSWORD)
987
+ db_host_input = gr.Textbox(
988
+ label=_("db_host_label"), value=DEFAULT_DB_HOST)
989
+ db_port_input = gr.Textbox(
990
+ label=_("db_port_label"), value=DEFAULT_DB_PORT)
991
+ db_name_input = gr.Textbox(
992
+ label=_("db_name_label"), value=DEFAULT_DB_NAME)
993
+
994
+ llm_config_title = gr.Markdown(_("llm_config_title"))
995
+ llm_provider_input = gr.Dropdown(
996
+ choices=LLM_PROVIDERS, label=_("llm_provider_label"), value=LLM_PROVIDERS[0])
997
+ api_key_input = gr.Textbox(
998
+ label=_("api_key_label"),
999
+ type="password",
1000
+ placeholder=_("api_key_placeholder")
1001
+ )
1002
+
1003
+ connect_button = gr.Button(_("connect_button"))
1004
+
1005
+ status_title = gr.Markdown(_("status_title"))
1006
+ llm_status_output = gr.Textbox(
1007
+ label=_("llm_status_label"), interactive=False, lines=2, value=llm_status_message_global)
1008
+ connection_status_output = gr.Textbox(
1009
+ label=_("connection_status_label"), interactive=False, lines=2, value=connection_status_message_global)
1010
+ current_config_output = gr.Textbox(
1011
+ label=_("current_config_label"), interactive=False, value=current_db_config_display)
1012
+
1013
+ # Adicionar seção de configurações avançadas
1014
+ advanced_settings_title = gr.Markdown(
1015
+ "### " + _("advanced_settings"))
1016
+
1017
+ with gr.Accordion("Configurações avançadas", open=False):
1018
+ top_k_input = gr.Slider(
1019
+ minimum=5, maximum=500, value=50, step=5,
1020
+ label=_("top_k_label")
1021
+ )
1022
+ max_iterations_input = gr.Slider(
1023
+ minimum=5, maximum=50, value=15, step=1,
1024
+ label=_("max_iterations_label")
1025
+ )
1026
+ max_execution_time_input = gr.Slider(
1027
+ minimum=10, maximum=300, value=60, step=5,
1028
+ label=_("max_execution_time_label")
1029
+ )
1030
+ custom_prefix_input = gr.Textbox(
1031
+ label=_("custom_prefix_label"),
1032
+ placeholder="Deixe em branco para usar o padrão",
1033
+ lines=3
1034
+ )
1035
+
1036
+ # Adicionar botão para visualizar esquema do banco
1037
+ db_schema_button = gr.Button("Ver Esquema do Banco")
1038
+ db_schema_output = gr.TextArea(
1039
+ label="Esquema do Banco de Dados",
1040
+ interactive=False,
1041
+ visible=True,
1042
+ lines=10
1043
+ )
1044
+
1045
+ # Função para atualizar o esquema
1046
+ db_schema_button.click(
1047
+ fn=get_db_table_info,
1048
+ outputs=db_schema_output
1049
+ )
1050
+
1051
+ # Função para conectar usando o idioma atual
1052
+ def connect_with_language(db_user, db_password, db_host, db_port, db_name, api_key_ui, llm_provider_ui):
1053
+ return connect_db_and_setup_components(
1054
+ db_user, db_password, db_host, db_port, db_name,
1055
+ api_key_ui, llm_provider_ui, current_language
1056
+ )
1057
+
1058
+ connect_button.click(
1059
+ fn=connect_with_language,
1060
+ inputs=[db_user_input, db_password_input, db_host_input,
1061
+ db_port_input, db_name_input, api_key_input, llm_provider_input],
1062
+ outputs=[connection_status_output, current_config_output,
1063
+ llm_status_output, status_info]
1064
+ )
1065
+
1066
+ # SOLUÇÃO DEFINITIVA: Usar update_ui_on_language_change para atualizar textos
1067
+ def update_ui_on_language_change(language):
1068
+ """
1069
+ Atualiza a interface com base no novo idioma selecionado.
1070
+ Usa gr.update() para garantir que as propriedades dos componentes sejam atualizadas corretamente.
1071
+ """
1072
+ # Altera o idioma global
1073
+ set_language(language)
1074
+
1075
+ # Retorna os novos valores para os componentes
1076
+ return {
1077
+ app_title: _("app_title"),
1078
+ status_info: _get_chat_description(),
1079
+ submit: _("chat_button"),
1080
+ db_config_title: _("db_config_title"),
1081
+ llm_config_title: _("llm_config_title"),
1082
+ status_title: _("status_title"),
1083
+ # Atualizar propriedades de componentes
1084
+ msg: gr.update(placeholder=_("chat_placeholder")),
1085
+ db_user_input: gr.update(label=_("db_user_label")),
1086
+ db_password_input: gr.update(label=_("db_password_label")),
1087
+ db_host_input: gr.update(label=_("db_host_label")),
1088
+ db_port_input: gr.update(label=_("db_port_label")),
1089
+ db_name_input: gr.update(label=_("db_name_label")),
1090
+ llm_provider_input: gr.update(label=_("llm_provider_label")),
1091
+ api_key_input: gr.update(label=_("api_key_label"), placeholder=_("api_key_placeholder")),
1092
+ connect_button: _("connect_button"),
1093
+ llm_status_output: gr.update(label=_("llm_status_label")),
1094
+ connection_status_output: gr.update(label=_("connection_status_label")),
1095
+ current_config_output: gr.update(label=_("current_config_label")),
1096
+ clear: "Limpar Conversa" if language == "pt-BR" else "Clear Chat",
1097
+ advanced_settings_title: "### " + _("advanced_settings"),
1098
+ top_k_input: gr.update(label=_("top_k_label")),
1099
+ max_iterations_input: gr.update(label=_("max_iterations_label")),
1100
+ max_execution_time_input: gr.update(label=_("max_execution_time_label")),
1101
+ custom_prefix_input: gr.update(label=_("custom_prefix_label")),
1102
+ db_schema_button: "Ver Esquema do Banco" if language == "pt-BR" else "View Database Schema",
1103
+ }
1104
+
1105
+ # Registrar a função para executar quando o idioma mudar
1106
+ language_selector.change(
1107
+ fn=update_ui_on_language_change,
1108
+ inputs=language_selector,
1109
+ outputs=[
1110
+ app_title, status_info, submit, db_config_title, llm_config_title,
1111
+ status_title, msg, db_user_input, db_password_input, db_host_input,
1112
+ db_port_input, db_name_input, llm_provider_input, api_key_input,
1113
+ connect_button, llm_status_output, connection_status_output,
1114
+ current_config_output, clear, advanced_settings_title, top_k_input,
1115
+ max_iterations_input, max_execution_time_input, custom_prefix_input,
1116
+ db_schema_button
1117
+ ]
1118
+ )
1119
+
1120
+ print("Lançando interface Gradio...")
1121
+ iface.launch(mcp_server=True)
1122
+
1123
+
1124
+ # Função para obter informações sobre as tabelas do banco de dados
1125
+ def get_db_table_info():
1126
+ """Obtém informações detalhadas sobre as tabelas do banco de dados conectado."""
1127
+ global db
1128
+
1129
+ if not db:
1130
+ return _("db_not_connected_error")
1131
+
1132
+ try:
1133
+ # Usar a função interna do SQLDatabase para obter informações das tabelas
1134
+ table_info = db.get_table_info()
1135
+ return table_info
1136
+ except Exception as e:
1137
+ error_msg = f"Erro ao obter informações das tabelas: {str(e)}"
1138
+ print(error_msg)
1139
+ return error_msg
1140
+
1141
+
1142
+ if __name__ == "__main__":
1143
+ main()
pyproject.toml ADDED
@@ -0,0 +1,21 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ [project]
2
+ name = "sqlord"
3
+ version = "0.1.0"
4
+ description = "Add your description here"
5
+ readme = "README.md"
6
+ requires-python = ">=3.12"
7
+ dependencies = [
8
+ "gradio[mcp]>=5.33.0",
9
+ "langchain[anthropic]>=0.3.25",
10
+ "langchain-anthropic>=0.3.15",
11
+ "langchain-community>=0.3.24",
12
+ "langchain-experimental>=0.3.4",
13
+ "langchain-google-genai>=2.1.5",
14
+ "langchain-openai>=0.3.21",
15
+ "langgraph>=0.4.8",
16
+ "pandas>=2.3.0",
17
+ "plotly>=6.1.2",
18
+ "psycopg2-binary>=2.9.10",
19
+ "python-dotenv>=1.1.0",
20
+ "sqlalchemy>=2.0.41",
21
+ ]
requirements.txt ADDED
@@ -0,0 +1,12 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ gradio
2
+ langchain
3
+ langchain-openai
4
+ langchain-community
5
+ langchain-experimental
6
+ sqlalchemy
7
+ psycopg2-binary
8
+ python-dotenv
9
+ langchain-anthropic
10
+ langchain-google-genai
11
+ pandas
12
+ plotly
uv.lock ADDED
The diff for this file is too large to render. See raw diff