rwayz commited on
Commit
7094511
·
1 Parent(s): 1284083
agents/processing_agent.py CHANGED
@@ -6,6 +6,7 @@ import asyncio
6
  from typing import Optional, Dict, Any
7
  from langchain_openai import ChatOpenAI
8
  from langchain_anthropic import ChatAnthropic
 
9
  from langchain_community.llms import HuggingFaceEndpoint
10
  from langchain.schema import HumanMessage
11
 
@@ -14,6 +15,7 @@ from utils.config import (
14
  AVAILABLE_MODELS,
15
  OPENAI_MODELS,
16
  ANTHROPIC_MODELS,
 
17
  REFINEMENT_MODELS
18
  )
19
 
@@ -57,7 +59,17 @@ class ProcessingAgentManager:
57
  max_retries=2,
58
  timeout=60.0
59
  )
60
-
 
 
 
 
 
 
 
 
 
 
61
  else:
62
  # Modelos HuggingFace (refinement models)
63
  self.llm = HuggingFaceEndpoint(
 
6
  from typing import Optional, Dict, Any
7
  from langchain_openai import ChatOpenAI
8
  from langchain_anthropic import ChatAnthropic
9
+ from langchain_google_genai import ChatGoogleGenerativeAI
10
  from langchain_community.llms import HuggingFaceEndpoint
11
  from langchain.schema import HumanMessage
12
 
 
15
  AVAILABLE_MODELS,
16
  OPENAI_MODELS,
17
  ANTHROPIC_MODELS,
18
+ GOOGLE_MODELS,
19
  REFINEMENT_MODELS
20
  )
21
 
 
59
  max_retries=2,
60
  timeout=60.0
61
  )
62
+
63
+ elif model_id in GOOGLE_MODELS:
64
+ # Gemini com configurações otimizadas
65
+ self.llm = ChatGoogleGenerativeAI(
66
+ model=model_id,
67
+ temperature=TEMPERATURE,
68
+ max_tokens=4096,
69
+ max_retries=2,
70
+ timeout=60.0
71
+ )
72
+
73
  else:
74
  # Modelos HuggingFace (refinement models)
75
  self.llm = HuggingFaceEndpoint(
agents/sql_agent.py CHANGED
@@ -7,6 +7,7 @@ import asyncio
7
  from typing import Optional, Dict, Any, List
8
  from langchain_openai import ChatOpenAI
9
  from langchain_anthropic import ChatAnthropic
 
10
  from langchain_community.agent_toolkits import create_sql_agent
11
  from langchain_community.utilities import SQLDatabase
12
  from langchain.callbacks.base import BaseCallbackHandler
@@ -18,7 +19,8 @@ from utils.config import (
18
  TEMPERATURE,
19
  AVAILABLE_MODELS,
20
  OPENAI_MODELS,
21
- ANTHROPIC_MODELS
 
22
  )
23
 
24
  class SQLQueryCaptureHandler(BaseCallbackHandler):
@@ -126,18 +128,34 @@ async def retry_with_backoff(func, max_retries=3, base_delay=1.0):
126
 
127
 
128
 
129
- def create_sql_agent_executor(db: SQLDatabase, model_name: str = "gpt-4o-mini"):
130
  """
131
  Cria um agente SQL usando LangChain com suporte a diferentes provedores
132
 
133
  Args:
134
  db: Objeto SQLDatabase do LangChain
135
  model_name: Nome do modelo a usar (OpenAI, Anthropic)
 
 
136
 
137
  Returns:
138
  Agente SQL configurado
139
  """
140
  try:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
141
  # Obtém o ID real do modelo
142
  model_id = AVAILABLE_MODELS.get(model_name, model_name)
143
 
@@ -164,6 +182,17 @@ def create_sql_agent_executor(db: SQLDatabase, model_name: str = "gpt-4o-mini"):
164
  )
165
  agent_type = "tool-calling" # Claude usa tool-calling
166
 
 
 
 
 
 
 
 
 
 
 
 
167
  else:
168
  # Fallback para OpenAI
169
  llm = ChatOpenAI(
@@ -173,10 +202,10 @@ def create_sql_agent_executor(db: SQLDatabase, model_name: str = "gpt-4o-mini"):
173
  agent_type = "openai-tools"
174
  logging.warning(f"Modelo {model_name} não reconhecido, usando gpt-4o-mini como fallback")
175
 
176
- # Cria o agente SQL
177
  sql_agent = create_sql_agent(
178
  llm=llm,
179
- db=db,
180
  agent_type=agent_type,
181
  verbose=True,
182
  max_iterations=MAX_ITERATIONS,
@@ -195,32 +224,41 @@ class SQLAgentManager:
195
  """
196
  Gerenciador do agente SQL com funcionalidades avançadas
197
  """
198
-
199
- def __init__(self, db: SQLDatabase, model_name: str = "gpt-4o-mini"):
200
  self.db = db
201
  self.model_name = model_name
 
 
202
  self.agent = None
203
  self._initialize_agent()
204
-
205
  def _initialize_agent(self):
206
  """Inicializa o agente SQL"""
207
- self.agent = create_sql_agent_executor(self.db, self.model_name)
208
 
209
- def recreate_agent(self, new_db: SQLDatabase = None, new_model: str = None):
210
  """
211
  Recria o agente com novos parâmetros
212
-
213
  Args:
214
  new_db: Novo banco de dados (opcional)
215
  new_model: Novo modelo (opcional)
 
 
216
  """
217
  if new_db:
218
  self.db = new_db
219
  if new_model:
220
  self.model_name = new_model
221
-
 
 
 
 
222
  self._initialize_agent()
223
- logging.info("Agente SQL recriado com sucesso")
 
224
 
225
  def _extract_text_from_claude_response(self, output) -> str:
226
  """
@@ -279,12 +317,13 @@ class SQLAgentManager:
279
  # Criar handler para capturar SQL
280
  sql_handler = SQLQueryCaptureHandler()
281
 
282
- # Verifica se é agente Claude para aplicar retry
283
  model_id = getattr(self, 'model_name', '')
284
  is_claude = any(claude_model in model_id for claude_model in ANTHROPIC_MODELS)
 
285
 
286
- if is_claude:
287
- # Usa retry com backoff para Claude
288
  response = await retry_with_backoff(
289
  lambda: self.agent.invoke(
290
  {"input": instruction},
@@ -342,7 +381,7 @@ class SQLAgentManager:
342
  "intermediate_steps": [],
343
  "success": False
344
  }
345
-
346
  def get_agent_info(self) -> dict:
347
  """
348
  Retorna informações sobre o agente atual
 
7
  from typing import Optional, Dict, Any, List
8
  from langchain_openai import ChatOpenAI
9
  from langchain_anthropic import ChatAnthropic
10
+ from langchain_google_genai import ChatGoogleGenerativeAI
11
  from langchain_community.agent_toolkits import create_sql_agent
12
  from langchain_community.utilities import SQLDatabase
13
  from langchain.callbacks.base import BaseCallbackHandler
 
19
  TEMPERATURE,
20
  AVAILABLE_MODELS,
21
  OPENAI_MODELS,
22
+ ANTHROPIC_MODELS,
23
+ GOOGLE_MODELS
24
  )
25
 
26
  class SQLQueryCaptureHandler(BaseCallbackHandler):
 
128
 
129
 
130
 
131
+ def create_sql_agent_executor(db: SQLDatabase, model_name: str = "gpt-4o-mini", single_table_mode: bool = False, selected_table: str = None):
132
  """
133
  Cria um agente SQL usando LangChain com suporte a diferentes provedores
134
 
135
  Args:
136
  db: Objeto SQLDatabase do LangChain
137
  model_name: Nome do modelo a usar (OpenAI, Anthropic)
138
+ single_table_mode: Se deve restringir a uma única tabela
139
+ selected_table: Tabela específica para modo único
140
 
141
  Returns:
142
  Agente SQL configurado
143
  """
144
  try:
145
+ # Se modo tabela única, cria SQLDatabase restrito
146
+ if single_table_mode and selected_table:
147
+ # Cria uma nova instância do SQLDatabase restrita à tabela selecionada
148
+ restricted_db = SQLDatabase.from_uri(
149
+ db._engine.url,
150
+ include_tables=[selected_table]
151
+ )
152
+ logging.info(f"[SQL_AGENT] Criando agente em modo tabela única: {selected_table}")
153
+ db_to_use = restricted_db
154
+ else:
155
+ # Usa o SQLDatabase original (modo multi-tabela)
156
+ logging.info("[SQL_AGENT] Criando agente em modo multi-tabela")
157
+ db_to_use = db
158
+
159
  # Obtém o ID real do modelo
160
  model_id = AVAILABLE_MODELS.get(model_name, model_name)
161
 
 
182
  )
183
  agent_type = "tool-calling" # Claude usa tool-calling
184
 
185
+ elif model_id in GOOGLE_MODELS:
186
+ # Gemini com tool-calling e configurações otimizadas
187
+ llm = ChatGoogleGenerativeAI(
188
+ model=model_id,
189
+ temperature=TEMPERATURE,
190
+ max_tokens=4096,
191
+ max_retries=2,
192
+ timeout=60.0
193
+ )
194
+ agent_type = "tool-calling" # Gemini usa tool-calling
195
+
196
  else:
197
  # Fallback para OpenAI
198
  llm = ChatOpenAI(
 
202
  agent_type = "openai-tools"
203
  logging.warning(f"Modelo {model_name} não reconhecido, usando gpt-4o-mini como fallback")
204
 
205
+ # Cria o agente SQL
206
  sql_agent = create_sql_agent(
207
  llm=llm,
208
+ db=db_to_use, # Usa o SQLDatabase apropriado (restrito ou completo)
209
  agent_type=agent_type,
210
  verbose=True,
211
  max_iterations=MAX_ITERATIONS,
 
224
  """
225
  Gerenciador do agente SQL com funcionalidades avançadas
226
  """
227
+
228
+ def __init__(self, db: SQLDatabase, model_name: str = "gpt-4o-mini", single_table_mode: bool = False, selected_table: str = None):
229
  self.db = db
230
  self.model_name = model_name
231
+ self.single_table_mode = single_table_mode
232
+ self.selected_table = selected_table
233
  self.agent = None
234
  self._initialize_agent()
235
+
236
  def _initialize_agent(self):
237
  """Inicializa o agente SQL"""
238
+ self.agent = create_sql_agent_executor(self.db, self.model_name, self.single_table_mode, self.selected_table)
239
 
240
+ def recreate_agent(self, new_db: SQLDatabase = None, new_model: str = None, single_table_mode: bool = None, selected_table: str = None):
241
  """
242
  Recria o agente com novos parâmetros
243
+
244
  Args:
245
  new_db: Novo banco de dados (opcional)
246
  new_model: Novo modelo (opcional)
247
+ single_table_mode: Novo modo de tabela (opcional)
248
+ selected_table: Nova tabela selecionada (opcional)
249
  """
250
  if new_db:
251
  self.db = new_db
252
  if new_model:
253
  self.model_name = new_model
254
+ if single_table_mode is not None:
255
+ self.single_table_mode = single_table_mode
256
+ if selected_table is not None:
257
+ self.selected_table = selected_table
258
+
259
  self._initialize_agent()
260
+ mode_info = f"modo {'tabela única' if self.single_table_mode else 'multi-tabela'}"
261
+ logging.info(f"Agente SQL recriado com modelo {self.model_name} em {mode_info}")
262
 
263
  def _extract_text_from_claude_response(self, output) -> str:
264
  """
 
317
  # Criar handler para capturar SQL
318
  sql_handler = SQLQueryCaptureHandler()
319
 
320
+ # Verifica se é agente Claude ou Gemini para aplicar retry
321
  model_id = getattr(self, 'model_name', '')
322
  is_claude = any(claude_model in model_id for claude_model in ANTHROPIC_MODELS)
323
+ is_gemini = any(gemini_model in model_id for gemini_model in GOOGLE_MODELS)
324
 
325
+ if is_claude or is_gemini:
326
+ # Usa retry com backoff para Claude e Gemini
327
  response = await retry_with_backoff(
328
  lambda: self.agent.invoke(
329
  {"input": instruction},
 
381
  "intermediate_steps": [],
382
  "success": False
383
  }
384
+
385
  def get_agent_info(self) -> dict:
386
  """
387
  Retorna informações sobre o agente atual
agents/tools.py CHANGED
@@ -10,6 +10,7 @@ from langchain_community.utilities import SQLDatabase
10
  from langchain_openai import ChatOpenAI
11
  from langchain_anthropic import ChatAnthropic
12
  import pandas as pd
 
13
 
14
  from utils.config import (
15
  HUGGINGFACE_API_KEY,
@@ -102,58 +103,120 @@ def detect_query_type(user_query: str) -> str:
102
  else:
103
  return 'sql_query' # SQL normal
104
 
105
- def prepare_processing_context(user_query: str, db_sample: pd.DataFrame) -> str:
106
  """
107
- Prepara o contexto inicial para o Processing Agent
 
 
 
108
 
109
  Args:
110
  user_query: Pergunta do usuário
111
- db_sample: Amostra dos dados do banco
 
 
 
 
 
112
 
113
  Returns:
114
  Contexto formatado para o Processing Agent
115
  """
116
- # Obtém informações detalhadas das colunas
 
 
 
 
 
 
 
117
  column_info = []
118
- for col in db_sample.columns:
119
- col_data = db_sample[col].dropna()
120
- if len(col_data) > 0:
121
- # Exemplos de valores únicos (máximo 5)
122
- unique_values = col_data.unique()[:5]
123
- examples = ", ".join([str(v) for v in unique_values])
124
-
125
- # Tipo de dados
126
- dtype = str(col_data.dtype)
127
-
128
- # Estatísticas básicas para colunas numéricas
129
- stats = ""
130
- if col_data.dtype in ['int64', 'float64']:
131
- try:
132
- min_val = col_data.min()
133
- max_val = col_data.max()
134
- stats = f" | Min: {min_val}, Max: {max_val}"
135
- except:
136
- pass
137
 
138
- column_info.append(f"- {col} ({dtype}): {examples}{stats}")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
139
 
140
- columns_description = "\n".join(column_info)
141
 
142
- # Adiciona algumas linhas de exemplo dos dados
143
- sample_rows = []
144
- num_rows_to_show = min(3, len(db_sample))
145
- for i in range(num_rows_to_show):
146
- row_data = []
147
- for col in db_sample.columns:
148
- value = db_sample.iloc[i][col]
149
- # Trunca valores muito longos
150
- str_value = str(value)
151
- if len(str_value) > 30:
152
- str_value = str_value[:27] + "..."
153
- row_data.append(f"{col}: {str_value}")
154
- sample_rows.append(" " + " | ".join(row_data))
155
-
156
- sample_data = "\n".join(sample_rows) if sample_rows else "Nenhuma linha de exemplo disponível"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
157
 
158
  context = f"""
159
  Você é um especialista em SQL que deve analisar a pergunta do usuário e gerar uma query SQL otimizada.
@@ -161,24 +224,18 @@ def prepare_processing_context(user_query: str, db_sample: pd.DataFrame) -> str:
161
  INSTRUÇÕES IMPORTANTES:
162
  1. Analise a pergunta do usuário e o contexto dos dados
163
  2. Gere uma query SQL precisa e otimizada
164
- 3. Use apenas as colunas que existem na tabela "tabela"
165
  4. Para cálculos complexos, use CTEs quando necessário
166
  5. Inclua LIMIT quando apropriado para evitar resultados excessivos
167
  6. Considere os tipos de dados e valores de exemplo
168
- 7. O periodo da base é de 18 de janeiro de 2025 a 6 de julho de 2025
169
- 8. Nunca altere o idioma de nomes ou termos da pergunta do usuário
170
- 9. Nosso idioma é Português do Brasil
171
- 10. Sempre mantenha os nomes de colunas de texto em MAISCULO
172
 
173
  CONTEXTO DOS DADOS:
174
- Nome da tabela: tabela
 
175
 
176
- Colunas disponíveis com tipos e exemplos:
177
  {columns_description}
178
 
179
- Exemplos de linhas dos dados:
180
- {sample_data}
181
-
182
  PERGUNTA DO USUÁRIO:
183
  {user_query}
184
 
@@ -209,16 +266,6 @@ def prepare_sql_context(user_query: str, db_sample: pd.DataFrame, suggested_quer
209
  contexto_base = (
210
  "Você é um assistente especializado em consultas SQL, geração de querySQL e análise de dados.\n"
211
  "Sua tarefa é responder à pergunta do usuário abaixo, gerando uma query SQL que retorne os dados necessários para responder a pergunta.\n\n"
212
- """
213
-
214
- REGRAS:
215
-
216
- 1. Sempre mantenha a escrita de nomes ou termos de valores das colunas de texto.
217
- 2. Nosso idioma é Português do Brasil
218
- 3. Sempre mantenha os nomes de colunas de texto em MAIUSCULO
219
- 4. Estamos no ano de 2025
220
- 5. O jeito certo de escrever é Hamburguer e não Hamburger, estamos no Brasil, o correto é Hamburguer
221
- """
222
  )
223
 
224
  # Contexto com opção de query (se disponível)
 
10
  from langchain_openai import ChatOpenAI
11
  from langchain_anthropic import ChatAnthropic
12
  import pandas as pd
13
+ import sqlalchemy as sa
14
 
15
  from utils.config import (
16
  HUGGINGFACE_API_KEY,
 
103
  else:
104
  return 'sql_query' # SQL normal
105
 
106
+ def prepare_processing_context(user_query: str, columns_data: dict, connection_type: str = "csv", single_table_mode: bool = False, selected_table: str = None, available_tables: list = None) -> str:
107
  """
108
+ Prepara o contexto inicial para o Processing Agent de forma dinâmica
109
+
110
+ NOVA IMPLEMENTAÇÃO: Recebe dados já processados pelo processing_node.py
111
+ para evitar consultas redundantes e garantir consistência
112
 
113
  Args:
114
  user_query: Pergunta do usuário
115
+ columns_data: Dados das colunas processados pelo processing_node.py
116
+ Formato: {"table_name": [{"column": "nome", "type": "tipo", "examples": "exemplos", "stats": "estatísticas"}]}
117
+ connection_type: Tipo de conexão ("csv" ou "postgresql")
118
+ single_table_mode: Se está em modo tabela única (PostgreSQL)
119
+ selected_table: Tabela selecionada (PostgreSQL modo único)
120
+ available_tables: Lista de tabelas disponíveis (PostgreSQL)
121
 
122
  Returns:
123
  Contexto formatado para o Processing Agent
124
  """
125
+ logging.info(f"[TOOLS] ===== PREPARANDO CONTEXTO =====")
126
+ logging.info(f"[TOOLS] Connection type: {connection_type}")
127
+ logging.info(f"[TOOLS] Single table mode: {single_table_mode}")
128
+ logging.info(f"[TOOLS] Selected table: {selected_table}")
129
+ logging.info(f"[TOOLS] Available tables: {available_tables}")
130
+ logging.info(f"[TOOLS] Columns data keys: {list(columns_data.keys()) if columns_data else 'None'}")
131
+
132
+ # Processa os dados das colunas baseado no tipo de conexão
133
  column_info = []
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
134
 
135
+ if connection_type.lower() == "postgresql":
136
+ logging.info(f"[TOOLS] 🔵 PROCESSANDO POSTGRESQL")
137
+
138
+ if single_table_mode and selected_table:
139
+ # PostgreSQL - Modo tabela única: usa APENAS dados da tabela selecionada
140
+ logging.info(f"[TOOLS] 🔵 PostgreSQL MODO ÚNICO - tabela: {selected_table}")
141
+
142
+ table_data = columns_data.get(selected_table, [])
143
+ if table_data:
144
+ for col_info in table_data:
145
+ column_line = f"- {col_info['column']} ({col_info['type']})"
146
+ if col_info.get('examples'):
147
+ column_line += f": {col_info['examples']}"
148
+ if col_info.get('stats'):
149
+ column_line += f"{col_info['stats']}"
150
+ column_info.append(column_line)
151
+
152
+ logging.info(f"[TOOLS] ✅ PostgreSQL modo único processado: {len(column_info)} colunas")
153
+ else:
154
+ logging.warning(f"[TOOLS] ⚠️ Nenhum dado encontrado para tabela {selected_table}")
155
+
156
+ else:
157
+ # PostgreSQL - Modo multi-tabela: usa dados de TODAS as tabelas
158
+ logging.info(f"[TOOLS] 🔵 PostgreSQL MODO MULTI-TABELA - {len(columns_data)} tabelas")
159
+
160
+ for table_name, table_columns in columns_data.items():
161
+ column_info.append(f"\n**Tabela: {table_name}**")
162
+
163
+ if table_columns:
164
+ for col_info in table_columns:
165
+ column_line = f"- {col_info['column']} ({col_info['type']})"
166
+ if col_info.get('examples'):
167
+ column_line += f": {col_info['examples']}"
168
+ if col_info.get('stats'):
169
+ column_line += f"{col_info['stats']}"
170
+ column_info.append(column_line)
171
+ else:
172
+ column_info.append("- (Tabela sem dados ou colunas)")
173
 
174
+ logging.info(f"[TOOLS] Tabela {table_name} processada: {len(table_columns)} colunas")
175
 
176
+ logging.info(f"[TOOLS] PostgreSQL multi-tabela processado: {len(column_info)} itens")
177
+
178
+ else:
179
+ # CSV/SQLite - usa APENAS dados da tabela CSV
180
+ logging.info(f"[TOOLS] 🟡 PROCESSANDO CSV/SQLITE")
181
+
182
+ # Para CSV, deve haver apenas uma entrada no columns_data
183
+ for table_name, table_columns in columns_data.items():
184
+ for col_info in table_columns:
185
+ column_line = f"- {col_info['column']} ({col_info['type']})"
186
+ if col_info.get('examples'):
187
+ column_line += f": {col_info['examples']}"
188
+ if col_info.get('stats'):
189
+ column_line += f"{col_info['stats']}"
190
+ column_info.append(column_line)
191
+
192
+ logging.info(f"[TOOLS] ✅ CSV/SQLite processado: {len(column_info)} colunas")
193
+
194
+ columns_description = "\n".join(column_info)
195
+ logging.info(f"[TOOLS] ===== CONTEXTO FINAL =====")
196
+ logging.info(f"[TOOLS] Total de itens no contexto: {len(column_info)}")
197
+ logging.info(f"[TOOLS] ===== FIM PREPARAÇÃO =====")
198
+
199
+ # Determina informações da tabela de forma dinâmica
200
+ if connection_type.lower() == "postgresql":
201
+ if single_table_mode and selected_table:
202
+ # Modo tabela única PostgreSQL
203
+ table_info = f"Nome da tabela: {selected_table}"
204
+ table_instructions = f'Use apenas as colunas que existem na tabela "{selected_table}".'
205
+ context_note = f"MODO TABELA ÚNICA ATIVO - Trabalhando apenas com a tabela '{selected_table}'"
206
+ else:
207
+ # Modo multi-tabela PostgreSQL
208
+ if available_tables:
209
+ tables_list = ", ".join(available_tables)
210
+ table_info = f"Tabelas disponíveis: {tables_list}"
211
+ else:
212
+ table_info = "Múltiplas tabelas disponíveis no PostgreSQL"
213
+ table_instructions = "Use as tabelas disponíveis no PostgreSQL. Pode fazer JOINs entre tabelas quando necessário."
214
+ context_note = "MODO MULTI-TABELA ATIVO - Pode usar todas as tabelas e fazer JOINs"
215
+ else:
216
+ # Para CSV/SQLite, usa tabela padrão
217
+ table_info = "Nome da tabela: tabela"
218
+ table_instructions = 'Use apenas as colunas que existem na tabela "tabela".'
219
+ context_note = "CONEXÃO CSV/SQLite - Dados convertidos para tabela única"
220
 
221
  context = f"""
222
  Você é um especialista em SQL que deve analisar a pergunta do usuário e gerar uma query SQL otimizada.
 
224
  INSTRUÇÕES IMPORTANTES:
225
  1. Analise a pergunta do usuário e o contexto dos dados
226
  2. Gere uma query SQL precisa e otimizada
227
+ 3. {table_instructions}
228
  4. Para cálculos complexos, use CTEs quando necessário
229
  5. Inclua LIMIT quando apropriado para evitar resultados excessivos
230
  6. Considere os tipos de dados e valores de exemplo
 
 
 
 
231
 
232
  CONTEXTO DOS DADOS:
233
+ {context_note}
234
+ {table_info}
235
 
236
+ Colunas disponíveis com tipos e exemplos (baseado na amostra atual):
237
  {columns_description}
238
 
 
 
 
239
  PERGUNTA DO USUÁRIO:
240
  {user_query}
241
 
 
266
  contexto_base = (
267
  "Você é um assistente especializado em consultas SQL, geração de querySQL e análise de dados.\n"
268
  "Sua tarefa é responder à pergunta do usuário abaixo, gerando uma query SQL que retorne os dados necessários para responder a pergunta.\n\n"
 
 
 
 
 
 
 
 
 
 
269
  )
270
 
271
  # Contexto com opção de query (se disponível)
app.py CHANGED
@@ -31,18 +31,23 @@ logging.basicConfig(
31
  # Variáveis globais
32
  graph_manager = None
33
  show_history_flag = False
 
 
34
 
35
  async def initialize_app():
36
  """Inicializa a aplicação"""
37
- global graph_manager
38
-
39
  try:
40
  # Valida configurações
41
  validate_config()
42
-
43
  # Inicializa o grafo
44
  graph_manager = await initialize_graph()
45
 
 
 
 
46
  # Informa sobre o status do LangSmith
47
  if is_langsmith_enabled():
48
  logging.info(f"✅ LangSmith habilitado - Projeto: '{LANGSMITH_PROJECT}'")
@@ -67,7 +72,7 @@ def run_async(coro):
67
 
68
  return loop.run_until_complete(coro)
69
 
70
- def chatbot_response(user_input: str, selected_model: str, advanced_mode: bool = False, processing_enabled: bool = False, processing_model: str = "GPT-4o-mini") -> Tuple[str, Optional[str]]:
71
  """
72
  Processa resposta do chatbot usando LangGraph
73
 
@@ -77,6 +82,10 @@ def chatbot_response(user_input: str, selected_model: str, advanced_mode: bool =
77
  advanced_mode: Se deve usar refinamento avançado
78
  processing_enabled: Se o Processing Agent está habilitado
79
  processing_model: Modelo para o Processing Agent
 
 
 
 
80
 
81
  Returns:
82
  Tupla com (resposta_texto, caminho_imagem_grafico)
@@ -93,7 +102,11 @@ def chatbot_response(user_input: str, selected_model: str, advanced_mode: bool =
93
  selected_model=selected_model,
94
  advanced_mode=advanced_mode,
95
  processing_enabled=processing_enabled,
96
- processing_model=processing_model
 
 
 
 
97
  ))
98
 
99
  response_text = result.get("response", "Erro ao processar resposta")
@@ -146,7 +159,7 @@ def save_graph_image_to_temp(graph_image_id: str) -> Optional[str]:
146
 
147
  def handle_csv_upload(file) -> str:
148
  """
149
- Processa upload de arquivo CSV
150
 
151
  Args:
152
  file: Arquivo enviado pelo Gradio
@@ -173,9 +186,9 @@ def handle_csv_upload(file) -> str:
173
  if not os.path.exists(file.name):
174
  return f"❌ Arquivo não encontrado: {file.name}"
175
 
176
- # Verifica se é um arquivo CSV
177
  if not file.name.lower().endswith('.csv'):
178
- return "❌ Por favor, selecione um arquivo CSV válido."
179
 
180
  # Verifica o tamanho do arquivo
181
  file_size = os.path.getsize(file.name)
@@ -238,6 +251,73 @@ def reset_system() -> str:
238
  logging.error(error_msg)
239
  return error_msg
240
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
241
  def toggle_advanced_mode(enabled: bool) -> str:
242
  """
243
  Alterna modo avançado
@@ -266,7 +346,7 @@ def toggle_history():
266
  else:
267
  return {}
268
 
269
- 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"):
270
  """
271
  Função de resposta para o chatbot Gradio
272
 
@@ -277,6 +357,10 @@ def respond(message: str, chat_history: List[Dict[str, str]], selected_model: st
277
  advanced_mode: Modo avançado habilitado
278
  processing_enabled: Se o Processing Agent está habilitado
279
  processing_model: Modelo para o Processing Agent
 
 
 
 
280
 
281
  Returns:
282
  Tupla com (mensagem_vazia, histórico_atualizado, imagem_grafico)
@@ -294,7 +378,7 @@ def respond(message: str, chat_history: List[Dict[str, str]], selected_model: st
294
  return "", chat_history, None
295
 
296
  # Processa resposta
297
- response, graph_image_path = chatbot_response(message, selected_model, advanced_mode, processing_enabled, processing_model)
298
 
299
  # Atualiza histórico no formato messages
300
  chat_history.append({"role": "user", "content": message})
@@ -304,16 +388,126 @@ def respond(message: str, chat_history: List[Dict[str, str]], selected_model: st
304
 
305
  def handle_csv_and_clear_chat(file):
306
  """
307
- Processa CSV e limpa chat
308
 
309
  Args:
310
- file: Arquivo CSV
311
 
312
  Returns:
313
- Tupla com (feedback, chat_limpo, grafico_limpo)
314
  """
 
 
 
 
 
 
 
 
 
 
315
  feedback = handle_csv_upload(file)
316
- return feedback, [], gr.update(visible=False)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
317
 
318
  def reset_all():
319
  """
@@ -329,11 +523,121 @@ def reset_all():
329
  def create_interface():
330
  """Cria interface Gradio"""
331
 
332
- # CSS customizado para pequeno espaçamento lateral
333
  custom_css = """
334
  .gradio-container {
335
  padding: 20px 30px !important;
336
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
337
  """
338
 
339
  with gr.Blocks(theme=gr.themes.Soft(), css=custom_css) as demo:
@@ -341,30 +645,142 @@ def create_interface():
341
  with gr.Row():
342
  with gr.Column(scale=1):
343
  gr.Markdown("## Configurações")
344
- model_selector = gr.Dropdown(list(AVAILABLE_MODELS.keys()), value=DEFAULT_MODEL, label="")
345
- csv_file = gr.File(file_types=[".csv"], label="")
346
- upload_feedback = gr.Markdown()
347
- advanced_checkbox = gr.Checkbox(label="Refinar Resposta")
348
-
349
- # Controles do Processing Agent
350
- processing_checkbox = gr.Checkbox(label="Usar Processing Agent", value=False)
351
- processing_model_selector = gr.Dropdown(
352
- choices=list(AVAILABLE_MODELS.keys()) + list(REFINEMENT_MODELS.keys()),
353
- value="GPT-4o-mini", # Chave correta do AVAILABLE_MODELS
354
- label="Modelo do Processing Agent",
355
- visible=False
356
- )
357
-
358
- # Status do LangSmith
359
- if is_langsmith_enabled():
360
- gr.Markdown(f"🔍 **LangSmith**: Ativo")
361
- else:
362
- gr.Markdown("🔍 **LangSmith**: Desabilitado")
363
 
364
- reset_btn = gr.Button("Resetar")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
365
 
366
  with gr.Column(scale=4):
367
- gr.Markdown("## Seara Agent")
368
  chatbot = gr.Chatbot(
369
  height=600,
370
  show_label=False,
@@ -390,10 +806,38 @@ def create_interface():
390
 
391
  download_file = gr.File(visible=False)
392
 
 
 
 
 
 
 
 
 
 
393
  # Event handlers (usando as funções originais do sistema)
394
- def handle_response_with_graph(message, chat_history, model, advanced, processing_enabled, processing_model):
395
  """Wrapper para lidar com resposta e gráfico"""
396
- empty_msg, updated_history, graph_path = respond(message, chat_history, model, advanced, processing_enabled, processing_model)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
397
 
398
  # Controla visibilidade do componente de gráfico
399
  if graph_path:
@@ -405,22 +849,147 @@ def create_interface():
405
  """Controla visibilidade do seletor de modelo do Processing Agent"""
406
  return gr.update(visible=enabled)
407
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
408
  msg.submit(
409
  handle_response_with_graph,
410
- inputs=[msg, chatbot, model_selector, advanced_checkbox, processing_checkbox, processing_model_selector],
411
- outputs=[msg, chatbot, graph_image]
 
412
  )
413
 
414
  btn.click(
415
  handle_response_with_graph,
416
- inputs=[msg, chatbot, model_selector, advanced_checkbox, processing_checkbox, processing_model_selector],
417
  outputs=[msg, chatbot, graph_image]
418
  )
419
 
420
  csv_file.change(
421
  handle_csv_and_clear_chat,
422
  inputs=csv_file,
423
- outputs=[upload_feedback, chatbot, graph_image]
 
424
  )
425
 
426
  reset_btn.click(
@@ -444,6 +1013,33 @@ def create_interface():
444
  inputs=processing_checkbox,
445
  outputs=processing_model_selector
446
  )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
447
 
448
  return demo
449
 
@@ -465,9 +1061,17 @@ async def main():
465
  for port in ports_to_try:
466
  try:
467
  logging.info(f"Tentando iniciar interface Gradio na porta {port}")
 
 
 
 
 
 
 
468
  demo.launch(
469
- share=GRADIO_SHARE,
470
  server_port=port if port != 0 else None,
 
471
  show_error=True,
472
  quiet=False
473
  )
 
31
  # Variáveis globais
32
  graph_manager = None
33
  show_history_flag = False
34
+ connection_ready = False # Controla se a conexão está pronta para uso
35
+ chat_blocked = False # Controla se o chat está bloqueado durante carregamento
36
 
37
  async def initialize_app():
38
  """Inicializa a aplicação"""
39
+ global graph_manager, connection_ready
40
+
41
  try:
42
  # Valida configurações
43
  validate_config()
44
+
45
  # Inicializa o grafo
46
  graph_manager = await initialize_graph()
47
 
48
+ # Inicializa como conectado (base padrão já carregada)
49
+ connection_ready = True
50
+
51
  # Informa sobre o status do LangSmith
52
  if is_langsmith_enabled():
53
  logging.info(f"✅ LangSmith habilitado - Projeto: '{LANGSMITH_PROJECT}'")
 
72
 
73
  return loop.run_until_complete(coro)
74
 
75
+ 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]]:
76
  """
77
  Processa resposta do chatbot usando LangGraph
78
 
 
82
  advanced_mode: Se deve usar refinamento avançado
83
  processing_enabled: Se o Processing Agent está habilitado
84
  processing_model: Modelo para o Processing Agent
85
+ connection_type: Tipo de conexão ("csv" ou "postgresql")
86
+ postgresql_config: Configuração postgresql (se aplicável)
87
+ selected_table: Tabela selecionada (para postgresql)
88
+ single_table_mode: Se deve usar apenas uma tabela (postgresql)
89
 
90
  Returns:
91
  Tupla com (resposta_texto, caminho_imagem_grafico)
 
102
  selected_model=selected_model,
103
  advanced_mode=advanced_mode,
104
  processing_enabled=processing_enabled,
105
+ processing_model=processing_model,
106
+ connection_type=connection_type,
107
+ postgresql_config=postgresql_config,
108
+ selected_table=selected_table,
109
+ single_table_mode=single_table_mode
110
  ))
111
 
112
  response_text = result.get("response", "Erro ao processar resposta")
 
159
 
160
  def handle_csv_upload(file) -> str:
161
  """
162
+ Processa upload de arquivo csv
163
 
164
  Args:
165
  file: Arquivo enviado pelo Gradio
 
186
  if not os.path.exists(file.name):
187
  return f"❌ Arquivo não encontrado: {file.name}"
188
 
189
+ # Verifica se é um arquivo csv
190
  if not file.name.lower().endswith('.csv'):
191
+ return "❌ Por favor, selecione um arquivo csv válido."
192
 
193
  # Verifica o tamanho do arquivo
194
  file_size = os.path.getsize(file.name)
 
251
  logging.error(error_msg)
252
  return error_msg
253
 
254
+ def handle_postgresql_connection(host: str, port: str, database: str, username: str, password: str) -> str:
255
+ """
256
+ Processa conexão postgresql
257
+
258
+ Args:
259
+ host: Host do postgresql
260
+ port: Porta do postgresql
261
+ database: Nome do banco
262
+ username: Nome de usuário
263
+ password: Senha
264
+
265
+ Returns:
266
+ Mensagem de feedback
267
+ """
268
+ global graph_manager
269
+
270
+ if not graph_manager:
271
+ return "❌ Sistema não inicializado."
272
+
273
+ try:
274
+ # Valida campos obrigatórios
275
+ if not all([host, port, database, username, password]):
276
+ return "❌ Todos os campos são obrigatórios para conexão postgresql."
277
+
278
+ # Valida porta
279
+ try:
280
+ port_int = int(port)
281
+ if port_int < 1 or port_int > 65535:
282
+ return "❌ Porta deve estar entre 1 e 65535."
283
+ except ValueError:
284
+ return "❌ Porta deve ser um número válido."
285
+
286
+ # Prepara configuração postgresql
287
+ postgresql_config = {
288
+ "host": host.strip(),
289
+ "port": port_int,
290
+ "database": database.strip(),
291
+ "username": username.strip(),
292
+ "password": password
293
+ }
294
+
295
+ # Cria estado inicial para a conexão
296
+ initial_state = {
297
+ "user_input": "Conectar postgresql",
298
+ "selected_model": "gpt-4o-mini",
299
+ "advanced_mode": False,
300
+ "processing_enabled": False,
301
+ "processing_model": "gpt-4o-mini",
302
+ "connection_type": "postgresql",
303
+ "postgresql_config": postgresql_config,
304
+ "selected_table": None,
305
+ "single_table_mode": False
306
+ }
307
+
308
+ # Processa conexão através do LangGraph
309
+ logging.info(f"[POSTGRESQL] Iniciando conexão: {host}:{port}/{database}")
310
+ result = run_async(graph_manager.handle_postgresql_connection(initial_state))
311
+
312
+ logging.info(f"[POSTGRESQL] Resultado da conexão: {result}")
313
+ return result.get("message", "Erro na conexão postgresql")
314
+
315
+ except Exception as e:
316
+ error_msg = f"❌ Erro ao conectar postgresql: {e}"
317
+ logging.error(error_msg)
318
+ logging.error(f"[POSTGRESQL] Detalhes do erro: {type(e).__name__}: {str(e)}")
319
+ return error_msg
320
+
321
  def toggle_advanced_mode(enabled: bool) -> str:
322
  """
323
  Alterna modo avançado
 
346
  else:
347
  return {}
348
 
349
+ 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):
350
  """
351
  Função de resposta para o chatbot Gradio
352
 
 
357
  advanced_mode: Modo avançado habilitado
358
  processing_enabled: Se o Processing Agent está habilitado
359
  processing_model: Modelo para o Processing Agent
360
+ connection_type: Tipo de conexão ("csv" ou "postgresql")
361
+ postgresql_config: Configuração postgresql (se aplicável)
362
+ selected_table: Tabela selecionada (para postgresql)
363
+ single_table_mode: Se deve usar apenas uma tabela (postgresql)
364
 
365
  Returns:
366
  Tupla com (mensagem_vazia, histórico_atualizado, imagem_grafico)
 
378
  return "", chat_history, None
379
 
380
  # Processa resposta
381
+ response, graph_image_path = chatbot_response(message, selected_model, advanced_mode, processing_enabled, processing_model, connection_type, postgresql_config, selected_table, single_table_mode)
382
 
383
  # Atualiza histórico no formato messages
384
  chat_history.append({"role": "user", "content": message})
 
388
 
389
  def handle_csv_and_clear_chat(file):
390
  """
391
+ Processa csv e limpa chat com indicador de carregamento melhorado
392
 
393
  Args:
394
+ file: Arquivo csv
395
 
396
  Returns:
397
+ Tupla com (feedback, chat_limpo, grafico_limpo, status)
398
  """
399
+ global connection_ready
400
+
401
+ if file is None:
402
+ connection_ready = False
403
+ return "", [], gr.update(visible=False), "**Status**: <span class='status-error'>Nenhum arquivo selecionado</span>"
404
+
405
+ # Indica carregamento
406
+ connection_ready = False
407
+
408
+ # Processa arquivo
409
  feedback = handle_csv_upload(file)
410
+
411
+ # Status final baseado no resultado
412
+ if "✅" in feedback:
413
+ connection_ready = True
414
+ final_status = "**Status**: <span class='status-connected'>csv processado com sucesso</span>"
415
+ else:
416
+ connection_ready = False
417
+ final_status = "**Status**: <span class='status-error'>Erro no processamento do csv</span>"
418
+
419
+ return feedback, [], gr.update(visible=False), final_status
420
+
421
+ def is_connection_ready(conn_type, pg_host=None, pg_port=None, pg_db=None, pg_user=None, pg_pass=None):
422
+ """
423
+ Verifica se há uma conexão de dados ativa e pronta para uso
424
+
425
+ Args:
426
+ conn_type: Tipo de conexão ("csv" ou "postgresql")
427
+ pg_host, pg_port, pg_db, pg_user, pg_pass: Credenciais postgresql
428
+
429
+ Returns:
430
+ True se conexão está pronta, False caso contrário
431
+ """
432
+ global connection_ready, chat_blocked
433
+ return connection_ready and not chat_blocked
434
+
435
+ def show_loading_in_chat(message):
436
+ """
437
+ Mostra mensagem de carregamento apenas no chat
438
+
439
+ Args:
440
+ message: Mensagem de carregamento
441
+
442
+ Returns:
443
+ Histórico atualizado com mensagem de carregamento
444
+ """
445
+ global chat_blocked
446
+ chat_blocked = True
447
+
448
+ return [
449
+ {"role": "user", "content": "Alterando tipo de conexão..."},
450
+ {"role": "assistant", "content": f"🔄 {message}"}
451
+ ]
452
+
453
+ def clear_loading_from_chat():
454
+ """
455
+ Remove carregamento do chat
456
+ """
457
+ global chat_blocked
458
+ chat_blocked = False
459
+
460
+ def load_default_csv_and_cleanup_postgresql():
461
+ """
462
+ Carrega a base csv padrão e limpa conexões postgresql ativas
463
+
464
+ Returns:
465
+ Mensagem de feedback sobre a operação
466
+ """
467
+ global connection_ready
468
+
469
+ try:
470
+ from utils.config import DEFAULT_CSV_PATH
471
+ from utils.object_manager import get_object_manager
472
+ import os
473
+
474
+ # Verifica se o arquivo padrão existe
475
+ if not os.path.exists(DEFAULT_CSV_PATH):
476
+ connection_ready = False
477
+ return "Arquivo csv padrão (tabela.csv) não encontrado"
478
+
479
+ # Limpa conexões postgresql ativas
480
+ obj_manager = get_object_manager()
481
+
482
+ # Fecha engines postgresql (SQLAlchemy engines têm método dispose)
483
+ for engine_id, engine in obj_manager._engines.items():
484
+ try:
485
+ if hasattr(engine, 'dispose'):
486
+ engine.dispose()
487
+ logging.info(f"[CLEANUP] Engine postgresql {engine_id} fechada")
488
+ except Exception as e:
489
+ logging.warning(f"[CLEANUP] Erro ao fechar engine {engine_id}: {e}")
490
+
491
+ # Limpa objetos postgresql do ObjectManager
492
+ obj_manager.clear_all()
493
+ logging.info("[CLEANUP] Objetos postgresql limpos do ObjectManager")
494
+
495
+ # Carrega csv padrão através do LangGraph
496
+ logging.info(f"[CSV_DEFAULT] Carregando arquivo padrão: {DEFAULT_CSV_PATH}")
497
+ result = run_async(graph_manager.handle_csv_upload(DEFAULT_CSV_PATH))
498
+
499
+ if result.get("success", False):
500
+ connection_ready = True
501
+ return f"✅ Base padrão carregada: {os.path.basename(DEFAULT_CSV_PATH)}"
502
+ else:
503
+ connection_ready = False
504
+ return f"Erro ao carregar base padrão: {result.get('message', 'Erro desconhecido')}"
505
+
506
+ except Exception as e:
507
+ connection_ready = False
508
+ error_msg = f"Erro ao carregar base padrão: {e}"
509
+ logging.error(f"[CSV_DEFAULT] {error_msg}")
510
+ return error_msg
511
 
512
  def reset_all():
513
  """
 
523
  def create_interface():
524
  """Cria interface Gradio"""
525
 
526
+ # CSS customizado para interface limpa e moderna
527
  custom_css = """
528
  .gradio-container {
529
  padding: 20px 30px !important;
530
  }
531
+
532
+ /* Seções de configuração */
533
+ .config-section {
534
+ background: #f8f9fa;
535
+ border-radius: 15px;
536
+ padding: 0;
537
+ margin: 16px 0;
538
+ overflow: hidden;
539
+ }
540
+
541
+ /* Headers dos containers com espaçamento adequado */
542
+ .gradio-container h3 {
543
+ margin: 0 !important;
544
+ color: #f1f3f4 !important;
545
+ font-size: 16px !important;
546
+ font-weight: 600 !important;
547
+ }
548
+
549
+ /* Espaçamento para status e informações nos containers */
550
+ .config-section .status-connected,
551
+ .config-section .status-loading,
552
+ .config-section .status-error,
553
+ .config-section .status-waiting {
554
+ padding: 8px 20px !important;
555
+ display: block !important;
556
+ }
557
+
558
+ .prose.svelte-lag733 {
559
+ padding: 12px 20px !important;
560
+ margin: 0 !important;
561
+ }
562
+
563
+ /* Conteúdo dos containers */
564
+ .config-content {
565
+ padding: 20px;
566
+ }
567
+
568
+ /* Status indicators melhorados */
569
+ .status-connected {
570
+ color: #28a745;
571
+ font-weight: 600;
572
+ display: inline-flex;
573
+ align-items: center;
574
+ gap: 8px;
575
+ }
576
+
577
+ .status-loading {
578
+ color: #ffc107;
579
+ font-weight: 600;
580
+ display: inline-flex;
581
+ align-items: center;
582
+ gap: 8px;
583
+ }
584
+
585
+ .status-loading::before {
586
+ content: "⏳";
587
+ animation: pulse 1.5s infinite;
588
+ }
589
+
590
+ .status-error {
591
+ color: #dc3545;
592
+ font-weight: 600;
593
+ display: inline-flex;
594
+ align-items: center;
595
+ gap: 8px;
596
+ }
597
+
598
+ .status-waiting {
599
+ color: #6c757d;
600
+ font-weight: 500;
601
+ display: inline-flex;
602
+ align-items: center;
603
+ gap: 8px;
604
+ }
605
+
606
+ /* Animação de carregamento */
607
+ @keyframes pulse {
608
+ 0%, 100% { opacity: 1; }
609
+ 50% { opacity: 0.5; }
610
+ }
611
+
612
+ @keyframes spin {
613
+ 0% { transform: rotate(0deg); }
614
+ 100% { transform: rotate(360deg); }
615
+ }
616
+
617
+ /* Espaçamentos internos */
618
+ .gr-form {
619
+ padding: 16px;
620
+ }
621
+
622
+ .gr-box {
623
+ padding: 16px;
624
+ margin: 12px 0;
625
+ }
626
+
627
+ /* Melhorias para seção postgresql */
628
+ .pg-section {
629
+ background: #f8f9fa;
630
+ border-radius: 8px;
631
+ padding: 16px;
632
+ margin: 12px 0;
633
+ }
634
+
635
+ .pg-feedback {
636
+ padding: 12px;
637
+ margin: 8px 0;
638
+ border-radius: 6px;
639
+ background: #f1f3f4;
640
+ }
641
  """
642
 
643
  with gr.Blocks(theme=gr.themes.Soft(), css=custom_css) as demo:
 
645
  with gr.Row():
646
  with gr.Column(scale=1):
647
  gr.Markdown("## Configurações")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
648
 
649
+ # 1. CONEXÃO DE DADOS
650
+ with gr.Group():
651
+ gr.Markdown("### Conexão de Dados")
652
+
653
+ with gr.Group():
654
+ connection_type = gr.Radio(
655
+ choices=[("CSV", "csv"), ("PostgreSQL", "postgresql")],
656
+ value="csv",
657
+ label="Tipo de Conexão"
658
+ )
659
+
660
+ # Status da conexão
661
+ connection_status = gr.Markdown("**Status**: <span class='status-connected'>Base padrão carregada</span>")
662
+
663
+ # Seção csv
664
+ with gr.Group(visible=True) as csv_section:
665
+ csv_file = gr.File(
666
+ file_types=[".csv"],
667
+ label="Arquivo csv"
668
+ )
669
+ upload_feedback = gr.Markdown()
670
+
671
+ # Seção postgresql
672
+ with gr.Group(visible=False) as postgresql_section:
673
+ with gr.Group():
674
+ with gr.Row():
675
+ pg_host = gr.Textbox(
676
+ label="Host",
677
+ placeholder="localhost",
678
+ scale=2
679
+ )
680
+ pg_port = gr.Textbox(
681
+ label="Porta",
682
+ value="5432",
683
+ placeholder="5432",
684
+ scale=1
685
+ )
686
+
687
+ pg_database = gr.Textbox(
688
+ label="Banco de Dados",
689
+ placeholder="nome_do_banco"
690
+ )
691
+
692
+ with gr.Row():
693
+ pg_username = gr.Textbox(
694
+ label="Usuário",
695
+ placeholder="usuario",
696
+ scale=1
697
+ )
698
+ pg_password = gr.Textbox(
699
+ label="Senha",
700
+ type="password",
701
+ placeholder="senha",
702
+ scale=1
703
+ )
704
+
705
+ pg_connect_btn = gr.Button(
706
+ "Conectar postgresql",
707
+ variant="primary",
708
+ size="lg"
709
+ )
710
+
711
+ pg_feedback = gr.Markdown()
712
+
713
+ # Configuração de tabelas (visível após conexão)
714
+ with gr.Group(visible=False) as pg_table_section:
715
+ gr.Markdown("#### Configuração de Tabelas")
716
+
717
+ with gr.Group():
718
+ pg_single_table_mode = gr.Checkbox(
719
+ label="Modo Tabela Única",
720
+ value=False
721
+ )
722
+
723
+ # Seletor de tabela
724
+ with gr.Group(visible=False) as pg_table_selector_group:
725
+ pg_table_selector = gr.Dropdown(
726
+ choices=[],
727
+ label="Selecionar Tabela",
728
+ interactive=True
729
+ )
730
+
731
+ pg_table_info = gr.Markdown()
732
+
733
+ # 2. CONFIGURAÇÃO DE MODELOS
734
+ with gr.Group():
735
+ gr.Markdown("### Configuração de Agentes")
736
+
737
+ with gr.Group():
738
+ # Processing Agent
739
+ processing_checkbox = gr.Checkbox(
740
+ label="Processing Agent",
741
+ value=False
742
+ )
743
+ processing_model_selector = gr.Dropdown(
744
+ choices=list(AVAILABLE_MODELS.keys()) + list(REFINEMENT_MODELS.keys()),
745
+ value="GPT-4o-mini",
746
+ label="Modelo do Processing Agent",
747
+ visible=False
748
+ )
749
+
750
+ # Modelo principal SQL
751
+ model_selector = gr.Dropdown(
752
+ list(AVAILABLE_MODELS.keys()),
753
+ value=DEFAULT_MODEL,
754
+ label="Modelo SQL Principal"
755
+ )
756
+
757
+ # 3. CONFIGURAÇÕES AVANÇADAS
758
+ with gr.Group():
759
+ gr.Markdown("### Configurações Avançadas")
760
+
761
+ with gr.Group():
762
+ advanced_checkbox = gr.Checkbox(
763
+ label="Refinar Resposta"
764
+ )
765
+
766
+ # 4. STATUS E CONTROLES
767
+ with gr.Group():
768
+ gr.Markdown("### Status do Sistema")
769
+
770
+ with gr.Group():
771
+ # Status do LangSmith
772
+ if is_langsmith_enabled():
773
+ gr.Markdown(f"**LangSmith**: Ativo")
774
+ else:
775
+ gr.Markdown("**LangSmith**: Desabilitado")
776
+
777
+ reset_btn = gr.Button(
778
+ "Resetar Sistema",
779
+ variant="secondary"
780
+ )
781
 
782
  with gr.Column(scale=4):
783
+ gr.Markdown("## Agent86")
784
  chatbot = gr.Chatbot(
785
  height=600,
786
  show_label=False,
 
806
 
807
  download_file = gr.File(visible=False)
808
 
809
+ # Função para mostrar carregamento de transição no chat
810
+ def show_transition_loading(conn_type):
811
+ """Mostra carregamento de transição apenas no chat"""
812
+ if conn_type == "csv":
813
+ loading_chat = show_loading_in_chat("Fechando postgresql e carregando base csv padrão...")
814
+ return "", loading_chat, gr.update(visible=False)
815
+ else:
816
+ return "", [], gr.update(visible=False)
817
+
818
  # Event handlers (usando as funções originais do sistema)
819
+ 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):
820
  """Wrapper para lidar com resposta e gráfico"""
821
+
822
+ # Verifica se há conexão ativa antes de processar
823
+ if not is_connection_ready(conn_type, pg_host, pg_port, pg_db, pg_user, pg_pass):
824
+ error_msg = "⚠️ **Aguarde**: Configure e conecte a uma fonte de dados antes de fazer perguntas."
825
+ chat_history.append({"role": "user", "content": message})
826
+ chat_history.append({"role": "assistant", "content": error_msg})
827
+ return "", chat_history, gr.update(visible=False)
828
+
829
+ # Prepara configuração postgresql se necessário
830
+ postgresql_config = None
831
+ if conn_type == "postgresql":
832
+ postgresql_config = {
833
+ "host": pg_host,
834
+ "port": pg_port,
835
+ "database": pg_db,
836
+ "username": pg_user,
837
+ "password": pg_pass
838
+ }
839
+
840
+ 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)
841
 
842
  # Controla visibilidade do componente de gráfico
843
  if graph_path:
 
849
  """Controla visibilidade do seletor de modelo do Processing Agent"""
850
  return gr.update(visible=enabled)
851
 
852
+ def toggle_connection_type(conn_type):
853
+ """Controla visibilidade das seções de conexão - FECHA POSTGRES IMEDIATAMENTE"""
854
+ global connection_ready
855
+
856
+ if conn_type == "csv":
857
+ # PRIMEIRO: Fecha container postgresql imediatamente
858
+ # SEGUNDO: Executa transição em background
859
+ feedback_msg = load_default_csv_and_cleanup_postgresql()
860
+ if "✅" in feedback_msg:
861
+ connection_ready = True
862
+ status_msg = "**Status**: <span class='status-connected'>Base padrão carregada</span>"
863
+ else:
864
+ connection_ready = False
865
+ status_msg = "**Status**: <span class='status-error'>Erro na conexão</span>"
866
+
867
+ return (
868
+ gr.update(visible=True), # csv_section - MOSTRA IMEDIATAMENTE
869
+ gr.update(visible=False), # postgresql_section - FECHA IMEDIATAMENTE
870
+ feedback_msg, # upload_feedback
871
+ status_msg, # connection_status
872
+ # Limpa campos postgresql IMEDIATAMENTE
873
+ gr.update(value=""), # pg_host
874
+ gr.update(value="5432"), # pg_port
875
+ gr.update(value=""), # pg_database
876
+ gr.update(value=""), # pg_username
877
+ gr.update(value=""), # pg_password
878
+ gr.update(value=""), # pg_feedback
879
+ gr.update(visible=False), # pg_table_section
880
+ gr.update(value=False), # pg_single_table_mode
881
+ gr.update(visible=False), # pg_table_selector_group
882
+ gr.update(choices=[], value=None), # pg_table_selector
883
+ gr.update(value="") # pg_table_info
884
+ )
885
+
886
+ else: # postgresql
887
+ connection_ready = False
888
+ status_msg = "**Status**: <span class='status-waiting'>Aguardando configuração postgresql</span>"
889
+ return (
890
+ gr.update(visible=False), # csv_section
891
+ gr.update(visible=True), # postgresql_section
892
+ "", # upload_feedback
893
+ status_msg, # connection_status
894
+ # Mantém campos postgresql como estão
895
+ gr.update(), # pg_host
896
+ gr.update(), # pg_port
897
+ gr.update(), # pg_database
898
+ gr.update(), # pg_username
899
+ gr.update(), # pg_password
900
+ gr.update(), # pg_feedback
901
+ gr.update(), # pg_table_section
902
+ gr.update(), # pg_single_table_mode
903
+ gr.update(), # pg_table_selector_group
904
+ gr.update(), # pg_table_selector
905
+ gr.update() # pg_table_info
906
+ )
907
+
908
+ def handle_postgresql_connect(host, port, database, username, password):
909
+ """Wrapper para conexão postgresql"""
910
+ global connection_ready
911
+
912
+ # Executa conexão
913
+ connection_ready = False
914
+ result = handle_postgresql_connection(host, port, database, username, password)
915
+
916
+ # Se conexão foi bem-sucedida, retorna tabelas disponíveis
917
+ if "✅" in result:
918
+ connection_ready = True
919
+ try:
920
+ # Obtém tabelas do ObjectManager
921
+ from utils.object_manager import get_object_manager
922
+ obj_manager = get_object_manager()
923
+
924
+ # Busca metadados de conexão mais recente
925
+ all_metadata = obj_manager.get_all_connection_metadata()
926
+ if all_metadata:
927
+ latest_metadata = list(all_metadata.values())[-1]
928
+ tables = latest_metadata.get("tables", [])
929
+
930
+ # Status de sucesso
931
+ success_status = "**Status**: <span class='status-connected'>postgresql conectado com sucesso</span>"
932
+ table_info = f"**Modo Multi-Tabela ativo** - {len(tables)} tabelas disponíveis"
933
+
934
+ # Retorna resultado + atualização do seletor
935
+ return (
936
+ f"✅ **Conectado com sucesso!** {len(tables)} tabelas encontradas", # feedback
937
+ gr.update(visible=True), # pg_table_section
938
+ False, # pg_single_table_mode (padrão desativado)
939
+ gr.update(visible=False), # pg_table_selector_group (oculto por padrão)
940
+ gr.update(choices=tables, value=tables[0] if tables else None), # pg_table_selector
941
+ table_info, # pg_table_info
942
+ success_status # connection_status
943
+ )
944
+ except Exception as e:
945
+ logging.error(f"Erro ao obter tabelas: {e}")
946
+
947
+ # Se falhou, mantém seção de tabela oculta
948
+ connection_ready = False
949
+ error_status = "**Status**: <span class='status-error'>Falha na conexão postgresql</span>"
950
+ return (
951
+ result, # feedback
952
+ gr.update(visible=False), # pg_table_section
953
+ False, # pg_single_table_mode
954
+ gr.update(visible=False), # pg_table_selector_group
955
+ gr.update(choices=[], value=None), # pg_table_selector
956
+ "", # pg_table_info
957
+ error_status # connection_status
958
+ )
959
+
960
+ def toggle_table_mode(single_mode_enabled, current_table):
961
+ """Alterna entre modo multi-tabela e tabela única"""
962
+ if single_mode_enabled:
963
+ # Modo tabela única ativado
964
+ return (
965
+ gr.update(visible=True), # pg_table_selector_group
966
+ f"**Modo Tabela Única ativo** - Usando: {current_table or 'Selecione uma tabela'}"
967
+ )
968
+ else:
969
+ # Modo multi-tabela ativado
970
+ return (
971
+ gr.update(visible=False), # pg_table_selector_group
972
+ "**Modo Multi-Tabela ativo** - Pode usar todas as tabelas e fazer JOINs"
973
+ )
974
+
975
  msg.submit(
976
  handle_response_with_graph,
977
+ 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],
978
+ outputs=[msg, chatbot, graph_image],
979
+ show_progress=True # Mostra carregamento no input do chat
980
  )
981
 
982
  btn.click(
983
  handle_response_with_graph,
984
+ 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],
985
  outputs=[msg, chatbot, graph_image]
986
  )
987
 
988
  csv_file.change(
989
  handle_csv_and_clear_chat,
990
  inputs=csv_file,
991
+ outputs=[upload_feedback, chatbot, graph_image, connection_status],
992
+ show_progress="minimal" # Mostra carregamento mínimo
993
  )
994
 
995
  reset_btn.click(
 
1013
  inputs=processing_checkbox,
1014
  outputs=processing_model_selector
1015
  )
1016
+
1017
+ # Executa toggle imediatamente (sem carregamento nos campos)
1018
+ connection_type.change(
1019
+ toggle_connection_type,
1020
+ inputs=connection_type,
1021
+ outputs=[
1022
+ csv_section, postgresql_section, upload_feedback, connection_status,
1023
+ pg_host, pg_port, pg_database, pg_username, pg_password, pg_feedback,
1024
+ pg_table_section, pg_single_table_mode, pg_table_selector_group,
1025
+ pg_table_selector, pg_table_info
1026
+ ],
1027
+ show_progress=False # Não mostra carregamento nos campos
1028
+ )
1029
+
1030
+ pg_connect_btn.click(
1031
+ handle_postgresql_connect,
1032
+ inputs=[pg_host, pg_port, pg_database, pg_username, pg_password],
1033
+ outputs=[pg_feedback, pg_table_section, pg_single_table_mode, pg_table_selector_group, pg_table_selector, pg_table_info, connection_status],
1034
+ show_progress="minimal" # Mostra carregamento mínimo
1035
+ )
1036
+
1037
+ # Event handler para toggle de modo de tabela
1038
+ pg_single_table_mode.change(
1039
+ toggle_table_mode,
1040
+ inputs=[pg_single_table_mode, pg_table_selector],
1041
+ outputs=[pg_table_selector_group, pg_table_info]
1042
+ )
1043
 
1044
  return demo
1045
 
 
1061
  for port in ports_to_try:
1062
  try:
1063
  logging.info(f"Tentando iniciar interface Gradio na porta {port}")
1064
+
1065
+ # Configurações para Docker
1066
+ server_name = "0.0.0.0" if GRADIO_SHARE else "127.0.0.1"
1067
+
1068
+ if GRADIO_SHARE:
1069
+ logging.info("🌐 Configurando link público do Gradio...")
1070
+
1071
  demo.launch(
1072
+ server_name=server_name,
1073
  server_port=port if port != 0 else None,
1074
+ share=GRADIO_SHARE,
1075
  show_error=True,
1076
  quiet=False
1077
  )
graphs/main_graph.py CHANGED
@@ -43,6 +43,12 @@ from nodes.cache_node import (
43
  from nodes.graph_selection_node import graph_selection_node
44
  from nodes.graph_generation_node import graph_generation_node
45
  from nodes.custom_nodes import CustomNodeManager
 
 
 
 
 
 
46
  from agents.sql_agent import SQLAgentManager
47
  from agents.tools import CacheManager
48
  from utils.database import create_sql_database
@@ -96,8 +102,8 @@ class AgentGraphManager:
96
  self.db = db
97
  self.db_id = self.object_manager.store_database(db)
98
 
99
- # Cria agente SQL
100
- self.sql_agent = SQLAgentManager(db)
101
 
102
  # Armazena objetos no gerenciador
103
  self.agent_id = self.object_manager.store_sql_agent(self.sql_agent, self.db_id)
@@ -139,6 +145,15 @@ class AgentGraphManager:
139
  # Adiciona nós de validação e preparação
140
  workflow.add_node("validate_input", validate_query_input_node)
141
  workflow.add_node("check_cache", check_cache_node)
 
 
 
 
 
 
 
 
 
142
  workflow.add_node("validate_processing", validate_processing_input_node)
143
  workflow.add_node("process_initial_context", process_initial_context_node)
144
  workflow.add_node("prepare_context", prepare_query_context_node)
@@ -165,22 +180,42 @@ class AgentGraphManager:
165
  # Fluxo principal
166
  workflow.add_edge("validate_input", "check_cache")
167
 
168
- # Condicional para cache hit ou processing
169
  workflow.add_conditional_edges(
170
  "check_cache",
171
  route_after_cache_check,
172
  {
173
  "update_history": "update_history",
174
  "validate_processing": "validate_processing",
175
- "prepare_context": "prepare_context"
176
  }
177
  )
178
 
179
  # Fluxo do Processing Agent
180
  workflow.add_edge("validate_processing", "process_initial_context")
181
  workflow.add_edge("process_initial_context", "prepare_context")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
182
 
183
- workflow.add_edge("prepare_context", "get_db_sample")
 
 
 
 
184
  workflow.add_edge("get_db_sample", "process_query")
185
 
186
  # Condicional para gráficos (após AgentSQL)
@@ -229,6 +264,10 @@ class AgentGraphManager:
229
  advanced_mode: bool = False,
230
  processing_enabled: bool = False,
231
  processing_model: str = "GPT-4o-mini",
 
 
 
 
232
  thread_id: str = "default"
233
  ) -> Dict[str, Any]:
234
  """
@@ -240,6 +279,10 @@ class AgentGraphManager:
240
  advanced_mode: Se deve usar refinamento avançado
241
  processing_enabled: Se deve usar o Processing Agent
242
  processing_model: Modelo para o Processing Agent
 
 
 
 
243
  thread_id: ID da thread para checkpoint
244
 
245
  Returns:
@@ -256,7 +299,7 @@ class AgentGraphManager:
256
  if db_id:
257
  db = self.object_manager.get_database(db_id)
258
  if db:
259
- new_sql_agent = SQLAgentManager(db, selected_model)
260
  self.agent_id = self.object_manager.store_sql_agent(new_sql_agent, db_id)
261
  logging.info(f"Agente SQL recriado com sucesso para modelo {selected_model}")
262
  else:
@@ -271,6 +314,12 @@ class AgentGraphManager:
271
  logging.info(f"[MAIN GRAPH] Advanced mode: {advanced_mode}")
272
  logging.info(f"[MAIN GRAPH] Processing enabled: {processing_enabled}")
273
  logging.info(f"[MAIN GRAPH] Processing model: {processing_model}")
 
 
 
 
 
 
274
 
275
  # Prepara estado inicial com IDs serializáveis
276
  initial_state = {
@@ -313,7 +362,15 @@ class AgentGraphManager:
313
  "quality_metrics": None,
314
  # Campos relacionados ao contexto SQL
315
  "sql_context": None,
316
- "sql_result": None
 
 
 
 
 
 
 
 
317
  }
318
 
319
  # Executa o grafo
@@ -372,10 +429,10 @@ class AgentGraphManager:
372
  self.engine_id = db_result["engine_id"]
373
  self.db_id = db_result["db_id"]
374
 
375
- # Cria novo agente SQL
376
  new_engine = self.object_manager.get_engine(self.engine_id)
377
  new_db = self.object_manager.get_database(self.db_id)
378
- new_sql_agent = SQLAgentManager(new_db)
379
 
380
  # Atualiza agente
381
  self.agent_id = self.object_manager.store_sql_agent(new_sql_agent, self.db_id)
@@ -396,7 +453,79 @@ class AgentGraphManager:
396
  "success": False,
397
  "message": error_msg
398
  }
399
-
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
400
  async def reset_system(self) -> Dict[str, Any]:
401
  """
402
  Reseta o sistema ao estado inicial
 
43
  from nodes.graph_selection_node import graph_selection_node
44
  from nodes.graph_generation_node import graph_generation_node
45
  from nodes.custom_nodes import CustomNodeManager
46
+ from nodes.connection_selection_node import (
47
+ connection_selection_node,
48
+ validate_connection_input_node,
49
+ route_by_connection_type
50
+ )
51
+ from nodes.postgresql_connection_node import postgresql_connection_node
52
  from agents.sql_agent import SQLAgentManager
53
  from agents.tools import CacheManager
54
  from utils.database import create_sql_database
 
102
  self.db = db
103
  self.db_id = self.object_manager.store_database(db)
104
 
105
+ # Cria agente SQL (modo padrão multi-tabela)
106
+ self.sql_agent = SQLAgentManager(db, single_table_mode=False, selected_table=None)
107
 
108
  # Armazena objetos no gerenciador
109
  self.agent_id = self.object_manager.store_sql_agent(self.sql_agent, self.db_id)
 
145
  # Adiciona nós de validação e preparação
146
  workflow.add_node("validate_input", validate_query_input_node)
147
  workflow.add_node("check_cache", check_cache_node)
148
+
149
+ # Adiciona nós de conexão
150
+ workflow.add_node("connection_selection", connection_selection_node)
151
+ workflow.add_node("validate_connection", validate_connection_input_node)
152
+ workflow.add_node("postgresql_connection", postgresql_connection_node)
153
+ workflow.add_node("csv_processing", csv_processing_node)
154
+ workflow.add_node("create_database", create_database_from_dataframe_node)
155
+ workflow.add_node("load_database", load_existing_database_node)
156
+
157
  workflow.add_node("validate_processing", validate_processing_input_node)
158
  workflow.add_node("process_initial_context", process_initial_context_node)
159
  workflow.add_node("prepare_context", prepare_query_context_node)
 
180
  # Fluxo principal
181
  workflow.add_edge("validate_input", "check_cache")
182
 
183
+ # Condicional para cache hit ou processing/conexão
184
  workflow.add_conditional_edges(
185
  "check_cache",
186
  route_after_cache_check,
187
  {
188
  "update_history": "update_history",
189
  "validate_processing": "validate_processing",
190
+ "connection_selection": "connection_selection"
191
  }
192
  )
193
 
194
  # Fluxo do Processing Agent
195
  workflow.add_edge("validate_processing", "process_initial_context")
196
  workflow.add_edge("process_initial_context", "prepare_context")
197
+ workflow.add_edge("prepare_context", "connection_selection")
198
+
199
+ # Fluxo de seleção de conexão
200
+ workflow.add_edge("connection_selection", "validate_connection")
201
+
202
+ # Roteamento por tipo de conexão (apenas se necessário)
203
+ workflow.add_conditional_edges(
204
+ "validate_connection",
205
+ route_by_connection_type,
206
+ {
207
+ "postgresql_connection": "postgresql_connection",
208
+ "csv_processing": "csv_processing",
209
+ "load_database": "load_database",
210
+ "get_db_sample": "get_db_sample" # Pula conexão se já existe
211
+ }
212
+ )
213
 
214
+ # Fluxos específicos de conexão (apenas quando necessário)
215
+ workflow.add_edge("postgresql_connection", "get_db_sample")
216
+ workflow.add_edge("csv_processing", "create_database")
217
+ workflow.add_edge("create_database", "get_db_sample")
218
+ workflow.add_edge("load_database", "get_db_sample")
219
  workflow.add_edge("get_db_sample", "process_query")
220
 
221
  # Condicional para gráficos (após AgentSQL)
 
264
  advanced_mode: bool = False,
265
  processing_enabled: bool = False,
266
  processing_model: str = "GPT-4o-mini",
267
+ connection_type: str = "csv",
268
+ postgresql_config: Optional[Dict] = None,
269
+ selected_table: str = None,
270
+ single_table_mode: bool = False,
271
  thread_id: str = "default"
272
  ) -> Dict[str, Any]:
273
  """
 
279
  advanced_mode: Se deve usar refinamento avançado
280
  processing_enabled: Se deve usar o Processing Agent
281
  processing_model: Modelo para o Processing Agent
282
+ connection_type: Tipo de conexão ("csv" ou "postgresql")
283
+ postgresql_config: Configuração PostgreSQL (se aplicável)
284
+ selected_table: Tabela selecionada (para PostgreSQL)
285
+ single_table_mode: Se deve usar apenas uma tabela (PostgreSQL)
286
  thread_id: ID da thread para checkpoint
287
 
288
  Returns:
 
299
  if db_id:
300
  db = self.object_manager.get_database(db_id)
301
  if db:
302
+ new_sql_agent = SQLAgentManager(db, selected_model, single_table_mode=False, selected_table=None)
303
  self.agent_id = self.object_manager.store_sql_agent(new_sql_agent, db_id)
304
  logging.info(f"Agente SQL recriado com sucesso para modelo {selected_model}")
305
  else:
 
314
  logging.info(f"[MAIN GRAPH] Advanced mode: {advanced_mode}")
315
  logging.info(f"[MAIN GRAPH] Processing enabled: {processing_enabled}")
316
  logging.info(f"[MAIN GRAPH] Processing model: {processing_model}")
317
+ logging.info(f"[MAIN GRAPH] Connection type: {connection_type}")
318
+ if postgresql_config:
319
+ logging.info(f"[MAIN GRAPH] PostgreSQL config: {postgresql_config['host']}:{postgresql_config['port']}/{postgresql_config['database']}")
320
+ if selected_table:
321
+ logging.info(f"[MAIN GRAPH] Selected table: {selected_table}")
322
+ logging.info(f"[MAIN GRAPH] Single table mode: {single_table_mode}")
323
 
324
  # Prepara estado inicial com IDs serializáveis
325
  initial_state = {
 
362
  "quality_metrics": None,
363
  # Campos relacionados ao contexto SQL
364
  "sql_context": None,
365
+ "sql_result": None,
366
+ # Campos relacionados ao tipo de conexão
367
+ "connection_type": connection_type,
368
+ "postgresql_config": postgresql_config,
369
+ "selected_table": selected_table,
370
+ "single_table_mode": single_table_mode,
371
+ "connection_success": self.db_id is not None, # True se já tem conexão
372
+ "connection_error": None,
373
+ "connection_info": None
374
  }
375
 
376
  # Executa o grafo
 
429
  self.engine_id = db_result["engine_id"]
430
  self.db_id = db_result["db_id"]
431
 
432
+ # Cria novo agente SQL (modo padrão multi-tabela)
433
  new_engine = self.object_manager.get_engine(self.engine_id)
434
  new_db = self.object_manager.get_database(self.db_id)
435
+ new_sql_agent = SQLAgentManager(new_db, single_table_mode=False, selected_table=None)
436
 
437
  # Atualiza agente
438
  self.agent_id = self.object_manager.store_sql_agent(new_sql_agent, self.db_id)
 
453
  "success": False,
454
  "message": error_msg
455
  }
456
+
457
+ async def handle_postgresql_connection(self, state: Dict[str, Any]) -> Dict[str, Any]:
458
+ """
459
+ Processa conexão PostgreSQL usando nova arquitetura de nós
460
+
461
+ Args:
462
+ state: Estado contendo configuração PostgreSQL
463
+
464
+ Returns:
465
+ Resultado da conexão
466
+ """
467
+ try:
468
+ # Adiciona campos necessários ao estado
469
+ state.update({
470
+ "success": False,
471
+ "message": "",
472
+ "connection_info": {},
473
+ "connection_error": None,
474
+ "connection_success": False
475
+ })
476
+
477
+ # Executa nó de conexão PostgreSQL
478
+ pg_result = await postgresql_connection_node(state)
479
+
480
+ if not pg_result["success"]:
481
+ return pg_result
482
+
483
+ # Atualiza sistema se conexão foi bem-sucedida
484
+ if pg_result["success"]:
485
+ # Atualiza IDs dos objetos
486
+ self.engine_id = pg_result["engine_id"]
487
+ self.db_id = pg_result["db_id"]
488
+
489
+ # Cria novo agente SQL com configurações do estado
490
+ new_engine = self.object_manager.get_engine(self.engine_id)
491
+ new_db = self.object_manager.get_database(self.db_id)
492
+
493
+ # Obtém configurações de tabela do estado
494
+ single_table_mode = state.get("single_table_mode", False)
495
+ selected_table = state.get("selected_table")
496
+ selected_model = state.get("selected_model", "gpt-4o-mini")
497
+
498
+ new_sql_agent = SQLAgentManager(
499
+ new_db,
500
+ selected_model,
501
+ single_table_mode=single_table_mode,
502
+ selected_table=selected_table
503
+ )
504
+
505
+ # Atualiza agente
506
+ self.agent_id = self.object_manager.store_sql_agent(new_sql_agent, self.db_id)
507
+
508
+ # Armazena metadados de conexão
509
+ connection_info = pg_result.get("connection_info", {})
510
+ self.object_manager.store_connection_metadata(self.db_id, connection_info)
511
+
512
+ # Limpa cache
513
+ cache_manager = self.object_manager.get_cache_manager(self.cache_id)
514
+ if cache_manager:
515
+ cache_manager.clear_cache()
516
+
517
+ logging.info("[POSTGRESQL] Sistema atualizado com nova conexão PostgreSQL")
518
+
519
+ return pg_result
520
+
521
+ except Exception as e:
522
+ error_msg = f"❌ Erro na conexão PostgreSQL: {e}"
523
+ logging.error(error_msg)
524
+ return {
525
+ "success": False,
526
+ "message": error_msg
527
+ }
528
+
529
  async def reset_system(self) -> Dict[str, Any]:
530
  """
531
  Reseta o sistema ao estado inicial
nodes/agent_node.py CHANGED
@@ -54,6 +54,15 @@ class AgentState(TypedDict):
54
  sql_context: Optional[str] # Contexto preparado para o agente SQL
55
  sql_result: Optional[dict] # Resultado do agente SQL
56
 
 
 
 
 
 
 
 
 
 
57
 
58
  def should_refine_response(state: Dict[str, Any]) -> str:
59
  """
@@ -111,6 +120,8 @@ def route_after_cache_check(state: Dict[str, Any]) -> str:
111
  """
112
  Roteamento após verificação de cache
113
 
 
 
114
  Args:
115
  state: Estado atual
116
 
@@ -122,7 +133,11 @@ def route_after_cache_check(state: Dict[str, Any]) -> str:
122
  cache_hit = state.get("cache_hit", False)
123
  processing_enabled = state.get("processing_enabled", False)
124
 
125
- logging.info(f"[ROUTING] Cache hit: {cache_hit}")
 
 
 
 
126
  logging.info(f"[ROUTING] Processing enabled: {processing_enabled}")
127
 
128
  if cache_hit:
@@ -132,5 +147,5 @@ def route_after_cache_check(state: Dict[str, Any]) -> str:
132
  logging.info("[ROUTING] Direcionando para validate_processing (processing habilitado)")
133
  return "validate_processing"
134
  else:
135
- logging.info("[ROUTING] Direcionando para prepare_context (fluxo normal)")
136
- return "prepare_context"
 
54
  sql_context: Optional[str] # Contexto preparado para o agente SQL
55
  sql_result: Optional[dict] # Resultado do agente SQL
56
 
57
+ # Campos relacionados ao tipo de conexão
58
+ connection_type: str # "csv" | "postgresql"
59
+ postgresql_config: Optional[dict] # Configuração PostgreSQL
60
+ selected_table: Optional[str] # Tabela selecionada (para PostgreSQL)
61
+ single_table_mode: bool # Se deve usar apenas uma tabela (PostgreSQL)
62
+ connection_success: bool # Se a conexão foi estabelecida com sucesso
63
+ connection_error: Optional[str] # Erro na conexão
64
+ connection_info: Optional[dict] # Informações da conexão estabelecida
65
+
66
 
67
  def should_refine_response(state: Dict[str, Any]) -> str:
68
  """
 
120
  """
121
  Roteamento após verificação de cache
122
 
123
+ CACHE TEMPORARIAMENTE DESATIVADO - Sempre ignora cache hit
124
+
125
  Args:
126
  state: Estado atual
127
 
 
133
  cache_hit = state.get("cache_hit", False)
134
  processing_enabled = state.get("processing_enabled", False)
135
 
136
+ # DESATIVAÇÃO TEMPORÁRIA DO CACHE
137
+ # Força cache_hit = False para sempre processar queries
138
+ cache_hit = False
139
+
140
+ logging.info(f"[ROUTING] Cache hit: {cache_hit} (CACHE DESATIVADO TEMPORARIAMENTE)")
141
  logging.info(f"[ROUTING] Processing enabled: {processing_enabled}")
142
 
143
  if cache_hit:
 
147
  logging.info("[ROUTING] Direcionando para validate_processing (processing habilitado)")
148
  return "validate_processing"
149
  else:
150
+ logging.info("[ROUTING] Direcionando para connection_selection (fluxo direto)")
151
+ return "connection_selection"
nodes/connection_selection_node.py ADDED
@@ -0,0 +1,161 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Nó para seleção do tipo de conexão (csv ou postgresql)
3
+ """
4
+ import logging
5
+ from typing import Dict, Any
6
+ from utils.validation import validate_connection_state
7
+
8
+
9
+ async def connection_selection_node(state: Dict[str, Any]) -> Dict[str, Any]:
10
+ """
11
+ Nó para determinar o tipo de conexão baseado na entrada do usuário
12
+
13
+ Args:
14
+ state: Estado atual do agente
15
+
16
+ Returns:
17
+ Estado atualizado com tipo de conexão definido
18
+ """
19
+ try:
20
+ logging.info("[CONNECTION_SELECTION] Iniciando seleção de tipo de conexão")
21
+
22
+ # Verifica se o tipo de conexão já foi definido
23
+ connection_type = state.get("connection_type")
24
+
25
+ if not connection_type:
26
+ # Se não foi definido, assume csv como padrão (compatibilidade)
27
+ connection_type = "csv"
28
+ logging.info("[CONNECTION_SELECTION] Tipo de conexão não definido, usando csv como padrão")
29
+
30
+ # Valida tipo de conexão
31
+ valid_types = ["csv", "postgresql"]
32
+ if connection_type.upper() not in [t.upper() for t in valid_types]:
33
+ error_msg = f"Tipo de conexão inválido: {connection_type}. Tipos válidos: {valid_types}"
34
+ logging.error(f"[CONNECTION_SELECTION] {error_msg}")
35
+ state.update({
36
+ "connection_type": "csv", # Fallback para csv
37
+ "connection_error": error_msg,
38
+ "connection_success": False
39
+ })
40
+ return state
41
+
42
+ # Atualiza estado com tipo de conexão validado
43
+ state.update({
44
+ "connection_type": connection_type,
45
+ "connection_error": None,
46
+ "connection_success": True
47
+ })
48
+
49
+ logging.info(f"[CONNECTION_SELECTION] Tipo de conexão selecionado: {connection_type}")
50
+
51
+ return state
52
+
53
+ except Exception as e:
54
+ error_msg = f"Erro na seleção de tipo de conexão: {e}"
55
+ logging.error(f"[CONNECTION_SELECTION] {error_msg}")
56
+
57
+ # Fallback para csv em caso de erro
58
+ state.update({
59
+ "connection_type": "csv",
60
+ "connection_error": error_msg,
61
+ "connection_success": False
62
+ })
63
+
64
+ return state
65
+
66
+
67
+ def route_by_connection_type(state: Dict[str, Any]) -> str:
68
+ """
69
+ Função de roteamento baseada no tipo de conexão
70
+
71
+ Args:
72
+ state: Estado atual do agente
73
+
74
+ Returns:
75
+ Nome do próximo nó baseado no tipo de conexão
76
+ """
77
+ connection_type = state.get("connection_type", "csv")
78
+ file_path = state.get("file_path")
79
+ db_id = state.get("db_id")
80
+ engine_id = state.get("engine_id")
81
+
82
+ logging.info(f"[CONNECTION_ROUTING] Roteando para tipo: {connection_type}")
83
+ logging.info(f"[CONNECTION_ROUTING] DB ID existente: {db_id}")
84
+ logging.info(f"[CONNECTION_ROUTING] Engine ID existente: {engine_id}")
85
+
86
+ # Se já tem conexão estabelecida, pula para get_db_sample
87
+ # Verifica se o sistema já foi inicializado
88
+ from utils.object_manager import get_object_manager
89
+ obj_manager = get_object_manager()
90
+
91
+ # Verifica se há agentes SQL já criados (indicando sistema inicializado)
92
+ stats = obj_manager.get_stats()
93
+ has_sql_agents = stats.get("sql_agents", 0) > 0
94
+ has_databases = stats.get("databases", 0) > 0
95
+
96
+ if has_sql_agents and has_databases:
97
+ logging.info("[CONNECTION_ROUTING] Sistema já inicializado com agentes e bancos, pulando para get_db_sample")
98
+ return "get_db_sample"
99
+
100
+ # Fallback: verifica IDs específicos
101
+ if db_id and engine_id:
102
+ logging.info("[CONNECTION_ROUTING] Conexão já estabelecida via IDs, pulando para get_db_sample")
103
+ return "get_db_sample"
104
+
105
+ if connection_type.upper() == "POSTGRESQL":
106
+ return "postgresql_connection"
107
+ elif file_path:
108
+ # Há arquivo csv para processar
109
+ return "csv_processing"
110
+ else:
111
+ # Usar banco existente
112
+ return "load_database"
113
+
114
+
115
+ async def validate_connection_input_node(state: Dict[str, Any]) -> Dict[str, Any]:
116
+ """
117
+ Nó para validar entrada de conexão antes do processamento
118
+
119
+ Args:
120
+ state: Estado atual do agente
121
+
122
+ Returns:
123
+ Estado atualizado com validação
124
+ """
125
+ try:
126
+ logging.info("[CONNECTION_VALIDATION] Validando entrada de conexão")
127
+
128
+ connection_type = state.get("connection_type", "csv")
129
+
130
+ # Usa validação centralizada
131
+ is_valid, validation_error = validate_connection_state(state)
132
+
133
+ if not is_valid:
134
+ logging.error(f"[CONNECTION_VALIDATION] {validation_error}")
135
+ state.update({
136
+ "connection_error": validation_error,
137
+ "connection_success": False
138
+ })
139
+ return state
140
+
141
+ logging.info(f"[CONNECTION_VALIDATION] Validação de conexão {connection_type} bem-sucedida")
142
+
143
+ # Validação bem-sucedida
144
+ state.update({
145
+ "connection_error": None,
146
+ "connection_success": True
147
+ })
148
+
149
+ logging.info("[CONNECTION_VALIDATION] Validação de conexão concluída com sucesso")
150
+ return state
151
+
152
+ except Exception as e:
153
+ error_msg = f"Erro na validação de conexão: {e}"
154
+ logging.error(f"[CONNECTION_VALIDATION] {error_msg}")
155
+
156
+ state.update({
157
+ "connection_error": error_msg,
158
+ "connection_success": False
159
+ })
160
+
161
+ return state
nodes/custom_nodes.py CHANGED
@@ -136,8 +136,8 @@ async def reset_system_node(state: Dict[str, Any]) -> Dict[str, Any]:
136
  engine = obj_manager.get_engine(db_result["engine_id"])
137
  db = obj_manager.get_object(db_result["db_id"])
138
 
139
- # Recria agente SQL
140
- sql_agent = SQLAgentManager(db)
141
 
142
  # Atualiza objetos no gerenciador
143
  engine_id = obj_manager.store_engine(engine)
 
136
  engine = obj_manager.get_engine(db_result["engine_id"])
137
  db = obj_manager.get_object(db_result["db_id"])
138
 
139
+ # Recria agente SQL (modo padrão multi-tabela)
140
+ sql_agent = SQLAgentManager(db, single_table_mode=False, selected_table=None)
141
 
142
  # Atualiza objetos no gerenciador
143
  engine_id = obj_manager.store_engine(engine)
nodes/database_node.py CHANGED
@@ -202,8 +202,63 @@ async def get_database_sample_node(state: Dict[str, Any]) -> Dict[str, Any]:
202
  if not engine:
203
  raise ValueError("Engine não encontrada")
204
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
205
  # Obtém amostra dos dados
206
- sample_df = pd.read_sql_query("SELECT * FROM tabela LIMIT 10", engine)
 
 
 
 
 
 
207
 
208
  # Converte para formato serializável
209
  db_sample_dict = {
 
202
  if not engine:
203
  raise ValueError("Engine não encontrada")
204
 
205
+ # Determina qual tabela usar para amostra
206
+ connection_type = state.get("connection_type", "csv")
207
+
208
+ if connection_type == "postgresql":
209
+ # Para PostgreSQL, detecta dinamicamente a primeira tabela disponível com dados
210
+ import sqlalchemy as sa
211
+
212
+ try:
213
+ with engine.connect() as conn:
214
+ # Obtém lista de tabelas disponíveis
215
+ tables_result = conn.execute(sa.text("""
216
+ SELECT table_name
217
+ FROM information_schema.tables
218
+ WHERE table_schema = 'public'
219
+ ORDER BY table_name
220
+ """))
221
+ available_tables = [row[0] for row in tables_result.fetchall()]
222
+
223
+ if not available_tables:
224
+ raise ValueError("Nenhuma tabela encontrada no banco PostgreSQL")
225
+
226
+ # Tenta encontrar uma tabela com dados
227
+ table_name = None
228
+ for table in available_tables:
229
+ try:
230
+ # Verifica se a tabela tem dados
231
+ count_result = conn.execute(sa.text(f"SELECT COUNT(*) FROM {table} LIMIT 1"))
232
+ count = count_result.scalar()
233
+ if count > 0:
234
+ table_name = table
235
+ logging.info(f"[DATABASE] PostgreSQL - usando tabela '{table_name}' para amostra ({count} registros)")
236
+ break
237
+ except Exception as e:
238
+ logging.warning(f"[DATABASE] Erro ao verificar tabela {table}: {e}")
239
+ continue
240
+
241
+ # Se nenhuma tabela tem dados, usa a primeira disponível
242
+ if not table_name:
243
+ table_name = available_tables[0]
244
+ logging.info(f"[DATABASE] PostgreSQL - usando primeira tabela '{table_name}' (sem dados detectados)")
245
+
246
+ except Exception as e:
247
+ logging.error(f"[DATABASE] Erro ao detectar tabelas PostgreSQL: {e}")
248
+ raise ValueError(f"Erro ao acessar tabelas PostgreSQL: {e}")
249
+
250
+ else:
251
+ table_name = "tabela" # Padrão para CSV
252
+ logging.info(f"[DATABASE] CSV - usando tabela padrão: {table_name}")
253
+
254
  # Obtém amostra dos dados
255
+ try:
256
+ sample_df = pd.read_sql_query(f"SELECT * FROM {table_name} LIMIT 10", engine)
257
+ logging.info(f"[DATABASE] Amostra obtida da tabela '{table_name}': {sample_df.shape[0]} registros")
258
+ except Exception as e:
259
+ logging.error(f"[DATABASE] Erro ao obter amostra da tabela '{table_name}': {e}")
260
+ # Se falhar, cria DataFrame vazio para não quebrar o fluxo
261
+ sample_df = pd.DataFrame()
262
 
263
  # Converte para formato serializável
264
  db_sample_dict = {
nodes/postgresql_connection_node.py ADDED
@@ -0,0 +1,293 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Nó para conexão com PostgreSQL
3
+ """
4
+ import logging
5
+ import time
6
+ from typing import Dict, Any, Optional
7
+ from sqlalchemy import create_engine, text
8
+ from langchain_community.utilities import SQLDatabase
9
+
10
+ from utils.database import create_sql_database
11
+ from utils.object_manager import get_object_manager
12
+ from utils.validation import (
13
+ validate_postgresql_config,
14
+ sanitize_postgresql_config,
15
+ get_connection_error_message
16
+ )
17
+
18
+
19
+ async def postgresql_connection_node(state: Dict[str, Any]) -> Dict[str, Any]:
20
+ """
21
+ Nó para estabelecer conexão com PostgreSQL
22
+
23
+ Args:
24
+ state: Estado atual do agente
25
+
26
+ Returns:
27
+ Estado atualizado com conexão PostgreSQL estabelecida
28
+ """
29
+ try:
30
+ logging.info("[POSTGRESQL_CONNECTION] Iniciando conexão com PostgreSQL")
31
+
32
+ # Recupera e valida configuração PostgreSQL
33
+ postgresql_config = state.get("postgresql_config", {})
34
+
35
+ if not postgresql_config:
36
+ error_msg = "Configuração PostgreSQL não encontrada"
37
+ logging.error(f"[POSTGRESQL_CONNECTION] {error_msg}")
38
+ state.update({
39
+ "success": False,
40
+ "message": f"❌ {error_msg}",
41
+ "connection_error": error_msg,
42
+ "connection_success": False
43
+ })
44
+ return state
45
+
46
+ # Sanitiza e valida configuração
47
+ postgresql_config = sanitize_postgresql_config(postgresql_config)
48
+ is_valid, validation_error = validate_postgresql_config(postgresql_config)
49
+
50
+ if not is_valid:
51
+ error_msg = f"Configuração PostgreSQL inválida: {validation_error}"
52
+ logging.error(f"[POSTGRESQL_CONNECTION] {error_msg}")
53
+ state.update({
54
+ "success": False,
55
+ "message": f"❌ {validation_error}",
56
+ "connection_error": error_msg,
57
+ "connection_success": False
58
+ })
59
+ return state
60
+
61
+ # Extrai credenciais
62
+ host = postgresql_config.get("host")
63
+ port = postgresql_config.get("port", 5432)
64
+ database = postgresql_config.get("database")
65
+ username = postgresql_config.get("username")
66
+ password = postgresql_config.get("password")
67
+
68
+ # Constrói URI de conexão
69
+ connection_uri = f"postgresql+psycopg2://{username}:{password}@{host}:{port}/{database}"
70
+
71
+ logging.info(f"[POSTGRESQL_CONNECTION] Conectando a: {host}:{port}/{database}")
72
+
73
+ # Tenta estabelecer conexão
74
+ start_time = time.time()
75
+
76
+ try:
77
+ # Cria engine SQLAlchemy
78
+ engine = create_engine(
79
+ connection_uri,
80
+ pool_timeout=30,
81
+ pool_recycle=3600,
82
+ echo=False # Não mostrar SQL queries no log
83
+ )
84
+
85
+ # Testa conexão
86
+ with engine.connect() as conn:
87
+ result = conn.execute(text("SELECT 1"))
88
+ result.fetchone()
89
+
90
+ connection_time = time.time() - start_time
91
+ logging.info(f"[POSTGRESQL_CONNECTION] Conexão estabelecida em {connection_time:.2f}s")
92
+
93
+ except Exception as conn_error:
94
+ error_msg = f"Falha na conexão PostgreSQL: {str(conn_error)}"
95
+ logging.error(f"[POSTGRESQL_CONNECTION] {error_msg}")
96
+
97
+ # Usa função de tratamento de erro amigável
98
+ user_error = get_connection_error_message(conn_error)
99
+
100
+ state.update({
101
+ "success": False,
102
+ "message": user_error,
103
+ "connection_error": error_msg,
104
+ "connection_success": False
105
+ })
106
+ return state
107
+
108
+ # Cria objeto SQLDatabase do LangChain (sempre com todas as tabelas para amostra)
109
+ try:
110
+ db = SQLDatabase.from_uri(connection_uri)
111
+ logging.info("[POSTGRESQL_CONNECTION] SQLDatabase criado com sucesso")
112
+
113
+ # Obtém informações do banco
114
+ table_names = db.get_usable_table_names()
115
+ logging.info(f"[POSTGRESQL_CONNECTION] Tabelas encontradas: {table_names}")
116
+
117
+ if not table_names:
118
+ warning_msg = "⚠️ Nenhuma tabela encontrada no banco de dados"
119
+ logging.warning(f"[POSTGRESQL_CONNECTION] {warning_msg}")
120
+ # Não é um erro fatal, mas avisa o usuário
121
+
122
+ except Exception as db_error:
123
+ error_msg = f"Erro ao criar SQLDatabase: {str(db_error)}"
124
+ logging.error(f"[POSTGRESQL_CONNECTION] {error_msg}")
125
+ state.update({
126
+ "success": False,
127
+ "message": f"❌ {error_msg}",
128
+ "connection_error": error_msg,
129
+ "connection_success": False
130
+ })
131
+ return state
132
+
133
+ # Armazena objetos no ObjectManager
134
+ obj_manager = get_object_manager()
135
+ engine_id = obj_manager.store_engine(engine)
136
+ db_id = obj_manager.store_database(db)
137
+
138
+ # Informações da conexão
139
+ connection_info = {
140
+ "type": "postgresql",
141
+ "host": host,
142
+ "port": port,
143
+ "database": database,
144
+ "username": username,
145
+ "table_count": len(table_names),
146
+ "tables": table_names[:10], # Primeiras 10 tabelas
147
+ "connection_time": connection_time,
148
+ "engine_id": engine_id,
149
+ "db_id": db_id
150
+ }
151
+
152
+ # Atualiza estado com sucesso
153
+ state.update({
154
+ "success": True,
155
+ "message": f"✅ Conectado ao PostgreSQL: {len(table_names)} tabelas encontradas",
156
+ "connection_info": connection_info,
157
+ "connection_error": None,
158
+ "connection_success": True,
159
+ "engine_id": engine_id,
160
+ "db_id": db_id
161
+ })
162
+
163
+ logging.info(f"[POSTGRESQL_CONNECTION] Conexão PostgreSQL estabelecida com sucesso")
164
+ logging.info(f"[POSTGRESQL_CONNECTION] Informações: {connection_info}")
165
+
166
+ return state
167
+
168
+ except Exception as e:
169
+ error_msg = f"Erro inesperado na conexão PostgreSQL: {e}"
170
+ logging.error(f"[POSTGRESQL_CONNECTION] {error_msg}")
171
+
172
+ state.update({
173
+ "success": False,
174
+ "message": f"❌ {error_msg}",
175
+ "connection_error": error_msg,
176
+ "connection_success": False
177
+ })
178
+
179
+ return state
180
+
181
+
182
+ def validate_postgresql_credentials(postgresql_config: Dict[str, Any]) -> tuple[bool, Optional[str]]:
183
+ """
184
+ Valida credenciais PostgreSQL sem estabelecer conexão completa
185
+
186
+ Args:
187
+ postgresql_config: Configuração PostgreSQL
188
+
189
+ Returns:
190
+ Tupla (válido, mensagem_erro)
191
+ """
192
+ try:
193
+ required_fields = ["host", "port", "database", "username", "password"]
194
+
195
+ for field in required_fields:
196
+ if not postgresql_config.get(field):
197
+ return False, f"Campo obrigatório ausente: {field}"
198
+
199
+ # Validações básicas
200
+ port = postgresql_config.get("port")
201
+ try:
202
+ port_int = int(port)
203
+ if port_int < 1 or port_int > 65535:
204
+ return False, "Porta deve estar entre 1 e 65535"
205
+ except (ValueError, TypeError):
206
+ return False, "Porta deve ser um número válido"
207
+
208
+ host = postgresql_config.get("host", "").strip()
209
+ if not host:
210
+ return False, "Host não pode estar vazio"
211
+
212
+ database = postgresql_config.get("database", "").strip()
213
+ if not database:
214
+ return False, "Nome do banco não pode estar vazio"
215
+
216
+ username = postgresql_config.get("username", "").strip()
217
+ if not username:
218
+ return False, "Nome de usuário não pode estar vazio"
219
+
220
+ return True, None
221
+
222
+ except Exception as e:
223
+ return False, f"Erro na validação: {e}"
224
+
225
+
226
+ async def test_postgresql_connection_node(state: Dict[str, Any]) -> Dict[str, Any]:
227
+ """
228
+ Nó para testar conexão PostgreSQL sem armazenar
229
+
230
+ Args:
231
+ state: Estado atual do agente
232
+
233
+ Returns:
234
+ Estado atualizado com resultado do teste
235
+ """
236
+ try:
237
+ logging.info("[POSTGRESQL_TEST] Testando conexão PostgreSQL")
238
+
239
+ postgresql_config = state.get("postgresql_config", {})
240
+
241
+ # Valida credenciais
242
+ is_valid, error_msg = validate_postgresql_credentials(postgresql_config)
243
+ if not is_valid:
244
+ state.update({
245
+ "test_success": False,
246
+ "test_message": f"❌ {error_msg}",
247
+ "test_error": error_msg
248
+ })
249
+ return state
250
+
251
+ # Testa conexão rápida
252
+ host = postgresql_config.get("host")
253
+ port = postgresql_config.get("port", 5432)
254
+ database = postgresql_config.get("database")
255
+ username = postgresql_config.get("username")
256
+ password = postgresql_config.get("password")
257
+
258
+ connection_uri = f"postgresql+psycopg2://{username}:{password}@{host}:{port}/{database}"
259
+
260
+ try:
261
+ engine = create_engine(connection_uri, pool_timeout=10)
262
+ with engine.connect() as conn:
263
+ conn.execute(text("SELECT 1"))
264
+
265
+ state.update({
266
+ "test_success": True,
267
+ "test_message": "✅ Conexão PostgreSQL testada com sucesso",
268
+ "test_error": None
269
+ })
270
+
271
+ except Exception as e:
272
+ error_msg = f"Falha no teste de conexão: {str(e)}"
273
+ state.update({
274
+ "test_success": False,
275
+ "test_message": f"❌ {error_msg}",
276
+ "test_error": error_msg
277
+ })
278
+
279
+ return state
280
+
281
+ except Exception as e:
282
+ error_msg = f"Erro no teste de conexão: {e}"
283
+ logging.error(f"[POSTGRESQL_TEST] {error_msg}")
284
+
285
+ state.update({
286
+ "test_success": False,
287
+ "test_message": f"❌ {error_msg}",
288
+ "test_error": error_msg
289
+ })
290
+
291
+ return state
292
+
293
+
nodes/processing_node.py CHANGED
@@ -41,46 +41,120 @@ async def process_initial_context_node(state: Dict[str, Any]) -> Dict[str, Any]:
41
  logging.warning("[PROCESSING NODE] Entrada do usuário não disponível")
42
  return state
43
 
44
- # Acessa diretamente o banco de dados para criar amostra
45
  obj_manager = get_object_manager()
46
 
47
- # Usa os IDs do GraphManager (que são globais)
48
  try:
49
- # Acessa diretamente os IDs do GraphManager através do ObjectManager
50
- # Pega o primeiro engine e database disponíveis (assumindo que há apenas um)
51
- engines = obj_manager._engines
52
- databases = obj_manager._databases
53
-
54
- if not engines or not databases:
55
- logging.error("[PROCESSING NODE] Nenhum engine ou database encontrado no ObjectManager")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
56
  return state
57
 
58
- # Pega o primeiro engine e database disponíveis
59
- engine_id = list(engines.keys())[0]
60
- db_id = list(databases.keys())[0]
61
-
62
- engine = engines[engine_id]
63
- database = databases[db_id]
64
-
65
- logging.info(f"[PROCESSING NODE] Usando engine {engine_id} e database {db_id}")
66
-
67
- # Cria amostra diretamente do banco
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
68
  import sqlalchemy as sa
69
- with engine.connect() as conn:
70
- # Obtém amostra de dados (10 linhas)
71
- result = conn.execute(sa.text("SELECT * FROM tabela LIMIT 10"))
72
- columns = result.keys()
73
- rows = result.fetchall()
74
 
75
- # Converte para DataFrame
76
- db_sample = pd.DataFrame(rows, columns=columns)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
77
 
78
- logging.info(f"[PROCESSING NODE] Amostra criada diretamente do banco: {db_sample.shape[0]} linhas, {db_sample.shape[1]} colunas")
79
- logging.info(f"[PROCESSING NODE] Colunas: {list(db_sample.columns)}")
80
 
81
  except Exception as e:
82
- logging.error(f"[PROCESSING NODE] Erro ao acessar banco de dados: {e}")
83
  logging.error(f"[PROCESSING NODE] Detalhes do erro: {str(e)}")
 
 
 
84
  return state
85
 
86
  # Recupera ou cria Processing Agent
@@ -102,15 +176,44 @@ async def process_initial_context_node(state: Dict[str, Any]) -> Dict[str, Any]:
102
  state["processing_agent_id"] = processing_agent_id
103
  logging.info(f"[PROCESSING NODE] Novo Processing Agent criado e armazenado com ID: {processing_agent_id}")
104
 
105
- # Prepara contexto para o Processing Agent
106
- processing_context = prepare_processing_context(user_input, db_sample)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
107
 
108
  logging.info(f"[PROCESSING NODE] ===== CONTEXTO PARA PRIMEIRA LLM =====")
109
  logging.info(f"{processing_context}")
110
  logging.info(f"[PROCESSING NODE] ===== FIM DO CONTEXTO =====")
111
-
112
  # Executa processamento
113
- processing_result = await processing_agent.process_context(processing_context)
 
 
 
 
 
 
 
 
 
 
 
114
 
115
  # Log da resposta da primeira LLM
116
  logging.info(f"[PROCESSING NODE] ===== RESPOSTA DA PRIMEIRA LLM =====")
@@ -228,3 +331,147 @@ async def validate_processing_input_node(state: Dict[str, Any]) -> Dict[str, Any
228
  state["processing_enabled"] = False
229
 
230
  return state
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
41
  logging.warning("[PROCESSING NODE] Entrada do usuário não disponível")
42
  return state
43
 
44
+ # Acessa o banco de dados correto baseado no estado atual
45
  obj_manager = get_object_manager()
46
 
 
47
  try:
48
+ # Usa os IDs específicos do estado atual (não o primeiro disponível)
49
+ engine_id = state.get("engine_id")
50
+ db_id = state.get("db_id")
51
+
52
+ logging.info(f"[PROCESSING NODE] ===== DEBUG ESTADO =====")
53
+ logging.info(f"[PROCESSING NODE] engine_id do estado: {engine_id}")
54
+ logging.info(f"[PROCESSING NODE] db_id do estado: {db_id}")
55
+ logging.info(f"[PROCESSING NODE] connection_type do estado: {state.get('connection_type')}")
56
+ logging.info(f"[PROCESSING NODE] Chaves disponíveis no estado: {list(state.keys())}")
57
+ logging.info(f"[PROCESSING NODE] ===== FIM DEBUG =====")
58
+
59
+ if not engine_id or not db_id:
60
+ logging.error("[PROCESSING NODE] IDs de engine ou database não encontrados no estado")
61
+ logging.error(f"[PROCESSING NODE] engine_id: {engine_id}, db_id: {db_id}")
62
+
63
+ # Fallback: tenta usar os IDs disponíveis no ObjectManager
64
+ logging.info("[PROCESSING NODE] Tentando fallback para IDs disponíveis...")
65
+ engines = obj_manager._engines
66
+ databases = obj_manager._databases
67
+
68
+ if engines and databases:
69
+ engine_id = list(engines.keys())[-1] # Pega o último (mais recente)
70
+ db_id = list(databases.keys())[-1] # Pega o último (mais recente)
71
+ logging.info(f"[PROCESSING NODE] Fallback: usando engine_id={engine_id}, db_id={db_id}")
72
+ else:
73
+ logging.error("[PROCESSING NODE] Nenhum engine ou database disponível no ObjectManager")
74
+ return state
75
+
76
+ # Obtém engine e database específicos do estado atual
77
+ engine = obj_manager.get_engine(engine_id)
78
+ database = obj_manager.get_database(db_id)
79
+
80
+ logging.info(f"[PROCESSING NODE] Engine obtido: {engine is not None}")
81
+ logging.info(f"[PROCESSING NODE] Database obtido: {database is not None}")
82
+
83
+ if not engine or not database:
84
+ logging.error("[PROCESSING NODE] Engine ou database não encontrados no ObjectManager")
85
+ logging.error(f"[PROCESSING NODE] engine: {engine}, database: {database}")
86
+ logging.error(f"[PROCESSING NODE] Engines disponíveis: {list(obj_manager._engines.keys())}")
87
+ logging.error(f"[PROCESSING NODE] Databases disponíveis: {list(obj_manager._databases.keys())}")
88
  return state
89
 
90
+ logging.info(f"[PROCESSING NODE] Usando engine {engine_id} e database {db_id} do estado atual")
91
+
92
+ # Detecta o tipo de engine baseado no dialect
93
+ engine_dialect = str(engine.dialect.name).lower()
94
+ connection_type = state.get("connection_type", "csv")
95
+ single_table_mode = state.get("single_table_mode", False)
96
+ selected_table = state.get("selected_table")
97
+
98
+ logging.info(f"[PROCESSING NODE] ===== DETECÇÃO DE CONEXÃO =====")
99
+ logging.info(f"[PROCESSING NODE] Engine dialect detectado: {engine_dialect}")
100
+ logging.info(f"[PROCESSING NODE] Connection type do estado: {connection_type}")
101
+ logging.info(f"[PROCESSING NODE] Single table mode: {single_table_mode}")
102
+ logging.info(f"[PROCESSING NODE] Selected table: {selected_table}")
103
+ logging.info(f"[PROCESSING NODE] Engine URL: {str(engine.url)}")
104
+ logging.info(f"[PROCESSING NODE] ===== FIM DETECÇÃO =====")
105
+
106
+ # Validação: engine dialect deve corresponder ao connection_type
107
+ if connection_type.lower() == "postgresql" and engine_dialect != "postgresql":
108
+ logging.error(f"[PROCESSING NODE] INCONSISTÊNCIA: connection_type={connection_type} mas engine_dialect={engine_dialect}")
109
+ logging.error(f"[PROCESSING NODE] Isso indica que está usando o engine errado!")
110
+ elif connection_type.lower() == "csv" and engine_dialect != "sqlite":
111
+ logging.error(f"[PROCESSING NODE] INCONSISTÊNCIA: connection_type={connection_type} mas engine_dialect={engine_dialect}")
112
+ logging.error(f"[PROCESSING NODE] Isso indica que está usando o engine errado!")
113
+
114
+ # NOVA IMPLEMENTAÇÃO: Cria dados das colunas baseado no tipo de conexão
115
+ columns_data = {}
116
  import sqlalchemy as sa
 
 
 
 
 
117
 
118
+ if engine_dialect == "postgresql":
119
+ # Para PostgreSQL, processa baseado no modo
120
+ if single_table_mode and selected_table:
121
+ # Modo tabela única - processa APENAS a tabela selecionada
122
+ logging.info(f"[PROCESSING NODE] PostgreSQL - Modo tabela única: {selected_table}")
123
+ columns_data[selected_table] = _extract_table_columns_info(engine, selected_table)
124
+
125
+ else:
126
+ # Modo multi-tabela - processa TODAS as tabelas disponíveis
127
+ logging.info(f"[PROCESSING NODE] PostgreSQL - Modo multi-tabela")
128
+
129
+ # Obtém lista de todas as tabelas
130
+ with engine.connect() as conn:
131
+ tables_result = conn.execute(sa.text("""
132
+ SELECT table_name
133
+ FROM information_schema.tables
134
+ WHERE table_schema = 'public'
135
+ ORDER BY table_name
136
+ """))
137
+ available_tables = [row[0] for row in tables_result.fetchall()]
138
+
139
+ logging.info(f"[PROCESSING NODE] Tabelas encontradas: {available_tables}")
140
+
141
+ # Processa cada tabela (máximo 5 para performance)
142
+ for table_name in available_tables[:10]:
143
+ columns_data[table_name] = _extract_table_columns_info(engine, table_name)
144
+
145
+ else:
146
+ # Para SQLite (CSV convertido), processa tabela padrão
147
+ logging.info(f"[PROCESSING NODE] SQLite - processando tabela padrão")
148
+ columns_data["tabela"] = _extract_table_columns_info(engine, "tabela")
149
 
150
+ logging.info(f"[PROCESSING NODE] Dados das colunas extraídos para {len(columns_data)} tabela(s)")
 
151
 
152
  except Exception as e:
153
+ logging.error(f"[PROCESSING NODE] Erro ao acessar banco de dados: {e}")
154
  logging.error(f"[PROCESSING NODE] Detalhes do erro: {str(e)}")
155
+ logging.error(f"[PROCESSING NODE] Tipo do erro: {type(e)}")
156
+ import traceback
157
+ logging.error(f"[PROCESSING NODE] Traceback: {traceback.format_exc()}")
158
  return state
159
 
160
  # Recupera ou cria Processing Agent
 
176
  state["processing_agent_id"] = processing_agent_id
177
  logging.info(f"[PROCESSING NODE] Novo Processing Agent criado e armazenado com ID: {processing_agent_id}")
178
 
179
+ # Prepara contexto para o Processing Agent com dados já processados
180
+ connection_type = state.get("connection_type", "csv")
181
+ single_table_mode = state.get("single_table_mode", False)
182
+ selected_table = state.get("selected_table")
183
+
184
+ # Obtém lista de tabelas disponíveis se for PostgreSQL
185
+ available_tables = None
186
+ if engine_dialect == "postgresql":
187
+ available_tables = list(columns_data.keys())
188
+ logging.info(f"[PROCESSING NODE] Tabelas disponíveis para contexto: {available_tables}")
189
+
190
+ # NOVA CHAMADA: Passa dados já processados em vez de fazer consultas redundantes
191
+ processing_context = prepare_processing_context(
192
+ user_input,
193
+ columns_data, # Dados já processados das colunas
194
+ connection_type,
195
+ single_table_mode,
196
+ selected_table,
197
+ available_tables
198
+ )
199
 
200
  logging.info(f"[PROCESSING NODE] ===== CONTEXTO PARA PRIMEIRA LLM =====")
201
  logging.info(f"{processing_context}")
202
  logging.info(f"[PROCESSING NODE] ===== FIM DO CONTEXTO =====")
203
+
204
  # Executa processamento
205
+ logging.info(f"[PROCESSING NODE] 🚀 Iniciando execução do Processing Agent...")
206
+ logging.info(f"[PROCESSING NODE] Processing Agent: {processing_agent}")
207
+ logging.info(f"[PROCESSING NODE] Modelo: {processing_agent.model_name if processing_agent else 'N/A'}")
208
+
209
+ try:
210
+ processing_result = await processing_agent.process_context(processing_context)
211
+ logging.info(f"[PROCESSING NODE] ✅ Processing Agent executado com sucesso")
212
+ except Exception as e:
213
+ logging.error(f"[PROCESSING NODE] ❌ Erro na execução do Processing Agent: {e}")
214
+ import traceback
215
+ logging.error(f"[PROCESSING NODE] Traceback: {traceback.format_exc()}")
216
+ return state
217
 
218
  # Log da resposta da primeira LLM
219
  logging.info(f"[PROCESSING NODE] ===== RESPOSTA DA PRIMEIRA LLM =====")
 
331
  state["processing_enabled"] = False
332
 
333
  return state
334
+
335
+
336
+ def _extract_table_columns_info(engine, table_name: str) -> list:
337
+ """
338
+ Extrai informações das colunas de uma tabela específica
339
+
340
+ Args:
341
+ engine: Engine SQLAlchemy
342
+ table_name: Nome da tabela
343
+
344
+ Returns:
345
+ Lista de dicionários com informações das colunas
346
+ """
347
+ import sqlalchemy as sa
348
+ import pandas as pd
349
+
350
+ try:
351
+ logging.info(f"[PROCESSING NODE] Extraindo informações da tabela: {table_name}")
352
+
353
+ with engine.connect() as conn:
354
+ # Primeiro, tenta obter dados da tabela (máximo 5 linhas)
355
+ try:
356
+ result = conn.execute(sa.text(f"SELECT * FROM {table_name} LIMIT 5"))
357
+ columns = result.keys()
358
+ rows = result.fetchall()
359
+
360
+ if rows:
361
+ # Tabela com dados - processa normalmente
362
+ table_df = pd.DataFrame(rows, columns=columns)
363
+ columns_info = []
364
+
365
+ for col in table_df.columns:
366
+ col_data = table_df[col].dropna()
367
+
368
+ col_info = {
369
+ "column": col,
370
+ "type": str(col_data.dtype) if len(col_data) > 0 else "object",
371
+ "examples": "",
372
+ "stats": ""
373
+ }
374
+
375
+ if len(col_data) > 0:
376
+ # Adiciona exemplos de valores
377
+ unique_values = col_data.unique()[:3]
378
+ col_info["examples"] = ", ".join([str(v) for v in unique_values])
379
+
380
+ # Adiciona estatísticas para colunas numéricas
381
+ if col_data.dtype in ['int64', 'float64']:
382
+ try:
383
+ min_val = col_data.min()
384
+ max_val = col_data.max()
385
+ col_info["stats"] = f" | Min: {min_val}, Max: {max_val}"
386
+ except:
387
+ pass
388
+
389
+ columns_info.append(col_info)
390
+
391
+ logging.info(f"[PROCESSING NODE] ✅ Tabela {table_name}: {len(columns_info)} colunas com dados")
392
+ return columns_info
393
+
394
+ else:
395
+ # Tabela sem dados - obtém apenas estrutura das colunas
396
+ logging.info(f"[PROCESSING NODE] ⚠️ Tabela {table_name} sem dados - obtendo apenas estrutura")
397
+
398
+ # Para PostgreSQL, obtém informações das colunas do schema
399
+ if str(engine.dialect.name).lower() == "postgresql":
400
+ schema_result = conn.execute(sa.text(f"""
401
+ SELECT column_name, data_type
402
+ FROM information_schema.columns
403
+ WHERE table_name = '{table_name}'
404
+ ORDER BY ordinal_position
405
+ """))
406
+
407
+ columns_info = []
408
+ for row in schema_result.fetchall():
409
+ col_info = {
410
+ "column": row[0],
411
+ "type": row[1],
412
+ "examples": "(sem dados)",
413
+ "stats": ""
414
+ }
415
+ columns_info.append(col_info)
416
+ else:
417
+ # Para SQLite, usa PRAGMA
418
+ pragma_result = conn.execute(sa.text(f"PRAGMA table_info({table_name})"))
419
+ columns_info = []
420
+ for row in pragma_result.fetchall():
421
+ col_info = {
422
+ "column": row[1], # column name
423
+ "type": row[2], # column type
424
+ "examples": "(sem dados)",
425
+ "stats": ""
426
+ }
427
+ columns_info.append(col_info)
428
+
429
+ logging.info(f"[PROCESSING NODE] ✅ Tabela {table_name}: {len(columns_info)} colunas (estrutura apenas)")
430
+ return columns_info
431
+
432
+ except Exception as e:
433
+ # Se falhar ao acessar a tabela, tenta obter pelo menos a estrutura
434
+ logging.warning(f"[PROCESSING NODE] Erro ao acessar dados da tabela {table_name}: {e}")
435
+
436
+ try:
437
+ # Fallback: obtém estrutura das colunas
438
+ if str(engine.dialect.name).lower() == "postgresql":
439
+ schema_result = conn.execute(sa.text(f"""
440
+ SELECT column_name, data_type
441
+ FROM information_schema.columns
442
+ WHERE table_name = '{table_name}'
443
+ ORDER BY ordinal_position
444
+ """))
445
+
446
+ columns_info = []
447
+ for row in schema_result.fetchall():
448
+ col_info = {
449
+ "column": row[0],
450
+ "type": row[1],
451
+ "examples": "(erro ao acessar dados)",
452
+ "stats": ""
453
+ }
454
+ columns_info.append(col_info)
455
+ else:
456
+ # Para SQLite
457
+ pragma_result = conn.execute(sa.text(f"PRAGMA table_info({table_name})"))
458
+ columns_info = []
459
+ for row in pragma_result.fetchall():
460
+ col_info = {
461
+ "column": row[1],
462
+ "type": row[2],
463
+ "examples": "(erro ao acessar dados)",
464
+ "stats": ""
465
+ }
466
+ columns_info.append(col_info)
467
+
468
+ logging.info(f"[PROCESSING NODE] ⚠️ Tabela {table_name}: {len(columns_info)} colunas (fallback)")
469
+ return columns_info
470
+
471
+ except Exception as e2:
472
+ logging.error(f"[PROCESSING NODE] ❌ Erro total ao processar tabela {table_name}: {e2}")
473
+ return []
474
+
475
+ except Exception as e:
476
+ logging.error(f"[PROCESSING NODE] ❌ Erro ao extrair informações da tabela {table_name}: {e}")
477
+ return []
nodes/query_node.py CHANGED
@@ -55,8 +55,9 @@ async def process_user_query_node(state: Dict[str, Any]) -> Dict[str, Any]:
55
  cache_id = state.get("cache_id")
56
  cache_manager = obj_manager.get_cache_manager(cache_id) if cache_id else None
57
 
 
58
  # Verifica cache se disponível
59
- if cache_manager:
60
  cached_response = cache_manager.get_cached_response(user_input)
61
  if cached_response:
62
  logging.info(f"[CACHE] Retornando resposta do cache")
@@ -108,10 +109,38 @@ async def process_user_query_node(state: Dict[str, Any]) -> Dict[str, Any]:
108
  agent_id = state.get("agent_id")
109
  if not agent_id:
110
  raise ValueError("ID do agente SQL não encontrado")
111
-
112
  sql_agent = obj_manager.get_sql_agent(agent_id)
113
  if not sql_agent:
114
  raise ValueError("Agente SQL não encontrado")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
115
 
116
  # Executa query no agente SQL com contexto direto
117
  sql_result = await sql_agent.execute_query(state["sql_context"])
 
55
  cache_id = state.get("cache_id")
56
  cache_manager = obj_manager.get_cache_manager(cache_id) if cache_id else None
57
 
58
+ # CACHE TEMPORARIAMENTE DESATIVADO
59
  # Verifica cache se disponível
60
+ if False: # cache_manager:
61
  cached_response = cache_manager.get_cached_response(user_input)
62
  if cached_response:
63
  logging.info(f"[CACHE] Retornando resposta do cache")
 
109
  agent_id = state.get("agent_id")
110
  if not agent_id:
111
  raise ValueError("ID do agente SQL não encontrado")
112
+
113
  sql_agent = obj_manager.get_sql_agent(agent_id)
114
  if not sql_agent:
115
  raise ValueError("Agente SQL não encontrado")
116
+
117
+ # Verifica se precisa recriar o agente SQL para PostgreSQL com configurações atuais
118
+ connection_type = state.get("connection_type", "csv")
119
+ if connection_type == "postgresql":
120
+ single_table_mode = state.get("single_table_mode", False)
121
+ selected_table = state.get("selected_table")
122
+ selected_model = state.get("selected_model", "gpt-4o-mini")
123
+
124
+ # Verifica se as configurações mudaram
125
+ current_single_mode = getattr(sql_agent, 'single_table_mode', False)
126
+ current_table = getattr(sql_agent, 'selected_table', None)
127
+ current_model = getattr(sql_agent, 'model_name', 'gpt-4o-mini')
128
+
129
+ if (single_table_mode != current_single_mode or
130
+ selected_table != current_table or
131
+ selected_model != current_model):
132
+
133
+ logging.info(f"[QUERY] Recriando agente SQL - Modo: {'único' if single_table_mode else 'multi'}, Tabela: {selected_table}")
134
+
135
+ # Recria o agente com as novas configurações
136
+ sql_agent.recreate_agent(
137
+ single_table_mode=single_table_mode,
138
+ selected_table=selected_table,
139
+ new_model=selected_model
140
+ )
141
+
142
+ # Atualiza no ObjectManager
143
+ obj_manager.store_sql_agent(sql_agent, state.get("db_id"))
144
 
145
  # Executa query no agente SQL com contexto direto
146
  sql_result = await sql_agent.execute_query(state["sql_context"])
requirements.txt CHANGED
@@ -3,6 +3,7 @@ langgraph>=0.2.0
3
  langchain>=0.3.0
4
  langchain-openai>=0.2.0
5
  langchain-anthropic>=0.2.0
 
6
  langchain-community>=0.3.0
7
  langchain-core>=0.3.0
8
 
@@ -13,6 +14,7 @@ langsmith>=0.1.39
13
  pandas>=2.0.0
14
  sqlalchemy>=2.0.0
15
  numpy>=1.24.0
 
16
 
17
  # AI/ML Libraries
18
  huggingface_hub>=0.20.0
 
3
  langchain>=0.3.0
4
  langchain-openai>=0.2.0
5
  langchain-anthropic>=0.2.0
6
+ langchain-google-genai>=2.0.0
7
  langchain-community>=0.3.0
8
  langchain-core>=0.3.0
9
 
 
14
  pandas>=2.0.0
15
  sqlalchemy>=2.0.0
16
  numpy>=1.24.0
17
+ psycopg2-binary>=2.9.0
18
 
19
  # AI/ML Libraries
20
  huggingface_hub>=0.20.0
testes/README.md ADDED
@@ -0,0 +1,275 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # 🧪 Sistema de Testes Massivos - AgentGraph
2
+
3
+ Sistema completo para testes de consistência e performance dos agentes SQL com paralelismo otimizado.
4
+
5
+ ## 🎯 **Funcionalidades**
6
+
7
+ ### **✅ Configuração Dinâmica**
8
+ - **Pergunta personalizada**: Define a query que será testada
9
+ - **Múltiplos grupos**: Diferentes configurações de modelos
10
+ - **Processing Agent**: Ativa/desativa com modelo específico
11
+ - **Iterações configuráveis**: 1-100 testes por grupo
12
+
13
+ ### **⚡ Execução Paralela**
14
+ - **Paralelismo otimizado**: Até 8 workers simultâneos
15
+ - **Controle de recursos**: Semáforos para evitar sobrecarga
16
+ - **Progress tracking**: Acompanhamento em tempo real
17
+ - **Estimativa de tempo**: Tempo restante calculado dinamicamente
18
+
19
+ ### **🔍 Validação Inteligente**
20
+ - **Validação LLM**: Análise automática com GPT/Claude
21
+ - **Validação por palavra-chave**: Busca por conteúdo específico
22
+ - **Pontuação 0-100**: Score detalhado de qualidade
23
+ - **Análise de consistência**: Taxa de respostas similares
24
+
25
+ ### **📊 Relatórios Completos**
26
+ - **Dashboard interativo**: Visualização em tempo real
27
+ - **Múltiplas abas**: Resumo, grupos, resultados individuais
28
+ - **Exportação Excel**: Relatórios detalhados em XLSX
29
+ - **Métricas avançadas**: Taxa de acerto, consistência, performance
30
+
31
+ ## 🚀 **Como Usar**
32
+
33
+ ### **1. Instalação**
34
+ ```bash
35
+ # Instalar dependências
36
+ pip install -r testes/requirements.txt
37
+
38
+ # Verificar configuração do AgentGraph
39
+ python -c "from utils.config import validate_config; validate_config()"
40
+ ```
41
+
42
+ ### **2. Inicialização**
43
+ ```bash
44
+ # IMPORTANTE: Execute da raiz do projeto!
45
+ # Método 1: Script automático (RECOMENDADO)
46
+ python run_massive_tests.py
47
+
48
+ # Método 2: Direto (apenas se necessário)
49
+ python testes/app_teste.py
50
+ ```
51
+
52
+ ### **3. Acesso**
53
+ - **URL**: http://localhost:5001
54
+ - **Interface**: HTML responsiva
55
+ - **Compatibilidade**: Todos os navegadores modernos
56
+
57
+ ## 📋 **Fluxo de Uso**
58
+
59
+ ### **Passo 1: Configurar Pergunta**
60
+ ```
61
+ 1. Digite a pergunta que será testada
62
+ 2. Clique em "Criar Sessão"
63
+ 3. Sessão será criada com ID único
64
+ ```
65
+
66
+ ### **Passo 2: Adicionar Grupos**
67
+ ```
68
+ 1. Selecione modelo do SQL Agent
69
+ 2. Ative Processing Agent (opcional)
70
+ 3. Escolha modelo do Processing Agent
71
+ 4. Defina número de iterações (1-100)
72
+ 5. Clique em "Adicionar Grupo"
73
+ 6. Repita para criar múltiplos grupos
74
+ ```
75
+
76
+ ### **Passo 3: Configurar Validação**
77
+ ```
78
+ Método LLM (Recomendado):
79
+ - Análise automática da qualidade
80
+ - Pontuação 0-100
81
+ - Razão detalhada
82
+
83
+ Método Palavra-chave:
84
+ - Busca por texto específico
85
+ - Validação binária (contém/não contém)
86
+ ```
87
+
88
+ ### **Passo 4: Executar Testes**
89
+ ```
90
+ 1. Clique em "Executar Testes"
91
+ 2. Acompanhe progresso em tempo real
92
+ 3. Visualize métricas durante execução
93
+ 4. Aguarde conclusão automática
94
+ ```
95
+
96
+ ### **Passo 5: Analisar Resultados**
97
+ ```
98
+ Aba Resumo:
99
+ - Métricas gerais
100
+ - Melhor grupo
101
+ - Grupo mais consistente
102
+
103
+ Aba Por Grupo:
104
+ - Taxa de sucesso por grupo
105
+ - Comparação de modelos
106
+ - Performance detalhada
107
+
108
+ Aba Individual:
109
+ - Cada teste executado
110
+ - Detalhes completos
111
+ - Query SQL gerada
112
+ ```
113
+
114
+ ## 📊 **Métricas Calculadas**
115
+
116
+ ### **Taxa de Sucesso**
117
+ - **Cálculo**: (Testes sem erro / Total de testes) × 100
118
+ - **Indica**: Estabilidade do modelo
119
+ - **Ideal**: > 90%
120
+
121
+ ### **Taxa de Validação**
122
+ - **Cálculo**: (Respostas válidas / Total de testes) × 100
123
+ - **Indica**: Qualidade das respostas
124
+ - **Ideal**: > 80%
125
+
126
+ ### **Consistência de Resposta**
127
+ - **Cálculo**: (Respostas idênticas / Total de respostas) × 100
128
+ - **Indica**: Determinismo do modelo
129
+ - **Ideal**: > 70%
130
+
131
+ ### **Consistência SQL**
132
+ - **Cálculo**: (Queries idênticas / Total de queries) × 100
133
+ - **Indica**: Estabilidade da geração SQL
134
+ - **Ideal**: > 80%
135
+
136
+ ### **Tempo Médio**
137
+ - **Cálculo**: Soma dos tempos / Número de testes
138
+ - **Indica**: Performance do modelo
139
+ - **Ideal**: < 10s
140
+
141
+ ## 🔧 **Configurações Avançadas**
142
+
143
+ ### **Paralelismo**
144
+ ```python
145
+ # Em test_runner.py
146
+ max_workers = 8 # Ajuste conforme sua máquina
147
+ ```
148
+
149
+ ### **Validação LLM**
150
+ ```python
151
+ # Em test_validator.py
152
+ validator_model = "gpt-4o-mini" # Modelo para validação
153
+ temperature = 0.1 # Baixa para consistência
154
+ ```
155
+
156
+ ### **Timeouts**
157
+ ```python
158
+ # Em app_teste.py
159
+ status_polling = 2000 # ms entre verificações
160
+ ```
161
+
162
+ ## 📁 **Estrutura de Arquivos**
163
+
164
+ ```
165
+ testes/
166
+ ├── app_teste.py # Servidor Flask principal
167
+ ├── test_runner.py # Executor de testes paralelos
168
+ ├── test_validator.py # Sistema de validação
169
+ ├── report_generator.py # Gerador de relatórios
170
+ ├── run_tests.py # Script de inicialização
171
+ ├── requirements.txt # Dependências
172
+ ├── README.md # Esta documentação
173
+ ├── templates/
174
+ │ └── index.html # Interface HTML
175
+ ├── static/
176
+ │ └── js/
177
+ │ └── app.js # JavaScript da interface
178
+ └── reports/ # Relatórios gerados
179
+ ├── *.xlsx # Relatórios Excel
180
+ ├── *.json # Dados brutos
181
+ └── *.html # Resumos HTML
182
+ ```
183
+
184
+ ## 🎯 **Casos de Uso**
185
+
186
+ ### **Comparação de Modelos**
187
+ ```
188
+ Objetivo: Qual modelo SQL é mais consistente?
189
+ Configuração:
190
+ - Grupo 1: GPT-4o + Processing Agent
191
+ - Grupo 2: Claude-3.5-Sonnet + Processing Agent
192
+ - Grupo 3: GPT-4o sem Processing Agent
193
+ - 20 iterações cada
194
+ ```
195
+
196
+ ### **Impacto do Processing Agent**
197
+ ```
198
+ Objetivo: Processing Agent melhora a qualidade?
199
+ Configuração:
200
+ - Grupo 1: GPT-4o-mini COM Processing Agent
201
+ - Grupo 2: GPT-4o-mini SEM Processing Agent
202
+ - Mesma pergunta, 50 iterações cada
203
+ ```
204
+
205
+ ### **Teste de Stress**
206
+ ```
207
+ Objetivo: Como o sistema se comporta sob carga?
208
+ Configuração:
209
+ - 5 grupos diferentes
210
+ - 100 iterações cada
211
+ - Monitorar tempo de resposta
212
+ ```
213
+
214
+ ## 🚨 **Limitações e Considerações**
215
+
216
+ ### **Rate Limits das APIs**
217
+ - **OpenAI**: ~3000 requests/minuto
218
+ - **Anthropic**: ~1000 requests/minuto
219
+ - **Ajuste**: Reduza max_workers se necessário
220
+
221
+ ### **Recursos do Sistema**
222
+ - **RAM**: ~100MB por worker ativo
223
+ - **CPU**: Intensivo durante execução
224
+ - **Rede**: Dependente das APIs LLM
225
+
226
+ ### **Custos**
227
+ - **Validação LLM**: ~$0.001 por teste
228
+ - **Testes massivos**: Pode gerar custos significativos
229
+ - **Recomendação**: Comece com poucos testes
230
+
231
+ ## 🔍 **Troubleshooting**
232
+
233
+ ### **Erro: "Dependência faltando"**
234
+ ```bash
235
+ pip install -r testes/requirements.txt
236
+ ```
237
+
238
+ ### **Erro: "AgentGraph não configurado"**
239
+ ```bash
240
+ # Verifique .env com APIs configuradas
241
+ cp .env.example .env
242
+ # Edite .env com suas chaves
243
+ ```
244
+
245
+ ### **Erro: "Porta 5001 em uso"**
246
+ ```python
247
+ # Em app_teste.py, altere:
248
+ app.run(port=5002) # Use porta diferente
249
+ ```
250
+
251
+ ### **Performance lenta**
252
+ ```python
253
+ # Reduza workers em test_runner.py:
254
+ max_workers = 4 # Ao invés de 8
255
+ ```
256
+
257
+ ## 📈 **Próximas Funcionalidades**
258
+
259
+ - [ ] **Testes agendados**: Execução automática
260
+ - [ ] **Comparação histórica**: Evolução ao longo do tempo
261
+ - [ ] **Alertas**: Notificações de degradação
262
+ - [ ] **API REST**: Integração com CI/CD
263
+ - [ ] **Dashboards avançados**: Gráficos interativos
264
+
265
+ ## 🤝 **Contribuição**
266
+
267
+ Para melhorias ou bugs:
268
+ 1. Documente o problema/sugestão
269
+ 2. Teste em ambiente isolado
270
+ 3. Mantenha compatibilidade com AgentGraph
271
+ 4. Atualize documentação se necessário
272
+
273
+ ---
274
+
275
+ **🎉 Sistema pronto para uso! Execute `python run_massive_tests.py` da raiz do projeto e comece a testar!**
testes/__init__.py ADDED
@@ -0,0 +1 @@
 
 
1
+ # Pasta de testes massivos para AgentGraph
testes/app_teste.py ADDED
@@ -0,0 +1,494 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """
3
+ Sistema de Testes Massivos para AgentGraph
4
+ Interface HTML isolada para testes de consistência e performance
5
+ """
6
+ import os
7
+ import sys
8
+ import asyncio
9
+ import logging
10
+ from flask import Flask, render_template, request, jsonify, send_file
11
+ from datetime import datetime
12
+ import json
13
+
14
+ # Adiciona o diretório pai ao path para importar módulos do AgentGraph
15
+ sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
16
+
17
+ from testes.test_runner import MassiveTestRunner
18
+ from testes.test_validator import TestValidator
19
+ from testes.report_generator import ReportGenerator
20
+ from utils.config import AVAILABLE_MODELS
21
+
22
+ # Configuração do Flask
23
+ app = Flask(__name__,
24
+ template_folder='templates',
25
+ static_folder='static')
26
+
27
+ # Configuração de logging
28
+ logging.basicConfig(
29
+ level=logging.INFO,
30
+ format='%(asctime)s - %(levelname)s - %(message)s'
31
+ )
32
+
33
+ # Instância global do test runner
34
+ test_runner = None
35
+ current_test_session = None
36
+
37
+ @app.route('/')
38
+ def index():
39
+ """Página principal do sistema de testes"""
40
+ return render_template('index.html',
41
+ available_models=AVAILABLE_MODELS)
42
+
43
+ @app.route('/api/models')
44
+ def get_models():
45
+ """Retorna modelos disponíveis"""
46
+ return jsonify({
47
+ 'success': True,
48
+ 'models': AVAILABLE_MODELS
49
+ })
50
+
51
+ @app.route('/api/create_test_session', methods=['POST'])
52
+ def create_test_session():
53
+ """Cria nova sessão de teste"""
54
+ global current_test_session
55
+
56
+ try:
57
+ data = request.json
58
+ question = data.get('question', '').strip()
59
+
60
+ if not question:
61
+ return jsonify({
62
+ 'success': False,
63
+ 'error': 'Pergunta é obrigatória'
64
+ })
65
+
66
+ # Cria nova sessão
67
+ current_test_session = {
68
+ 'id': datetime.now().strftime('%Y%m%d_%H%M%S'),
69
+ 'question': question,
70
+ 'groups': [],
71
+ 'created_at': datetime.now().isoformat(),
72
+ 'status': 'created'
73
+ }
74
+
75
+ logging.info(f"Nova sessão de teste criada: {current_test_session['id']}")
76
+
77
+ return jsonify({
78
+ 'success': True,
79
+ 'session_id': current_test_session['id'],
80
+ 'message': 'Sessão de teste criada com sucesso'
81
+ })
82
+
83
+ except Exception as e:
84
+ logging.error(f"Erro ao criar sessão de teste: {e}")
85
+ return jsonify({
86
+ 'success': False,
87
+ 'error': str(e)
88
+ })
89
+
90
+ @app.route('/api/add_test_group', methods=['POST'])
91
+ def add_test_group():
92
+ """Adiciona grupo de teste à sessão atual"""
93
+ global current_test_session
94
+
95
+ try:
96
+ if not current_test_session:
97
+ return jsonify({
98
+ 'success': False,
99
+ 'error': 'Nenhuma sessão de teste ativa'
100
+ })
101
+
102
+ data = request.json
103
+
104
+ # Validação dos dados
105
+ required_fields = ['sql_model', 'iterations']
106
+ for field in required_fields:
107
+ if field not in data:
108
+ return jsonify({
109
+ 'success': False,
110
+ 'error': f'Campo obrigatório: {field}'
111
+ })
112
+
113
+ sql_model = data['sql_model']
114
+ iterations = int(data['iterations'])
115
+ processing_enabled = data.get('processing_enabled', False)
116
+ processing_model = data.get('processing_model', None)
117
+
118
+ # Validações
119
+ if sql_model not in AVAILABLE_MODELS.values():
120
+ return jsonify({
121
+ 'success': False,
122
+ 'error': 'Modelo SQL inválido'
123
+ })
124
+
125
+ if iterations < 1 or iterations > 100:
126
+ return jsonify({
127
+ 'success': False,
128
+ 'error': 'Número de iterações deve ser entre 1 e 100'
129
+ })
130
+
131
+ if processing_enabled and processing_model not in AVAILABLE_MODELS.values():
132
+ return jsonify({
133
+ 'success': False,
134
+ 'error': 'Modelo de processamento inválido'
135
+ })
136
+
137
+ # Cria grupo
138
+ group = {
139
+ 'id': len(current_test_session['groups']) + 1,
140
+ 'sql_model': sql_model,
141
+ 'sql_model_name': next(k for k, v in AVAILABLE_MODELS.items() if v == sql_model),
142
+ 'processing_enabled': processing_enabled,
143
+ 'processing_model': processing_model,
144
+ 'processing_model_name': next((k for k, v in AVAILABLE_MODELS.items() if v == processing_model), None) if processing_model else None,
145
+ 'iterations': iterations,
146
+ 'created_at': datetime.now().isoformat()
147
+ }
148
+
149
+ current_test_session['groups'].append(group)
150
+
151
+ logging.info(f"Grupo adicionado à sessão {current_test_session['id']}: {group}")
152
+
153
+ return jsonify({
154
+ 'success': True,
155
+ 'group': group,
156
+ 'total_groups': len(current_test_session['groups']),
157
+ 'message': 'Grupo adicionado com sucesso'
158
+ })
159
+
160
+ except Exception as e:
161
+ logging.error(f"Erro ao adicionar grupo: {e}")
162
+ return jsonify({
163
+ 'success': False,
164
+ 'error': str(e)
165
+ })
166
+
167
+ @app.route('/api/get_session_info')
168
+ def get_session_info():
169
+ """Retorna informações da sessão atual"""
170
+ global current_test_session
171
+
172
+ if not current_test_session:
173
+ return jsonify({
174
+ 'success': False,
175
+ 'error': 'Nenhuma sessão ativa'
176
+ })
177
+
178
+ return jsonify({
179
+ 'success': True,
180
+ 'session': current_test_session
181
+ })
182
+
183
+ @app.route('/api/run_tests', methods=['POST'])
184
+ def run_tests():
185
+ """Executa os testes da sessão atual"""
186
+ global test_runner, current_test_session
187
+
188
+ try:
189
+ if not current_test_session:
190
+ return jsonify({
191
+ 'success': False,
192
+ 'error': 'Nenhuma sessão de teste ativa'
193
+ })
194
+
195
+ if not current_test_session['groups']:
196
+ return jsonify({
197
+ 'success': False,
198
+ 'error': 'Nenhum grupo de teste configurado'
199
+ })
200
+
201
+ data = request.json
202
+ validation_method = data.get('validation_method', 'llm')
203
+ expected_content = data.get('expected_content', '') if validation_method == 'keyword' else None
204
+
205
+ # Atualiza status
206
+ current_test_session['status'] = 'running'
207
+ current_test_session['started_at'] = datetime.now().isoformat()
208
+
209
+ # Cria test runner
210
+ test_runner = MassiveTestRunner(max_workers=5) # Paralelismo de 5 workers
211
+
212
+ # Executa testes de forma assíncrona
213
+ def run_async_tests():
214
+ print(f"\n🚀 INICIANDO EXECUÇÃO DE TESTES - {datetime.now().strftime('%H:%M:%S')}")
215
+ print(f"📊 Sessão: {current_test_session['id']}")
216
+ print(f"❓ Pergunta: {current_test_session['question']}")
217
+ print(f"👥 Grupos: {len(current_test_session['groups'])}")
218
+ print(f"🔢 Total de testes: {sum(g['iterations'] for g in current_test_session['groups'])}")
219
+ print("=" * 60)
220
+
221
+ loop = asyncio.new_event_loop()
222
+ asyncio.set_event_loop(loop)
223
+ try:
224
+ result = loop.run_until_complete(
225
+ test_runner.run_test_session(
226
+ current_test_session,
227
+ validation_method,
228
+ expected_content
229
+ )
230
+ )
231
+ # Marca como concluído após execução
232
+ current_test_session['status'] = 'completed'
233
+ current_test_session['completed_at'] = datetime.now().isoformat()
234
+
235
+ print(f"\n🎉 TESTES CONCLUÍDOS COM SUCESSO - {datetime.now().strftime('%H:%M:%S')}")
236
+ print(f"✅ Sessão: {current_test_session['id']}")
237
+ print("=" * 60)
238
+
239
+ logging.info(f"✅ Testes concluídos com sucesso: {current_test_session['id']}")
240
+ return result
241
+ except Exception as e:
242
+ current_test_session['status'] = 'error'
243
+ current_test_session['error'] = str(e)
244
+
245
+ print(f"\n❌ ERRO NA EXECUÇÃO DOS TESTES - {datetime.now().strftime('%H:%M:%S')}")
246
+ print(f"💥 Erro: {e}")
247
+ print("=" * 60)
248
+
249
+ logging.error(f"❌ Erro na execução dos testes: {e}")
250
+ raise
251
+ finally:
252
+ loop.close()
253
+
254
+ # Inicia execução em thread separada
255
+ import threading
256
+ test_thread = threading.Thread(target=run_async_tests)
257
+ test_thread.daemon = False # Não daemon para garantir conclusão
258
+ test_thread.start()
259
+
260
+ return jsonify({
261
+ 'success': True,
262
+ 'message': 'Testes iniciados com sucesso',
263
+ 'session_id': current_test_session['id']
264
+ })
265
+
266
+ except Exception as e:
267
+ logging.error(f"Erro ao executar testes: {e}")
268
+ current_test_session['status'] = 'error'
269
+ return jsonify({
270
+ 'success': False,
271
+ 'error': str(e)
272
+ })
273
+
274
+ @app.route('/api/test_status')
275
+ def get_test_status():
276
+ """Retorna status atual dos testes"""
277
+ global test_runner, current_test_session
278
+
279
+ if not current_test_session:
280
+ return jsonify({
281
+ 'success': False,
282
+ 'error': 'Nenhuma sessão ativa'
283
+ })
284
+
285
+ status_info = {
286
+ 'session_id': current_test_session['id'],
287
+ 'status': current_test_session.get('status', 'unknown'),
288
+ 'progress': 0,
289
+ 'current_group': None,
290
+ 'completed_tests': 0,
291
+ 'total_tests': sum(group['iterations'] for group in current_test_session['groups']),
292
+ 'estimated_remaining': None
293
+ }
294
+
295
+ if test_runner:
296
+ try:
297
+ runner_status = test_runner.get_status()
298
+ status_info.update(runner_status)
299
+
300
+ # Se o runner indica que terminou, atualiza a sessão
301
+ if runner_status.get('current_status') == 'completed' and current_test_session.get('status') != 'completed':
302
+ current_test_session['status'] = 'completed'
303
+ current_test_session['completed_at'] = datetime.now().isoformat()
304
+ logging.info(f"🎉 Sessão {current_test_session['id']} marcada como concluída")
305
+
306
+ except Exception as e:
307
+ logging.error(f"Erro ao obter status do runner: {e}")
308
+ status_info['error'] = str(e)
309
+
310
+ # Adiciona informações de testes em execução
311
+ if test_runner:
312
+ status_info['running_tests'] = runner_status.get('running_tests_details', [])
313
+ status_info['running_tests_count'] = runner_status.get('running_tests_count', 0)
314
+ status_info['current_test'] = runner_status.get('current_test')
315
+ status_info['cancelled_tests'] = len(runner_status.get('cancelled_tests', []))
316
+ status_info['timeout_tests'] = len(runner_status.get('timeout_tests', []))
317
+
318
+ return jsonify({
319
+ 'success': True,
320
+ 'status': status_info
321
+ })
322
+
323
+ @app.route('/api/cancel_test', methods=['POST'])
324
+ def cancel_current_test():
325
+ """Cancela o teste atual ou específico"""
326
+ global test_runner
327
+
328
+ if not test_runner:
329
+ return jsonify({
330
+ 'success': False,
331
+ 'error': 'Nenhum teste em execução'
332
+ })
333
+
334
+ try:
335
+ data = request.get_json() or {}
336
+ thread_id = data.get('thread_id') # Opcional - cancela teste específico
337
+
338
+ cancelled = test_runner.cancel_current_test(thread_id)
339
+
340
+ if cancelled:
341
+ return jsonify({
342
+ 'success': True,
343
+ 'message': f'Teste {"específico" if thread_id else "atual"} cancelado com sucesso',
344
+ 'cancelled_test': thread_id
345
+ })
346
+ else:
347
+ return jsonify({
348
+ 'success': False,
349
+ 'error': 'Nenhum teste encontrado para cancelar'
350
+ })
351
+
352
+ except Exception as e:
353
+ return jsonify({
354
+ 'success': False,
355
+ 'error': f'Erro ao cancelar teste: {str(e)}'
356
+ })
357
+
358
+ @app.route('/api/cancel_all_tests', methods=['POST'])
359
+ def cancel_all_tests():
360
+ """Cancela todos os testes em execução"""
361
+ global test_runner
362
+
363
+ if not test_runner:
364
+ return jsonify({
365
+ 'success': False,
366
+ 'error': 'Nenhum teste em execução'
367
+ })
368
+
369
+ try:
370
+ cancelled_count = test_runner.cancel_all_tests()
371
+
372
+ return jsonify({
373
+ 'success': True,
374
+ 'message': f'{cancelled_count} testes cancelados',
375
+ 'cancelled_count': cancelled_count
376
+ })
377
+
378
+ except Exception as e:
379
+ return jsonify({
380
+ 'success': False,
381
+ 'error': f'Erro ao cancelar testes: {str(e)}'
382
+ })
383
+
384
+ @app.route('/api/skip_stuck_tests', methods=['POST'])
385
+ def skip_stuck_tests():
386
+ """Marca testes travados para cancelamento"""
387
+ global test_runner
388
+
389
+ if not test_runner:
390
+ return jsonify({
391
+ 'success': False,
392
+ 'error': 'Nenhum teste em execução'
393
+ })
394
+
395
+ try:
396
+ data = request.get_json() or {}
397
+ max_duration = data.get('max_duration', 120) # 2 minutos padrão
398
+
399
+ stuck_count = test_runner.skip_stuck_tests(max_duration)
400
+
401
+ return jsonify({
402
+ 'success': True,
403
+ 'message': f'{stuck_count} testes travados marcados para cancelamento',
404
+ 'stuck_count': stuck_count,
405
+ 'max_duration': max_duration
406
+ })
407
+
408
+ except Exception as e:
409
+ return jsonify({
410
+ 'success': False,
411
+ 'error': f'Erro ao marcar testes travados: {str(e)}'
412
+ })
413
+
414
+ @app.route('/api/test_results')
415
+ def get_test_results():
416
+ """Retorna resultados dos testes"""
417
+ global test_runner, current_test_session
418
+
419
+ if not test_runner:
420
+ return jsonify({
421
+ 'success': False,
422
+ 'error': 'Nenhum teste executado'
423
+ })
424
+
425
+ if not current_test_session:
426
+ return jsonify({
427
+ 'success': False,
428
+ 'error': 'Nenhuma sessão ativa'
429
+ })
430
+
431
+ try:
432
+ results = test_runner.get_results()
433
+
434
+ # Verifica se há resultados válidos
435
+ if not results or not results.get('group_results'):
436
+ return jsonify({
437
+ 'success': False,
438
+ 'error': 'Resultados ainda não disponíveis'
439
+ })
440
+
441
+ logging.info(f"📊 Enviando resultados: {len(results.get('group_results', []))} grupos, {len(results.get('individual_results', []))} testes individuais")
442
+
443
+ return jsonify({
444
+ 'success': True,
445
+ 'results': results
446
+ })
447
+ except Exception as e:
448
+ logging.error(f"Erro ao obter resultados: {e}")
449
+ return jsonify({
450
+ 'success': False,
451
+ 'error': str(e)
452
+ })
453
+
454
+ @app.route('/api/download_csv')
455
+ def download_csv():
456
+ """Gera e baixa relatório CSV"""
457
+ global test_runner
458
+
459
+ if not test_runner:
460
+ return jsonify({
461
+ 'success': False,
462
+ 'error': 'Nenhum teste executado'
463
+ })
464
+
465
+ try:
466
+ # Gera relatório
467
+ report_generator = ReportGenerator()
468
+ csv_path = report_generator.generate_csv_report(test_runner.get_results())
469
+
470
+ return send_file(
471
+ csv_path,
472
+ as_attachment=True,
473
+ download_name=f'teste_agentgraph_{datetime.now().strftime("%Y%m%d_%H%M%S")}.csv'
474
+ )
475
+ except Exception as e:
476
+ logging.error(f"Erro ao gerar CSV: {e}")
477
+ return jsonify({
478
+ 'success': False,
479
+ 'error': str(e)
480
+ })
481
+
482
+ if __name__ == '__main__':
483
+ print("🧪 Sistema de Testes Massivos - AgentGraph")
484
+ print("=" * 50)
485
+ print("🌐 Acesse: http://localhost:5001")
486
+ print("📊 Interface de testes disponível")
487
+ print("=" * 50)
488
+
489
+ app.run(
490
+ host='0.0.0.0',
491
+ port=5001,
492
+ debug=True,
493
+ threaded=True
494
+ )
testes/report_generator.py ADDED
@@ -0,0 +1,335 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """
3
+ Gerador de relatórios para testes massivos
4
+ """
5
+ import pandas as pd
6
+ import logging
7
+ import os
8
+ from datetime import datetime
9
+ from typing import Dict, Any, List
10
+ import json
11
+
12
+ class ReportGenerator:
13
+ """
14
+ Gerador de relatórios em CSV e outros formatos
15
+ """
16
+
17
+ def __init__(self, output_dir: str = "testes/reports"):
18
+ """
19
+ Inicializa o gerador de relatórios
20
+
21
+ Args:
22
+ output_dir: Diretório para salvar relatórios
23
+ """
24
+ self.output_dir = output_dir
25
+ self._ensure_output_dir()
26
+
27
+ def _ensure_output_dir(self):
28
+ """Garante que o diretório de saída existe"""
29
+ os.makedirs(self.output_dir, exist_ok=True)
30
+
31
+ def generate_csv_report(self, results: Dict[str, Any]) -> str:
32
+ """
33
+ Gera relatório completo em CSV/Excel
34
+
35
+ Args:
36
+ results: Resultados dos testes
37
+
38
+ Returns:
39
+ Caminho do arquivo Excel gerado
40
+ """
41
+ try:
42
+ timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
43
+
44
+ # Corrige caminhos para Windows
45
+ excel_filename = f"relatorio_testes_{timestamp}.xlsx"
46
+ excel_path = os.path.join(self.output_dir, excel_filename)
47
+ excel_path = os.path.normpath(excel_path) # Normaliza barras
48
+
49
+ print(f"📊 Gerando relatório Excel: {excel_path}")
50
+
51
+ # Cria Excel com múltiplas abas
52
+ with pd.ExcelWriter(excel_path, engine='openpyxl') as writer:
53
+
54
+ # Aba 1: Resumo por Grupo
55
+ group_summary_df = self._create_group_summary_dataframe(results)
56
+ if not group_summary_df.empty:
57
+ group_summary_df.to_excel(writer, sheet_name='Resumo_Grupos', index=False)
58
+ print(f"✅ Aba 'Resumo_Grupos' criada com {len(group_summary_df)} linhas")
59
+
60
+ # Aba 2: Resultados Individuais
61
+ individual_df = self._create_individual_results_dataframe(results)
62
+ if not individual_df.empty:
63
+ individual_df.to_excel(writer, sheet_name='Resultados_Individuais', index=False)
64
+ print(f"✅ Aba 'Resultados_Individuais' criada com {len(individual_df)} linhas")
65
+
66
+ # Aba 3: Resumo Geral
67
+ general_summary_df = self._create_general_summary_dataframe(results)
68
+ if not general_summary_df.empty:
69
+ general_summary_df.to_excel(writer, sheet_name='Resumo_Geral', index=False)
70
+ print(f"✅ Aba 'Resumo_Geral' criada com {len(general_summary_df)} linhas")
71
+
72
+ # Também gera CSVs separados com separador correto
73
+ csv_dir = os.path.join(self.output_dir, f"csv_{timestamp}")
74
+ os.makedirs(csv_dir, exist_ok=True)
75
+
76
+ if not group_summary_df.empty:
77
+ csv_grupos = os.path.join(csv_dir, "resumo_grupos.csv")
78
+ group_summary_df.to_csv(csv_grupos, index=False, encoding='utf-8-sig', sep=';')
79
+ print(f"✅ CSV grupos: {csv_grupos}")
80
+
81
+ if not individual_df.empty:
82
+ csv_individual = os.path.join(csv_dir, "resultados_individuais.csv")
83
+ individual_df.to_csv(csv_individual, index=False, encoding='utf-8-sig', sep=';')
84
+ print(f"✅ CSV individual: {csv_individual}")
85
+
86
+ if not general_summary_df.empty:
87
+ csv_geral = os.path.join(csv_dir, "resumo_geral.csv")
88
+ general_summary_df.to_csv(csv_geral, index=False, encoding='utf-8-sig', sep=';')
89
+ print(f"✅ CSV geral: {csv_geral}")
90
+
91
+ print(f"🎉 Relatório completo gerado: {excel_path}")
92
+ logging.info(f"Relatório gerado: {excel_path}")
93
+ return excel_path
94
+
95
+ except Exception as e:
96
+ print(f"❌ Erro ao gerar relatório: {e}")
97
+ logging.error(f"Erro ao gerar relatório CSV: {e}")
98
+ raise
99
+
100
+ def _create_group_summary_dataframe(self, results: Dict[str, Any]) -> pd.DataFrame:
101
+ """
102
+ Cria DataFrame com resumo por grupo
103
+
104
+ Args:
105
+ results: Resultados dos testes
106
+
107
+ Returns:
108
+ DataFrame com resumo dos grupos
109
+ """
110
+ group_results = results.get('group_results', [])
111
+
112
+ if not group_results:
113
+ return pd.DataFrame()
114
+
115
+ summary_data = []
116
+
117
+ for group in group_results:
118
+ config = group.get('group_config', {})
119
+
120
+ summary_data.append({
121
+ 'Grupo_ID': group.get('group_id'),
122
+ 'Modelo_SQL': config.get('sql_model_name'),
123
+ 'Processing_Agent_Ativo': 'Sim' if config.get('processing_enabled') else 'Não',
124
+ 'Modelo_Processing': config.get('processing_model_name', 'N/A'),
125
+ 'Total_Testes': group.get('total_tests'),
126
+ 'Testes_Sucessos': group.get('successful_tests'),
127
+ 'Respostas_Válidas': group.get('valid_responses'),
128
+ 'Taxa_Sucesso_%': group.get('success_rate'),
129
+ 'Taxa_Validação_%': group.get('validation_rate'),
130
+ 'Consistência_Resposta_%': group.get('response_consistency'),
131
+ 'Consistência_SQL_%': group.get('sql_consistency'),
132
+ 'Tempo_Médio_Execução_s': group.get('avg_execution_time'),
133
+ 'Erros_Totais': group.get('error_count', 0)
134
+ })
135
+
136
+ return pd.DataFrame(summary_data)
137
+
138
+ def _create_individual_results_dataframe(self, results: Dict[str, Any]) -> pd.DataFrame:
139
+ """
140
+ Cria DataFrame com resultados individuais
141
+
142
+ Args:
143
+ results: Resultados dos testes
144
+
145
+ Returns:
146
+ DataFrame com resultados individuais
147
+ """
148
+ individual_results = results.get('individual_results', [])
149
+
150
+ if not individual_results:
151
+ return pd.DataFrame()
152
+
153
+ individual_data = []
154
+
155
+ for result in individual_results:
156
+ validation = result.get('validation', {})
157
+
158
+ individual_data.append({
159
+ 'Grupo_ID': result.get('group_id'),
160
+ 'Iteração': result.get('iteration'),
161
+ 'Thread_ID': result.get('thread_id'),
162
+ 'Timestamp': result.get('timestamp'),
163
+ 'Modelo_SQL': result.get('sql_model'),
164
+ 'Processing_Agent_Ativo': 'Sim' if result.get('processing_enabled') else 'Não',
165
+ 'Modelo_Processing': result.get('processing_model', 'N/A'),
166
+ 'Pergunta': result.get('question'),
167
+ 'Query_SQL': result.get('sql_query'),
168
+ 'Resposta_Final': result.get('response'),
169
+ 'Sucesso': 'Sim' if result.get('success') else 'Não',
170
+ 'Erro': result.get('error', ''),
171
+ 'Tempo_Execução_s': result.get('execution_time'),
172
+ 'Validação_Válida': 'Sim' if validation.get('valid') else 'Não',
173
+ 'Pontuação_Validação': validation.get('score', 0),
174
+ 'Razão_Validação': validation.get('reason', ''),
175
+ 'Método_Validação': validation.get('method', '')
176
+ })
177
+
178
+ return pd.DataFrame(individual_data)
179
+
180
+ def _create_general_summary_dataframe(self, results: Dict[str, Any]) -> pd.DataFrame:
181
+ """
182
+ Cria DataFrame com resumo geral
183
+
184
+ Args:
185
+ results: Resultados dos testes
186
+
187
+ Returns:
188
+ DataFrame com resumo geral
189
+ """
190
+ session_info = results.get('session_info', {})
191
+ summary = results.get('summary', {})
192
+
193
+ general_data = [
194
+ {'Métrica': 'ID da Sessão', 'Valor': session_info.get('id', '')},
195
+ {'Métrica': 'Pergunta do Teste', 'Valor': session_info.get('question', '')},
196
+ {'Métrica': 'Método de Validação', 'Valor': session_info.get('validation_method', '')},
197
+ {'Métrica': 'Total de Grupos', 'Valor': summary.get('total_groups', 0)},
198
+ {'Métrica': 'Total de Testes', 'Valor': summary.get('total_tests', 0)},
199
+ {'Métrica': 'Total de Sucessos', 'Valor': summary.get('total_successful', 0)},
200
+ {'Métrica': 'Total de Válidos', 'Valor': summary.get('total_valid', 0)},
201
+ {'Métrica': 'Taxa Geral de Sucesso (%)', 'Valor': summary.get('overall_success_rate', 0)},
202
+ {'Métrica': 'Taxa Geral de Validação (%)', 'Valor': summary.get('overall_validation_rate', 0)},
203
+ {'Métrica': 'Consistência Média de Resposta (%)', 'Valor': summary.get('avg_response_consistency', 0)},
204
+ {'Métrica': 'Consistência Média de SQL (%)', 'Valor': summary.get('avg_sql_consistency', 0)},
205
+ ]
206
+
207
+ # Adiciona informações do melhor grupo
208
+ best_group = summary.get('best_performing_group', {})
209
+ if best_group:
210
+ config = best_group.get('group_config', {})
211
+ general_data.extend([
212
+ {'Métrica': 'Melhor Grupo - ID', 'Valor': best_group.get('group_id', '')},
213
+ {'Métrica': 'Melhor Grupo - Modelo SQL', 'Valor': config.get('sql_model_name', '')},
214
+ {'Métrica': 'Melhor Grupo - Processing Agent', 'Valor': 'Sim' if config.get('processing_enabled') else 'Não'},
215
+ {'Métrica': 'Melhor Grupo - Taxa Validação (%)', 'Valor': best_group.get('validation_rate', 0)},
216
+ ])
217
+
218
+ # Adiciona informações do grupo mais consistente
219
+ consistent_group = summary.get('most_consistent_group', {})
220
+ if consistent_group:
221
+ config = consistent_group.get('group_config', {})
222
+ general_data.extend([
223
+ {'Métrica': 'Grupo Mais Consistente - ID', 'Valor': consistent_group.get('group_id', '')},
224
+ {'Métrica': 'Grupo Mais Consistente - Modelo SQL', 'Valor': config.get('sql_model_name', '')},
225
+ {'Métrica': 'Grupo Mais Consistente - Consistência (%)', 'Valor': consistent_group.get('response_consistency', 0)},
226
+ ])
227
+
228
+ return pd.DataFrame(general_data)
229
+
230
+ def generate_json_report(self, results: Dict[str, Any]) -> str:
231
+ """
232
+ Gera relatório em formato JSON
233
+
234
+ Args:
235
+ results: Resultados dos testes
236
+
237
+ Returns:
238
+ Caminho do arquivo JSON gerado
239
+ """
240
+ try:
241
+ timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
242
+ json_path = os.path.join(self.output_dir, f"relatorio_testes_{timestamp}.json")
243
+
244
+ with open(json_path, 'w', encoding='utf-8') as f:
245
+ json.dump(results, f, indent=2, ensure_ascii=False, default=str)
246
+
247
+ logging.info(f"Relatório JSON gerado: {json_path}")
248
+ return json_path
249
+
250
+ except Exception as e:
251
+ logging.error(f"Erro ao gerar relatório JSON: {e}")
252
+ raise
253
+
254
+ def generate_html_summary(self, results: Dict[str, Any]) -> str:
255
+ """
256
+ Gera resumo em HTML
257
+
258
+ Args:
259
+ results: Resultados dos testes
260
+
261
+ Returns:
262
+ Caminho do arquivo HTML gerado
263
+ """
264
+ try:
265
+ timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
266
+ html_path = os.path.join(self.output_dir, f"resumo_testes_{timestamp}.html")
267
+
268
+ # Cria DataFrames
269
+ group_df = self._create_group_summary_dataframe(results)
270
+ general_df = self._create_general_summary_dataframe(results)
271
+
272
+ # Gera HTML
273
+ html_content = f"""
274
+ <!DOCTYPE html>
275
+ <html>
276
+ <head>
277
+ <title>Relatório de Testes - AgentGraph</title>
278
+ <style>
279
+ body {{ font-family: Arial, sans-serif; margin: 20px; }}
280
+ table {{ border-collapse: collapse; width: 100%; margin: 20px 0; }}
281
+ th, td {{ border: 1px solid #ddd; padding: 8px; text-align: left; }}
282
+ th {{ background-color: #f2f2f2; }}
283
+ .summary {{ background-color: #e7f3ff; padding: 15px; margin: 20px 0; }}
284
+ .metric {{ margin: 5px 0; }}
285
+ </style>
286
+ </head>
287
+ <body>
288
+ <h1>Relatório de Testes Massivos - AgentGraph</h1>
289
+ <p>Gerado em: {datetime.now().strftime('%d/%m/%Y %H:%M:%S')}</p>
290
+
291
+ <div class="summary">
292
+ <h2>Resumo Geral</h2>
293
+ {general_df.to_html(index=False, escape=False)}
294
+ </div>
295
+
296
+ <h2>Resumo por Grupo</h2>
297
+ {group_df.to_html(index=False, escape=False)}
298
+
299
+ </body>
300
+ </html>
301
+ """
302
+
303
+ with open(html_path, 'w', encoding='utf-8') as f:
304
+ f.write(html_content)
305
+
306
+ logging.info(f"Resumo HTML gerado: {html_path}")
307
+ return html_path
308
+
309
+ except Exception as e:
310
+ logging.error(f"Erro ao gerar resumo HTML: {e}")
311
+ raise
312
+
313
+ def generate_all_reports(self, results: Dict[str, Any]) -> Dict[str, str]:
314
+ """
315
+ Gera todos os tipos de relatório
316
+
317
+ Args:
318
+ results: Resultados dos testes
319
+
320
+ Returns:
321
+ Dicionário com caminhos dos arquivos gerados
322
+ """
323
+ report_paths = {}
324
+
325
+ try:
326
+ report_paths['csv'] = self.generate_csv_report(results)
327
+ report_paths['json'] = self.generate_json_report(results)
328
+ report_paths['html'] = self.generate_html_summary(results)
329
+
330
+ logging.info(f"Todos os relatórios gerados: {report_paths}")
331
+ return report_paths
332
+
333
+ except Exception as e:
334
+ logging.error(f"Erro ao gerar relatórios: {e}")
335
+ raise
testes/reports/.gitkeep ADDED
@@ -0,0 +1 @@
 
 
1
+ # Pasta para armazenar relatórios gerados pelos testes
testes/requirements.txt ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ flask>=2.3.0
2
+ pandas>=1.5.0
3
+ openpyxl>=3.1.0
4
+ asyncio
5
+ concurrent.futures
testes/static/js/app.js ADDED
@@ -0,0 +1,748 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // Sistema de Testes Massivos - JavaScript
2
+ class TestManager {
3
+ constructor() {
4
+ this.sessionId = null;
5
+ this.groups = [];
6
+ this.isRunning = false;
7
+ this.statusInterval = null;
8
+
9
+ this.initializeEventListeners();
10
+ }
11
+
12
+ initializeEventListeners() {
13
+ // Criar sessão
14
+ document.getElementById('createSession').addEventListener('click', () => {
15
+ this.createSession();
16
+ });
17
+
18
+ // Toggle Processing Agent
19
+ document.getElementById('enableProcessing').addEventListener('change', (e) => {
20
+ const processingDiv = document.getElementById('processingModelDiv');
21
+ processingDiv.style.display = e.target.checked ? 'block' : 'none';
22
+ });
23
+
24
+ // Toggle método de validação
25
+ document.getElementById('validationMethod').addEventListener('change', (e) => {
26
+ const keywordDiv = document.getElementById('keywordDiv');
27
+ keywordDiv.style.display = e.target.value === 'keyword' ? 'block' : 'none';
28
+ });
29
+
30
+ // Adicionar grupo
31
+ document.getElementById('addGroup').addEventListener('click', () => {
32
+ this.addGroup();
33
+ });
34
+
35
+ // Executar testes
36
+ document.getElementById('runTests').addEventListener('click', () => {
37
+ this.runTests();
38
+ });
39
+
40
+ // Download CSV
41
+ document.getElementById('downloadCsv').addEventListener('click', () => {
42
+ this.downloadCsv();
43
+ });
44
+ }
45
+
46
+ async createSession() {
47
+ const question = document.getElementById('testQuestion').value.trim();
48
+
49
+ if (!question) {
50
+ this.showAlert('Por favor, digite uma pergunta para o teste.', 'warning');
51
+ return;
52
+ }
53
+
54
+ try {
55
+ const response = await fetch('/api/create_test_session', {
56
+ method: 'POST',
57
+ headers: {
58
+ 'Content-Type': 'application/json'
59
+ },
60
+ body: JSON.stringify({ question })
61
+ });
62
+
63
+ const data = await response.json();
64
+
65
+ if (data.success) {
66
+ this.sessionId = data.session_id;
67
+ this.showSessionInfo(question);
68
+ this.showAlert('Sessão criada com sucesso!', 'success');
69
+
70
+ // Mostra configuração de grupo
71
+ document.getElementById('groupConfig').style.display = 'block';
72
+ document.getElementById('validationConfig').style.display = 'block';
73
+
74
+ // Desabilita criação de nova sessão
75
+ document.getElementById('createSession').disabled = true;
76
+ document.getElementById('testQuestion').disabled = true;
77
+
78
+ } else {
79
+ this.showAlert(data.error, 'danger');
80
+ }
81
+ } catch (error) {
82
+ this.showAlert('Erro ao criar sessão: ' + error.message, 'danger');
83
+ }
84
+ }
85
+
86
+ showSessionInfo(question) {
87
+ const sessionInfo = document.getElementById('sessionInfo');
88
+ const sessionDetails = document.getElementById('sessionDetails');
89
+
90
+ sessionDetails.innerHTML = `
91
+ <strong>ID:</strong> ${this.sessionId}<br>
92
+ <strong>Pergunta:</strong> ${question}<br>
93
+ <strong>Criado em:</strong> ${new Date().toLocaleString()}
94
+ `;
95
+
96
+ sessionInfo.style.display = 'block';
97
+ }
98
+
99
+ async addGroup() {
100
+ const sqlModel = document.getElementById('sqlModel').value;
101
+ const processingEnabled = document.getElementById('enableProcessing').checked;
102
+ const processingModel = processingEnabled ? document.getElementById('processingModel').value : null;
103
+ const iterations = parseInt(document.getElementById('iterations').value);
104
+
105
+ if (iterations < 1 || iterations > 100) {
106
+ this.showAlert('Número de iterações deve ser entre 1 e 100.', 'warning');
107
+ return;
108
+ }
109
+
110
+ try {
111
+ const response = await fetch('/api/add_test_group', {
112
+ method: 'POST',
113
+ headers: {
114
+ 'Content-Type': 'application/json'
115
+ },
116
+ body: JSON.stringify({
117
+ sql_model: sqlModel,
118
+ processing_enabled: processingEnabled,
119
+ processing_model: processingModel,
120
+ iterations: iterations
121
+ })
122
+ });
123
+
124
+ const data = await response.json();
125
+
126
+ if (data.success) {
127
+ this.groups.push(data.group);
128
+ this.updateGroupsList();
129
+ this.showAlert(`Grupo ${data.group.id} adicionado com sucesso!`, 'success');
130
+
131
+ // Reset form
132
+ document.getElementById('iterations').value = 5;
133
+ document.getElementById('enableProcessing').checked = false;
134
+ document.getElementById('processingModelDiv').style.display = 'none';
135
+
136
+ } else {
137
+ this.showAlert(data.error, 'danger');
138
+ }
139
+ } catch (error) {
140
+ this.showAlert('Erro ao adicionar grupo: ' + error.message, 'danger');
141
+ }
142
+ }
143
+
144
+ updateGroupsList() {
145
+ const groupsList = document.getElementById('groupsList');
146
+
147
+ if (this.groups.length === 0) {
148
+ groupsList.innerHTML = '<p class="text-muted">Nenhum grupo configurado ainda.</p>';
149
+ return;
150
+ }
151
+
152
+ let html = '';
153
+ this.groups.forEach(group => {
154
+ html += `
155
+ <div class="test-group-card">
156
+ <div class="d-flex justify-content-between align-items-center">
157
+ <div>
158
+ <h6><i class="fas fa-cog"></i> Grupo ${group.id}</h6>
159
+ <small class="text-muted">
160
+ <strong>SQL:</strong> ${group.sql_model_name}<br>
161
+ <strong>Processing:</strong> ${group.processing_enabled ? group.processing_model_name : 'Desativado'}<br>
162
+ <strong>Iterações:</strong> ${group.iterations}
163
+ </small>
164
+ </div>
165
+ <div class="text-end">
166
+ <span class="badge bg-primary">${group.iterations} testes</span>
167
+ </div>
168
+ </div>
169
+ </div>
170
+ `;
171
+ });
172
+
173
+ groupsList.innerHTML = html;
174
+ }
175
+
176
+ async runTests() {
177
+ if (this.groups.length === 0) {
178
+ this.showAlert('Adicione pelo menos um grupo de teste.', 'warning');
179
+ return;
180
+ }
181
+
182
+ const validationMethod = document.getElementById('validationMethod').value;
183
+ const expectedContent = document.getElementById('expectedContent').value;
184
+
185
+ if (validationMethod === 'keyword' && !expectedContent.trim()) {
186
+ this.showAlert('Digite o conteúdo esperado para validação por palavra-chave.', 'warning');
187
+ return;
188
+ }
189
+
190
+ try {
191
+ const response = await fetch('/api/run_tests', {
192
+ method: 'POST',
193
+ headers: {
194
+ 'Content-Type': 'application/json'
195
+ },
196
+ body: JSON.stringify({
197
+ validation_method: validationMethod,
198
+ expected_content: expectedContent
199
+ })
200
+ });
201
+
202
+ const data = await response.json();
203
+
204
+ if (data.success) {
205
+ this.isRunning = true;
206
+ this.updateStatus('running');
207
+ this.showProgressContainer();
208
+ this.startStatusPolling();
209
+ this.showAlert('Testes iniciados com sucesso!', 'success');
210
+
211
+ // Desabilita controles
212
+ document.getElementById('addGroup').disabled = true;
213
+ document.getElementById('runTests').disabled = true;
214
+
215
+ } else {
216
+ this.showAlert(data.error, 'danger');
217
+ }
218
+ } catch (error) {
219
+ this.showAlert('Erro ao executar testes: ' + error.message, 'danger');
220
+ }
221
+ }
222
+
223
+ showProgressContainer() {
224
+ document.getElementById('progressContainer').style.display = 'block';
225
+ }
226
+
227
+ startStatusPolling() {
228
+ this.statusInterval = setInterval(() => {
229
+ this.checkTestStatus();
230
+ }, 2000); // Verifica a cada 2 segundos
231
+ }
232
+
233
+ async checkTestStatus() {
234
+ try {
235
+ const response = await fetch('/api/test_status');
236
+ const data = await response.json();
237
+
238
+ if (data.success) {
239
+ const status = data.status;
240
+
241
+ // Atualiza progresso
242
+ this.updateProgress(status);
243
+
244
+ // Log para debug
245
+ console.log('Status atual:', status.status, 'Progresso:', status.progress);
246
+
247
+ // Verifica se terminou
248
+ if (status.status === 'completed') {
249
+ this.isRunning = false;
250
+ this.updateStatus('completed');
251
+ clearInterval(this.statusInterval);
252
+ this.statusInterval = null;
253
+
254
+ console.log('✅ Testes concluídos, carregando resultados...');
255
+ await this.loadResults();
256
+ this.showAlert('Testes concluídos com sucesso!', 'success');
257
+
258
+ // Reabilita controles
259
+ document.getElementById('addGroup').disabled = false;
260
+ document.getElementById('runTests').disabled = false;
261
+
262
+ } else if (status.status === 'error') {
263
+ this.isRunning = false;
264
+ this.updateStatus('error');
265
+ clearInterval(this.statusInterval);
266
+ this.statusInterval = null;
267
+ this.showAlert('Erro durante execução dos testes.', 'danger');
268
+
269
+ // Reabilita controles
270
+ document.getElementById('addGroup').disabled = false;
271
+ document.getElementById('runTests').disabled = false;
272
+ }
273
+ } else {
274
+ console.error('Erro na resposta do status:', data.error);
275
+ }
276
+ } catch (error) {
277
+ console.error('Erro ao verificar status:', error);
278
+ // Para o polling em caso de erro persistente
279
+ if (this.statusInterval) {
280
+ clearInterval(this.statusInterval);
281
+ this.statusInterval = null;
282
+ this.showAlert('Erro de comunicação com o servidor.', 'danger');
283
+ }
284
+ }
285
+ }
286
+
287
+ updateProgress(status) {
288
+ const progressBar = document.getElementById('progressBar');
289
+ const completedTests = document.getElementById('completedTests');
290
+ const totalTests = document.getElementById('totalTests');
291
+ const currentGroup = document.getElementById('currentGroup');
292
+ const estimatedTime = document.getElementById('estimatedTime');
293
+
294
+ progressBar.style.width = status.progress + '%';
295
+ progressBar.textContent = Math.round(status.progress) + '%';
296
+
297
+ completedTests.textContent = status.completed_tests;
298
+ totalTests.textContent = status.total_tests;
299
+ currentGroup.textContent = status.current_group || '-';
300
+
301
+ if (status.estimated_remaining) {
302
+ const minutes = Math.floor(status.estimated_remaining / 60);
303
+ const seconds = Math.floor(status.estimated_remaining % 60);
304
+ estimatedTime.textContent = `${minutes}m ${seconds}s`;
305
+ } else {
306
+ estimatedTime.textContent = '-';
307
+ }
308
+
309
+ // Atualiza informações de testes em execução
310
+ this.updateRunningTests(status);
311
+ }
312
+
313
+ updateRunningTests(status) {
314
+ const currentTestDetails = document.getElementById('currentTestDetails');
315
+ const runningTestsCount = document.getElementById('runningTestsCount');
316
+ const runningTestsList = document.getElementById('runningTestsList');
317
+ const runningTestsContainer = document.getElementById('runningTestsContainer');
318
+
319
+ // Atualiza contador
320
+ const runningCount = status.running_tests_count || 0;
321
+ runningTestsCount.textContent = runningCount;
322
+ runningTestsCount.className = runningCount > 0 ? 'badge bg-primary' : 'badge bg-secondary';
323
+
324
+ // Atualiza teste atual
325
+ if (status.current_test) {
326
+ currentTestDetails.innerHTML = `
327
+ <span class="badge bg-info">${status.current_test}</span>
328
+ <small class="text-muted d-block">Teste em execução</small>
329
+ `;
330
+ } else {
331
+ currentTestDetails.innerHTML = '<span class="text-muted">Nenhum teste em execução</span>';
332
+ }
333
+
334
+ // Atualiza lista de testes em execução
335
+ if (runningCount > 0 && status.running_tests) {
336
+ runningTestsList.style.display = 'block';
337
+ runningTestsContainer.innerHTML = '';
338
+
339
+ status.running_tests.forEach(test => {
340
+ const duration = Math.floor((Date.now() / 1000) - test.start_time);
341
+ const minutes = Math.floor(duration / 60);
342
+ const seconds = duration % 60;
343
+
344
+ const testElement = document.createElement('div');
345
+ testElement.className = 'list-group-item d-flex justify-content-between align-items-center';
346
+ testElement.innerHTML = `
347
+ <div>
348
+ <strong>Grupo ${test.group_id}</strong> - Iteração ${test.iteration}
349
+ <br><small class="text-muted">${test.question}</small>
350
+ </div>
351
+ <div class="text-end">
352
+ <span class="badge bg-warning">${minutes}m ${seconds}s</span>
353
+ <button class="btn btn-sm btn-outline-danger ms-2" onclick="testManager.cancelSpecificTest('${test.thread_id}')">
354
+ <i class="fas fa-times"></i>
355
+ </button>
356
+ </div>
357
+ `;
358
+ runningTestsContainer.appendChild(testElement);
359
+ });
360
+ } else {
361
+ runningTestsList.style.display = 'none';
362
+ }
363
+
364
+ // Habilita/desabilita botões
365
+ const hasRunningTests = runningCount > 0;
366
+ document.getElementById('cancelCurrentBtn').disabled = !hasRunningTests;
367
+ document.getElementById('cancelAllBtn').disabled = !hasRunningTests;
368
+ document.getElementById('skipStuckBtn').disabled = !hasRunningTests;
369
+ }
370
+
371
+ async cancelCurrentTest() {
372
+ try {
373
+ const response = await fetch('/api/cancel_test', {
374
+ method: 'POST',
375
+ headers: {
376
+ 'Content-Type': 'application/json'
377
+ }
378
+ });
379
+
380
+ const data = await response.json();
381
+
382
+ if (data.success) {
383
+ this.showAlert(data.message, 'success');
384
+ } else {
385
+ this.showAlert(data.error, 'danger');
386
+ }
387
+ } catch (error) {
388
+ this.showAlert('Erro ao cancelar teste: ' + error.message, 'danger');
389
+ }
390
+ }
391
+
392
+ async cancelSpecificTest(threadId) {
393
+ try {
394
+ const response = await fetch('/api/cancel_test', {
395
+ method: 'POST',
396
+ headers: {
397
+ 'Content-Type': 'application/json'
398
+ },
399
+ body: JSON.stringify({ thread_id: threadId })
400
+ });
401
+
402
+ const data = await response.json();
403
+
404
+ if (data.success) {
405
+ this.showAlert(data.message, 'success');
406
+ } else {
407
+ this.showAlert(data.error, 'danger');
408
+ }
409
+ } catch (error) {
410
+ this.showAlert('Erro ao cancelar teste específico: ' + error.message, 'danger');
411
+ }
412
+ }
413
+
414
+ async cancelAllTests() {
415
+ if (!confirm('Tem certeza que deseja cancelar TODOS os testes em execução?')) {
416
+ return;
417
+ }
418
+
419
+ try {
420
+ const response = await fetch('/api/cancel_all_tests', {
421
+ method: 'POST',
422
+ headers: {
423
+ 'Content-Type': 'application/json'
424
+ }
425
+ });
426
+
427
+ const data = await response.json();
428
+
429
+ if (data.success) {
430
+ this.showAlert(data.message, 'warning');
431
+ } else {
432
+ this.showAlert(data.error, 'danger');
433
+ }
434
+ } catch (error) {
435
+ this.showAlert('Erro ao cancelar todos os testes: ' + error.message, 'danger');
436
+ }
437
+ }
438
+
439
+ async skipStuckTests() {
440
+ try {
441
+ const response = await fetch('/api/skip_stuck_tests', {
442
+ method: 'POST',
443
+ headers: {
444
+ 'Content-Type': 'application/json'
445
+ },
446
+ body: JSON.stringify({ max_duration: 120 })
447
+ });
448
+
449
+ const data = await response.json();
450
+
451
+ if (data.success) {
452
+ this.showAlert(data.message, 'info');
453
+ } else {
454
+ this.showAlert(data.error, 'danger');
455
+ }
456
+ } catch (error) {
457
+ this.showAlert('Erro ao pular testes travados: ' + error.message, 'danger');
458
+ }
459
+ }
460
+
461
+ async loadResults() {
462
+ try {
463
+ console.log('🔄 Carregando resultados...');
464
+ const response = await fetch('/api/test_results');
465
+ const data = await response.json();
466
+
467
+ console.log('📊 Resposta dos resultados:', data);
468
+
469
+ if (data.success) {
470
+ console.log('✅ Resultados carregados, exibindo...');
471
+ this.displayResults(data.results);
472
+ document.getElementById('resultsContainer').style.display = 'block';
473
+ console.log('✅ Interface de resultados exibida');
474
+ } else {
475
+ console.error('❌ Erro nos resultados:', data.error);
476
+ this.showAlert('Erro ao carregar resultados: ' + data.error, 'danger');
477
+ }
478
+ } catch (error) {
479
+ console.error('❌ Erro ao carregar resultados:', error);
480
+ this.showAlert('Erro ao carregar resultados: ' + error.message, 'danger');
481
+ }
482
+ }
483
+
484
+ displayResults(results) {
485
+ this.displaySummary(results.summary);
486
+ this.displayGroupResults(results.group_results);
487
+ this.displayIndividualResults(results.individual_results);
488
+ }
489
+
490
+ displaySummary(summary) {
491
+ const summaryContent = document.getElementById('summaryContent');
492
+
493
+ summaryContent.innerHTML = `
494
+ <div class="row">
495
+ <div class="col-md-3">
496
+ <div class="metric-card">
497
+ <div class="metric-value">${summary.total_tests}</div>
498
+ <div class="metric-label">Total de Testes</div>
499
+ </div>
500
+ </div>
501
+ <div class="col-md-3">
502
+ <div class="metric-card">
503
+ <div class="metric-value">${summary.overall_success_rate}%</div>
504
+ <div class="metric-label">Taxa de Sucesso</div>
505
+ </div>
506
+ </div>
507
+ <div class="col-md-3">
508
+ <div class="metric-card">
509
+ <div class="metric-value">${summary.overall_validation_rate}%</div>
510
+ <div class="metric-label">Taxa de Validação</div>
511
+ </div>
512
+ </div>
513
+ <div class="col-md-3">
514
+ <div class="metric-card">
515
+ <div class="metric-value">${summary.avg_response_consistency}%</div>
516
+ <div class="metric-label">Consistência Média</div>
517
+ </div>
518
+ </div>
519
+ </div>
520
+
521
+ <div class="row mt-4">
522
+ <div class="col-md-6">
523
+ <h6>🏆 Melhor Grupo (Validação)</h6>
524
+ <div class="card">
525
+ <div class="card-body">
526
+ <strong>Grupo ${summary.best_performing_group.group_id}</strong><br>
527
+ <small>
528
+ ${summary.best_performing_group.group_config.sql_model_name}<br>
529
+ Processing: ${summary.best_performing_group.group_config.processing_enabled ? 'Ativo' : 'Inativo'}<br>
530
+ Taxa: ${summary.best_performing_group.validation_rate}%
531
+ </small>
532
+ </div>
533
+ </div>
534
+ </div>
535
+ <div class="col-md-6">
536
+ <h6>🎯 Grupo Mais Consistente</h6>
537
+ <div class="card">
538
+ <div class="card-body">
539
+ <strong>Grupo ${summary.most_consistent_group.group_id}</strong><br>
540
+ <small>
541
+ ${summary.most_consistent_group.group_config.sql_model_name}<br>
542
+ Processing: ${summary.most_consistent_group.group_config.processing_enabled ? 'Ativo' : 'Inativo'}<br>
543
+ Consistência: ${summary.most_consistent_group.response_consistency}%
544
+ </small>
545
+ </div>
546
+ </div>
547
+ </div>
548
+ </div>
549
+ `;
550
+ }
551
+
552
+ displayGroupResults(groupResults) {
553
+ const groupsContent = document.getElementById('groupsContent');
554
+
555
+ let html = '<div class="table-responsive"><table class="table table-striped"><thead><tr>';
556
+ html += '<th>Grupo</th><th>Modelo SQL</th><th>Processing</th><th>Testes</th>';
557
+ html += '<th>Sucesso</th><th>Validação</th><th>Consistência</th><th>Tempo Médio</th>';
558
+ html += '</tr></thead><tbody>';
559
+
560
+ groupResults.forEach(group => {
561
+ const config = group.group_config;
562
+ html += `
563
+ <tr>
564
+ <td><strong>${group.group_id}</strong></td>
565
+ <td>${config.sql_model_name}</td>
566
+ <td>${config.processing_enabled ? config.processing_model_name : 'Não'}</td>
567
+ <td>${group.total_tests}</td>
568
+ <td><span class="badge bg-${group.success_rate >= 80 ? 'success' : group.success_rate >= 60 ? 'warning' : 'danger'}">${group.success_rate}%</span></td>
569
+ <td><span class="badge bg-${group.validation_rate >= 80 ? 'success' : group.validation_rate >= 60 ? 'warning' : 'danger'}">${group.validation_rate}%</span></td>
570
+ <td>${group.response_consistency}%</td>
571
+ <td>${group.avg_execution_time}s</td>
572
+ </tr>
573
+ `;
574
+ });
575
+
576
+ html += '</tbody></table></div>';
577
+ groupsContent.innerHTML = html;
578
+ }
579
+
580
+ displayIndividualResults(individualResults) {
581
+ const individualContent = document.getElementById('individualContent');
582
+
583
+ let html = '<div class="table-responsive"><table class="table table-sm"><thead><tr>';
584
+ html += '<th>Grupo</th><th>Iter.</th><th>Modelo</th><th>Sucesso</th><th>Validação</th><th>Tempo</th><th>Ações</th>';
585
+ html += '</tr></thead><tbody>';
586
+
587
+ individualResults.slice(0, 100).forEach((result, index) => { // Limita a 100 para performance
588
+ const validation = result.validation || {};
589
+ html += `
590
+ <tr>
591
+ <td>${result.group_id}</td>
592
+ <td>${result.iteration}</td>
593
+ <td><small>${result.sql_model}</small></td>
594
+ <td><span class="badge bg-${result.success ? 'success' : 'danger'}">${result.success ? 'Sim' : 'Não'}</span></td>
595
+ <td><span class="badge bg-${validation.valid ? 'success' : 'danger'}">${validation.score || 0}</span></td>
596
+ <td>${result.execution_time}s</td>
597
+ <td><button class="btn btn-sm btn-outline-info" onclick="testManager.showResultDetails(${index})">Ver</button></td>
598
+ </tr>
599
+ `;
600
+ });
601
+
602
+ html += '</tbody></table></div>';
603
+
604
+ if (individualResults.length > 100) {
605
+ html += `<p class="text-muted mt-2">Mostrando primeiros 100 de ${individualResults.length} resultados. Baixe o CSV para ver todos.</p>`;
606
+ }
607
+
608
+ individualContent.innerHTML = html;
609
+
610
+ // Armazena resultados para detalhes
611
+ this.individualResults = individualResults;
612
+ }
613
+
614
+ showResultDetails(index) {
615
+ const result = this.individualResults[index];
616
+ const validation = result.validation || {};
617
+
618
+ const modal = `
619
+ <div class="modal fade" id="resultModal" tabindex="-1">
620
+ <div class="modal-dialog modal-lg">
621
+ <div class="modal-content">
622
+ <div class="modal-header">
623
+ <h5 class="modal-title">Detalhes do Teste - Grupo ${result.group_id}, Iteração ${result.iteration}</h5>
624
+ <button type="button" class="btn-close" data-bs-dismiss="modal"></button>
625
+ </div>
626
+ <div class="modal-body">
627
+ <h6>Configuração</h6>
628
+ <p><strong>Modelo SQL:</strong> ${result.sql_model}<br>
629
+ <strong>Processing Agent:</strong> ${result.processing_enabled ? result.processing_model : 'Desativado'}<br>
630
+ <strong>Tempo de Execução:</strong> ${result.execution_time}s</p>
631
+
632
+ <h6>Query SQL</h6>
633
+ <pre class="bg-light p-2"><code>${result.sql_query || 'N/A'}</code></pre>
634
+
635
+ <h6>Resposta</h6>
636
+ <div class="bg-light p-2" style="max-height: 200px; overflow-y: auto;">
637
+ ${result.response || 'N/A'}
638
+ </div>
639
+
640
+ <h6>Validação</h6>
641
+ <p><strong>Válida:</strong> ${validation.valid ? 'Sim' : 'Não'}<br>
642
+ <strong>Pontuação:</strong> ${validation.score || 0}<br>
643
+ <strong>Razão:</strong> ${validation.reason || 'N/A'}</p>
644
+
645
+ ${result.error ? `<h6>Erro</h6><div class="alert alert-danger">${result.error}</div>` : ''}
646
+ </div>
647
+ </div>
648
+ </div>
649
+ </div>
650
+ `;
651
+
652
+ // Remove modal anterior se existir
653
+ const existingModal = document.getElementById('resultModal');
654
+ if (existingModal) {
655
+ existingModal.remove();
656
+ }
657
+
658
+ // Adiciona novo modal
659
+ document.body.insertAdjacentHTML('beforeend', modal);
660
+
661
+ // Mostra modal
662
+ const modalElement = new bootstrap.Modal(document.getElementById('resultModal'));
663
+ modalElement.show();
664
+ }
665
+
666
+ async downloadCsv() {
667
+ try {
668
+ const response = await fetch('/api/download_csv');
669
+
670
+ if (response.ok) {
671
+ const blob = await response.blob();
672
+ const url = window.URL.createObjectURL(blob);
673
+ const a = document.createElement('a');
674
+ a.href = url;
675
+ a.download = `teste_agentgraph_${new Date().toISOString().slice(0, 19).replace(/:/g, '-')}.xlsx`;
676
+ document.body.appendChild(a);
677
+ a.click();
678
+ window.URL.revokeObjectURL(url);
679
+ document.body.removeChild(a);
680
+
681
+ this.showAlert('Relatório baixado com sucesso!', 'success');
682
+ } else {
683
+ this.showAlert('Erro ao baixar relatório.', 'danger');
684
+ }
685
+ } catch (error) {
686
+ this.showAlert('Erro ao baixar relatório: ' + error.message, 'danger');
687
+ }
688
+ }
689
+
690
+ updateStatus(status) {
691
+ const statusElement = document.getElementById('sessionStatus');
692
+ statusElement.className = `status-badge status-${status}`;
693
+
694
+ const statusText = {
695
+ 'idle': 'Aguardando',
696
+ 'running': 'Executando',
697
+ 'completed': 'Concluído',
698
+ 'error': 'Erro'
699
+ };
700
+
701
+ const statusIcon = {
702
+ 'idle': 'circle',
703
+ 'running': 'spinner fa-spin',
704
+ 'completed': 'check-circle',
705
+ 'error': 'exclamation-circle'
706
+ };
707
+
708
+ statusElement.innerHTML = `<i class="fas fa-${statusIcon[status]}"></i> ${statusText[status]}`;
709
+ }
710
+
711
+ showAlert(message, type) {
712
+ // Remove alertas anteriores
713
+ const existingAlerts = document.querySelectorAll('.alert-dismissible');
714
+ existingAlerts.forEach(alert => alert.remove());
715
+
716
+ const alert = document.createElement('div');
717
+ alert.className = `alert alert-${type} alert-dismissible fade show`;
718
+ alert.innerHTML = `
719
+ ${message}
720
+ <button type="button" class="btn-close" data-bs-dismiss="alert"></button>
721
+ `;
722
+
723
+ document.querySelector('.container-fluid').insertBefore(alert, document.querySelector('.row'));
724
+
725
+ // Remove automaticamente após 5 segundos
726
+ setTimeout(() => {
727
+ if (alert.parentNode) {
728
+ alert.remove();
729
+ }
730
+ }, 5000);
731
+ }
732
+ }
733
+
734
+ // Inicializa o sistema
735
+ const testManager = new TestManager();
736
+
737
+ // Funções globais para os botões HTML
738
+ function cancelCurrentTest() {
739
+ testManager.cancelCurrentTest();
740
+ }
741
+
742
+ function cancelAllTests() {
743
+ testManager.cancelAllTests();
744
+ }
745
+
746
+ function skipStuckTests() {
747
+ testManager.skipStuckTests();
748
+ }
testes/templates/index.html ADDED
@@ -0,0 +1,578 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="pt-BR">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>Sistema de Testes Massivos - AgentGraph</title>
7
+ <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css" rel="stylesheet">
8
+ <link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css" rel="stylesheet">
9
+ <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&display=swap" rel="stylesheet">
10
+ <style>
11
+ /* Design System - Cores Profissionais */
12
+ :root {
13
+ --primary-color: #2563eb;
14
+ --primary-dark: #1d4ed8;
15
+ --secondary-color: #64748b;
16
+ --success-color: #059669;
17
+ --warning-color: #d97706;
18
+ --danger-color: #dc2626;
19
+ --info-color: #0891b2;
20
+
21
+ --bg-primary: #f8fafc;
22
+ --bg-secondary: #f1f5f9;
23
+ --bg-dark: #0f172a;
24
+ --bg-card: #ffffff;
25
+
26
+ --text-primary: #1e293b;
27
+ --text-secondary: #64748b;
28
+ --text-muted: #94a3b8;
29
+
30
+ --border-color: #e2e8f0;
31
+ --border-radius: 12px;
32
+ --shadow-sm: 0 1px 2px 0 rgb(0 0 0 / 0.05);
33
+ --shadow-md: 0 4px 6px -1px rgb(0 0 0 / 0.1);
34
+ --shadow-lg: 0 10px 15px -3px rgb(0 0 0 / 0.1);
35
+ }
36
+
37
+ /* Layout Principal */
38
+ body {
39
+ font-family: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif;
40
+ background: var(--bg-primary);
41
+ color: var(--text-primary);
42
+ }
43
+
44
+ .container-fluid {
45
+ padding: 0;
46
+ }
47
+
48
+ /* Sidebar Profissional */
49
+ .sidebar {
50
+ background: linear-gradient(135deg, var(--bg-dark), #1e293b);
51
+ min-height: 100vh;
52
+ border-right: 1px solid var(--border-color);
53
+ box-shadow: var(--shadow-lg);
54
+ padding: 2rem 1.5rem;
55
+ }
56
+
57
+ .sidebar h4 {
58
+ color: white;
59
+ font-weight: 800;
60
+ margin-bottom: 2rem;
61
+ font-size: 1.5rem;
62
+ }
63
+
64
+ .sidebar h6 {
65
+ color: #cbd5e1;
66
+ font-weight: 600;
67
+ margin-bottom: 1rem;
68
+ margin-top: 2rem;
69
+ font-size: 0.875rem;
70
+ text-transform: uppercase;
71
+ letter-spacing: 0.05em;
72
+ }
73
+
74
+ .sidebar .form-control,
75
+ .sidebar .form-select {
76
+ background: rgba(255, 255, 255, 0.1);
77
+ border: 1px solid rgba(255, 255, 255, 0.2);
78
+ color: white;
79
+ border-radius: 8px;
80
+ }
81
+
82
+ .sidebar .form-control::placeholder {
83
+ color: rgba(255, 255, 255, 0.6);
84
+ }
85
+
86
+ .sidebar .form-control:focus,
87
+ .sidebar .form-select:focus {
88
+ background: rgba(255, 255, 255, 0.15);
89
+ border-color: var(--primary-color);
90
+ box-shadow: 0 0 0 3px rgb(37 99 235 / 0.2);
91
+ color: white;
92
+ }
93
+
94
+ /* Main Content */
95
+ .main-content {
96
+ background: var(--bg-primary);
97
+ min-height: 100vh;
98
+ padding: 2rem;
99
+ }
100
+
101
+ /* Header Dashboard */
102
+ .dashboard-header {
103
+ background: var(--bg-card);
104
+ border-radius: var(--border-radius);
105
+ padding: 2rem;
106
+ margin-bottom: 2rem;
107
+ border: 1px solid var(--border-color);
108
+ box-shadow: var(--shadow-sm);
109
+ }
110
+
111
+ .dashboard-header h2 {
112
+ color: var(--text-primary);
113
+ font-weight: 800;
114
+ margin: 0;
115
+ font-size: 2rem;
116
+ }
117
+
118
+ /* Cards e Componentes */
119
+ .metric-card {
120
+ background: linear-gradient(135deg, var(--primary-color), var(--info-color));
121
+ color: white;
122
+ border-radius: var(--border-radius);
123
+ padding: 2rem;
124
+ margin: 1rem 0;
125
+ box-shadow: var(--shadow-md);
126
+ transition: all 0.3s ease;
127
+ border: none;
128
+ }
129
+
130
+ .metric-card:hover {
131
+ transform: translateY(-2px);
132
+ box-shadow: var(--shadow-lg);
133
+ }
134
+
135
+ .metric-value {
136
+ font-size: 3rem;
137
+ font-weight: 800;
138
+ line-height: 1;
139
+ margin-bottom: 0.5rem;
140
+ }
141
+
142
+ .metric-label {
143
+ font-size: 0.875rem;
144
+ font-weight: 600;
145
+ opacity: 0.9;
146
+ text-transform: uppercase;
147
+ letter-spacing: 0.05em;
148
+ }
149
+
150
+ .test-group-card {
151
+ background: var(--bg-card);
152
+ border: 1px solid var(--border-color);
153
+ border-radius: var(--border-radius);
154
+ padding: 2rem;
155
+ margin: 1.5rem 0;
156
+ box-shadow: var(--shadow-sm);
157
+ transition: all 0.3s ease;
158
+ }
159
+
160
+ .test-group-card:hover {
161
+ box-shadow: var(--shadow-md);
162
+ border-color: var(--primary-color);
163
+ transform: translateY(-1px);
164
+ }
165
+
166
+ /* Status Badges Modernos */
167
+ .status-badge {
168
+ padding: 0.75rem 1.5rem;
169
+ border-radius: 50px;
170
+ font-weight: 700;
171
+ font-size: 0.875rem;
172
+ display: inline-flex;
173
+ align-items: center;
174
+ gap: 0.5rem;
175
+ box-shadow: var(--shadow-sm);
176
+ text-transform: uppercase;
177
+ letter-spacing: 0.05em;
178
+ }
179
+
180
+ .status-idle {
181
+ background: var(--secondary-color);
182
+ color: white;
183
+ }
184
+ .status-running {
185
+ background: var(--success-color);
186
+ color: white;
187
+ animation: pulse 2s infinite;
188
+ }
189
+ .status-completed {
190
+ background: var(--primary-color);
191
+ color: white;
192
+ }
193
+ .status-error {
194
+ background: var(--danger-color);
195
+ color: white;
196
+ }
197
+
198
+ @keyframes pulse {
199
+ 0%, 100% { opacity: 1; }
200
+ 50% { opacity: 0.8; }
201
+ }
202
+
203
+ /* Containers */
204
+ .progress-container {
205
+ display: none;
206
+ margin: 2rem 0;
207
+ background: var(--bg-card);
208
+ border-radius: var(--border-radius);
209
+ padding: 2rem;
210
+ border: 1px solid var(--border-color);
211
+ box-shadow: var(--shadow-md);
212
+ }
213
+
214
+ .results-container {
215
+ display: none;
216
+ margin: 2rem 0;
217
+ }
218
+
219
+ /* Botões Profissionais */
220
+ .btn {
221
+ border-radius: 10px;
222
+ font-weight: 600;
223
+ padding: 0.875rem 2rem;
224
+ border: none;
225
+ transition: all 0.3s ease;
226
+ box-shadow: var(--shadow-sm);
227
+ text-transform: uppercase;
228
+ letter-spacing: 0.05em;
229
+ font-size: 0.875rem;
230
+ }
231
+
232
+ .btn:hover {
233
+ transform: translateY(-2px);
234
+ box-shadow: var(--shadow-md);
235
+ }
236
+
237
+ .btn-primary {
238
+ background: linear-gradient(135deg, var(--primary-color), var(--primary-dark));
239
+ border: none;
240
+ }
241
+
242
+ .btn-success {
243
+ background: linear-gradient(135deg, var(--success-color), #047857);
244
+ }
245
+
246
+ .btn-warning {
247
+ background: linear-gradient(135deg, var(--warning-color), #b45309);
248
+ }
249
+
250
+ .btn-danger {
251
+ background: linear-gradient(135deg, var(--danger-color), #b91c1c);
252
+ }
253
+
254
+ /* Progress Bar Moderno */
255
+ .progress {
256
+ height: 16px;
257
+ border-radius: 8px;
258
+ background: var(--bg-secondary);
259
+ overflow: hidden;
260
+ box-shadow: inset 0 1px 2px rgba(0,0,0,0.1);
261
+ }
262
+
263
+ .progress-bar {
264
+ background: linear-gradient(90deg, var(--primary-color), var(--info-color));
265
+ border-radius: 8px;
266
+ transition: width 0.6s ease;
267
+ position: relative;
268
+ overflow: hidden;
269
+ }
270
+
271
+ .progress-bar::after {
272
+ content: '';
273
+ position: absolute;
274
+ top: 0;
275
+ left: 0;
276
+ right: 0;
277
+ bottom: 0;
278
+ background: linear-gradient(45deg, transparent 35%, rgba(255,255,255,0.2) 50%, transparent 65%);
279
+ animation: shimmer 2s infinite;
280
+ }
281
+
282
+ @keyframes shimmer {
283
+ 0% { transform: translateX(-100%); }
284
+ 100% { transform: translateX(100%); }
285
+ }
286
+
287
+ /* Alertas */
288
+ .alert {
289
+ border-radius: var(--border-radius);
290
+ border: none;
291
+ box-shadow: var(--shadow-sm);
292
+ padding: 1.5rem;
293
+ }
294
+
295
+ /* Formulários */
296
+ .form-control, .form-select {
297
+ border-radius: 10px;
298
+ border: 2px solid var(--border-color);
299
+ padding: 0.875rem 1rem;
300
+ transition: all 0.3s ease;
301
+ font-weight: 500;
302
+ }
303
+
304
+ .form-control:focus, .form-select:focus {
305
+ border-color: var(--primary-color);
306
+ box-shadow: 0 0 0 4px rgb(37 99 235 / 0.1);
307
+ transform: translateY(-1px);
308
+ }
309
+
310
+ /* Badges */
311
+ .badge {
312
+ font-weight: 700;
313
+ padding: 0.5rem 1rem;
314
+ border-radius: 8px;
315
+ font-size: 0.75rem;
316
+ text-transform: uppercase;
317
+ letter-spacing: 0.05em;
318
+ }
319
+
320
+ /* Ícones */
321
+ .fas, .far {
322
+ margin-right: 0.5rem;
323
+ }
324
+
325
+ /* Responsividade */
326
+ @media (max-width: 768px) {
327
+ .main-content {
328
+ padding: 1rem;
329
+ }
330
+
331
+ .metric-value {
332
+ font-size: 2rem;
333
+ }
334
+
335
+ .sidebar {
336
+ padding: 1rem;
337
+ }
338
+ }
339
+ </style>
340
+ </head>
341
+ <body>
342
+ <div class="container-fluid">
343
+ <div class="row">
344
+ <!-- Sidebar -->
345
+ <div class="col-md-3 sidebar">
346
+ <h4><i class="fas fa-flask"></i> Sistema de Testes</h4>
347
+
348
+ <!-- Configuração da Pergunta -->
349
+ <div class="mb-4">
350
+ <h6><i class="fas fa-question-circle"></i> Pergunta do Teste</h6>
351
+ <textarea id="testQuestion" class="form-control" rows="3"
352
+ placeholder="Digite a pergunta que será testada..."></textarea>
353
+ <button id="createSession" class="btn btn-primary btn-sm mt-2 w-100">
354
+ <i class="fas fa-plus"></i> Criar Sessão
355
+ </button>
356
+ </div>
357
+
358
+ <!-- Configuração de Grupo -->
359
+ <div id="groupConfig" style="display: none;">
360
+ <h6><i class="fas fa-cogs"></i> Configurar Grupo</h6>
361
+
362
+ <div class="mb-3">
363
+ <label class="form-label">Modelo SQL Agent</label>
364
+ <select id="sqlModel" class="form-select form-select-sm">
365
+ {% for name, value in available_models.items() %}
366
+ <option value="{{ value }}">{{ name }}</option>
367
+ {% endfor %}
368
+ </select>
369
+ </div>
370
+
371
+ <div class="mb-3">
372
+ <div class="form-check">
373
+ <input class="form-check-input" type="checkbox" id="enableProcessing">
374
+ <label class="form-check-label" for="enableProcessing">
375
+ Ativar Processing Agent
376
+ </label>
377
+ </div>
378
+ </div>
379
+
380
+ <div id="processingModelDiv" class="mb-3" style="display: none;">
381
+ <label class="form-label">Modelo Processing Agent</label>
382
+ <select id="processingModel" class="form-select form-select-sm">
383
+ {% for name, value in available_models.items() %}
384
+ <option value="{{ value }}">{{ name }}</option>
385
+ {% endfor %}
386
+ </select>
387
+ </div>
388
+
389
+ <div class="mb-3">
390
+ <label class="form-label">Número de Iterações</label>
391
+ <input type="number" id="iterations" class="form-control form-control-sm"
392
+ value="5" min="1" max="100">
393
+ </div>
394
+
395
+ <button id="addGroup" class="btn btn-success btn-sm w-100 mb-2">
396
+ <i class="fas fa-plus"></i> Adicionar Grupo
397
+ </button>
398
+ </div>
399
+
400
+ <!-- Validação -->
401
+ <div id="validationConfig" style="display: none;">
402
+ <hr>
403
+ <h6><i class="fas fa-check-circle"></i> Validação</h6>
404
+
405
+ <div class="mb-3">
406
+ <label class="form-label">Método</label>
407
+ <select id="validationMethod" class="form-select form-select-sm">
408
+ <option value="llm">LLM (Automático)</option>
409
+ <option value="keyword">Palavra-chave</option>
410
+ </select>
411
+ </div>
412
+
413
+ <div id="keywordDiv" class="mb-3" style="display: none;">
414
+ <label class="form-label">Conteúdo Esperado</label>
415
+ <input type="text" id="expectedContent" class="form-control form-control-sm"
416
+ placeholder="Texto que deve aparecer na resposta">
417
+ </div>
418
+
419
+ <button id="runTests" class="btn btn-warning btn-sm w-100">
420
+ <i class="fas fa-play"></i> Executar Testes
421
+ </button>
422
+ </div>
423
+ </div>
424
+
425
+ <!-- Main Content -->
426
+ <div class="col-md-9 main-content">
427
+ <div class="dashboard-header">
428
+ <div class="d-flex justify-content-between align-items-center">
429
+ <h2><i class="fas fa-chart-line"></i> Dashboard de Testes</h2>
430
+ <div id="sessionStatus" class="status-badge status-idle">
431
+ <i class="fas fa-circle"></i> Aguardando
432
+ </div>
433
+ </div>
434
+ </div>
435
+
436
+ <!-- Session Info -->
437
+ <div id="sessionInfo" class="alert alert-info" style="display: none;">
438
+ <h6><i class="fas fa-info-circle"></i> Informações da Sessão</h6>
439
+ <div id="sessionDetails"></div>
440
+ </div>
441
+
442
+ <!-- Test Groups -->
443
+ <div id="testGroups">
444
+ <h5><i class="fas fa-layer-group"></i> Grupos de Teste</h5>
445
+ <div id="groupsList">
446
+ <p class="text-muted">Nenhum grupo configurado ainda.</p>
447
+ </div>
448
+ </div>
449
+
450
+ <!-- Progress -->
451
+ <div id="progressContainer" class="progress-container">
452
+ <h5><i class="fas fa-tasks"></i> Progresso dos Testes</h5>
453
+ <div class="progress mb-3">
454
+ <div id="progressBar" class="progress-bar progress-bar-striped progress-bar-animated"
455
+ role="progressbar" style="width: 0%"></div>
456
+ </div>
457
+ <div id="progressDetails" class="row">
458
+ <div class="col-md-3">
459
+ <div class="metric-card">
460
+ <div class="metric-value" id="completedTests">0</div>
461
+ <div class="metric-label">Testes Concluídos</div>
462
+ </div>
463
+ </div>
464
+ <div class="col-md-3">
465
+ <div class="metric-card">
466
+ <div class="metric-value" id="totalTests">0</div>
467
+ <div class="metric-label">Total de Testes</div>
468
+ </div>
469
+ </div>
470
+ <div class="col-md-3">
471
+ <div class="metric-card">
472
+ <div class="metric-value" id="currentGroup">-</div>
473
+ <div class="metric-label">Grupo Atual</div>
474
+ </div>
475
+ </div>
476
+ <div class="col-md-3">
477
+ <div class="metric-card">
478
+ <div class="metric-value" id="estimatedTime">-</div>
479
+ <div class="metric-label">Tempo Restante</div>
480
+ </div>
481
+ </div>
482
+ </div>
483
+
484
+ <!-- Controles de Cancelamento -->
485
+ <div class="card mt-3">
486
+ <div class="card-header">
487
+ <h6><i class="fas fa-stop-circle"></i> Controles de Teste</h6>
488
+ </div>
489
+ <div class="card-body">
490
+ <div class="row">
491
+ <div class="col-md-6">
492
+ <div id="currentTestInfo" class="mb-3">
493
+ <strong>Teste Atual:</strong>
494
+ <div id="currentTestDetails" class="text-muted">Nenhum teste em execução</div>
495
+ </div>
496
+ <div id="runningTestsInfo" class="mb-3">
497
+ <strong>Testes em Execução:</strong>
498
+ <span id="runningTestsCount" class="badge bg-primary">0</span>
499
+ </div>
500
+ </div>
501
+ <div class="col-md-6">
502
+ <div class="d-grid gap-2">
503
+ <button id="cancelCurrentBtn" class="btn btn-warning btn-sm" onclick="cancelCurrentTest()">
504
+ <i class="fas fa-stop"></i> Cancelar Teste Atual
505
+ </button>
506
+ <button id="skipStuckBtn" class="btn btn-danger btn-sm" onclick="skipStuckTests()">
507
+ <i class="fas fa-forward"></i> Pular Testes Travados (>2min)
508
+ </button>
509
+ <button id="cancelAllBtn" class="btn btn-dark btn-sm" onclick="cancelAllTests()">
510
+ <i class="fas fa-ban"></i> Cancelar Todos os Testes
511
+ </button>
512
+ </div>
513
+ </div>
514
+ </div>
515
+
516
+ <!-- Lista de Testes em Execução -->
517
+ <div id="runningTestsList" class="mt-3" style="display: none;">
518
+ <h6>Testes em Execução:</h6>
519
+ <div id="runningTestsContainer" class="list-group">
520
+ <!-- Preenchido dinamicamente -->
521
+ </div>
522
+ </div>
523
+ </div>
524
+ </div>
525
+ </div>
526
+
527
+ <!-- Results -->
528
+ <div id="resultsContainer" class="results-container">
529
+ <div class="d-flex justify-content-between align-items-center mb-3">
530
+ <h5><i class="fas fa-chart-bar"></i> Resultados</h5>
531
+ <button id="downloadCsv" class="btn btn-outline-primary btn-sm">
532
+ <i class="fas fa-download"></i> Baixar CSV
533
+ </button>
534
+ </div>
535
+
536
+ <!-- Tabs -->
537
+ <ul class="nav nav-tabs" id="resultsTabs" role="tablist">
538
+ <li class="nav-item" role="presentation">
539
+ <button class="nav-link active" id="summary-tab" data-bs-toggle="tab"
540
+ data-bs-target="#summary" type="button" role="tab">
541
+ <i class="fas fa-chart-pie"></i> Resumo
542
+ </button>
543
+ </li>
544
+ <li class="nav-item" role="presentation">
545
+ <button class="nav-link" id="groups-tab" data-bs-toggle="tab"
546
+ data-bs-target="#groups" type="button" role="tab">
547
+ <i class="fas fa-layer-group"></i> Por Grupo
548
+ </button>
549
+ </li>
550
+ <li class="nav-item" role="presentation">
551
+ <button class="nav-link" id="individual-tab" data-bs-toggle="tab"
552
+ data-bs-target="#individual" type="button" role="tab">
553
+ <i class="fas fa-list"></i> Individual
554
+ </button>
555
+ </li>
556
+ </ul>
557
+
558
+ <div class="tab-content" id="resultsTabContent">
559
+ <div class="tab-pane fade show active" id="summary" role="tabpanel">
560
+ <div id="summaryContent" class="p-3"></div>
561
+ </div>
562
+ <div class="tab-pane fade" id="groups" role="tabpanel">
563
+ <div id="groupsContent" class="p-3"></div>
564
+ </div>
565
+ <div class="tab-pane fade" id="individual" role="tabpanel">
566
+ <div id="individualContent" class="p-3"></div>
567
+ </div>
568
+ </div>
569
+ </div>
570
+ </div>
571
+ </div>
572
+ </div>
573
+
574
+ <!-- Scripts -->
575
+ <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/js/bootstrap.bundle.min.js"></script>
576
+ <script src="{{ url_for('static', filename='js/app.js') }}"></script>
577
+ </body>
578
+ </html>
testes/test_runner.py ADDED
@@ -0,0 +1,673 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """
3
+ Sistema de execução massiva de testes com paralelismo
4
+ """
5
+ import asyncio
6
+ import logging
7
+ import time
8
+ import threading
9
+ from concurrent.futures import ThreadPoolExecutor, as_completed
10
+ from typing import Dict, List, Any, Optional
11
+ from datetime import datetime
12
+ import uuid
13
+ import sys
14
+ import os
15
+
16
+ # Adiciona path do projeto
17
+ sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
18
+
19
+ from graphs.main_graph import AgentGraphManager
20
+ from testes.test_validator import TestValidator
21
+ from utils.config import AVAILABLE_MODELS
22
+
23
+ class MassiveTestRunner:
24
+ """
25
+ Executor de testes massivos com paralelismo otimizado
26
+ """
27
+
28
+ def __init__(self, max_workers: int = 5):
29
+ """
30
+ Inicializa o test runner
31
+
32
+ Args:
33
+ max_workers: Número máximo de workers paralelos
34
+ """
35
+ self.max_workers = max_workers
36
+ logging.info(f"🔧 MassiveTestRunner inicializado com {max_workers} workers paralelos")
37
+ self.validator = TestValidator()
38
+ self.results = {
39
+ 'session_info': {},
40
+ 'group_results': [],
41
+ 'individual_results': [],
42
+ 'summary': {}
43
+ }
44
+ self.status = {
45
+ 'current_status': 'idle',
46
+ 'progress': 0,
47
+ 'current_group': None,
48
+ 'completed_tests': 0,
49
+ 'total_tests': 0,
50
+ 'start_time': None,
51
+ 'estimated_remaining': None,
52
+ 'errors': [],
53
+ 'current_test': None,
54
+ 'running_tests': {}, # {thread_id: {start_time, group_id, iteration, task, future}}
55
+ 'cancelled_tests': set(),
56
+ 'timeout_tests': set()
57
+ }
58
+ self._lock = threading.Lock()
59
+ self._cancel_event = threading.Event()
60
+ self._test_timeout = 360 # 1.5 minutos timeout por teste
61
+ self._active_futures = {} # {thread_id: future} para cancelamento real
62
+
63
+ async def run_test_session(self, session: Dict[str, Any], validation_method: str = 'llm', expected_content: str = None) -> Dict[str, Any]:
64
+ """
65
+ Executa sessão completa de testes
66
+
67
+ Args:
68
+ session: Dados da sessão de teste
69
+ validation_method: Método de validação ('llm' ou 'keyword')
70
+ expected_content: Conteúdo esperado (para validação keyword)
71
+
72
+ Returns:
73
+ Resultados completos dos testes
74
+ """
75
+ try:
76
+ print(f"\n🔥 MASSIVE TEST RUNNER INICIADO")
77
+ print(f"📋 Sessão: {session['id']}")
78
+ print(f"❓ Pergunta: {session['question']}")
79
+ print(f"👥 Grupos: {len(session['groups'])}")
80
+
81
+ total_tests = sum(group['iterations'] for group in session['groups'])
82
+ print(f"🔢 Total de testes: {total_tests}")
83
+ print(f"⚡ Workers paralelos: {self.max_workers}")
84
+ print("-" * 60)
85
+
86
+ logging.info(f"🚀 Iniciando sessão de testes: {session['id']}")
87
+
88
+ # Atualiza status
89
+ with self._lock:
90
+ self.status.update({
91
+ 'current_status': 'initializing',
92
+ 'start_time': time.time(),
93
+ 'total_tests': total_tests
94
+ })
95
+
96
+ # Armazena informações da sessão
97
+ self.results['session_info'] = {
98
+ 'id': session['id'],
99
+ 'question': session['question'],
100
+ 'validation_method': validation_method,
101
+ 'expected_content': expected_content,
102
+ 'total_groups': len(session['groups']),
103
+ 'total_tests': self.status['total_tests'],
104
+ 'started_at': datetime.now().isoformat()
105
+ }
106
+
107
+ # Executa grupos de teste
108
+ group_results = []
109
+
110
+ for group_idx, group in enumerate(session['groups']):
111
+ print(f"\n📊 EXECUTANDO GRUPO {group_idx + 1}/{len(session['groups'])}")
112
+ print(f"🤖 Modelo SQL: {group['sql_model_name']}")
113
+ print(f"🔄 Processing Agent: {'✅ ' + group['processing_model_name'] if group['processing_enabled'] else '❌ Desativado'}")
114
+ print(f"🔢 Iterações: {group['iterations']}")
115
+ print(f"⏰ {datetime.now().strftime('%H:%M:%S')}")
116
+
117
+ logging.info(f"📊 Executando grupo {group_idx + 1}/{len(session['groups'])}: {group['sql_model_name']}")
118
+
119
+ with self._lock:
120
+ self.status['current_group'] = group_idx + 1
121
+ self.status['current_status'] = 'running_group'
122
+
123
+ # Executa testes do grupo em paralelo
124
+ group_result = await self._run_group_tests(
125
+ session['question'],
126
+ group,
127
+ validation_method,
128
+ expected_content
129
+ )
130
+
131
+ group_results.append(group_result)
132
+ self.results['group_results'] = group_results
133
+
134
+ # Atualiza progresso
135
+ completed_so_far = sum(len(gr['individual_results']) for gr in group_results)
136
+ with self._lock:
137
+ self.status['completed_tests'] = completed_so_far
138
+ self.status['progress'] = (completed_so_far / self.status['total_tests']) * 100
139
+
140
+ # Estima tempo restante
141
+ if self.status['start_time']:
142
+ elapsed = time.time() - self.status['start_time']
143
+ if completed_so_far > 0:
144
+ avg_time_per_test = elapsed / completed_so_far
145
+ remaining_tests = self.status['total_tests'] - completed_so_far
146
+ self.status['estimated_remaining'] = avg_time_per_test * remaining_tests
147
+
148
+ # Gera resumo final
149
+ self._generate_summary()
150
+
151
+ with self._lock:
152
+ self.status['current_status'] = 'completed'
153
+ self.status['progress'] = 100
154
+ self.status['end_time'] = time.time()
155
+ total_time = self.status['end_time'] - self.status['start_time']
156
+ self.status['total_execution_time'] = total_time
157
+
158
+ logging.info(f"✅ Sessão de testes concluída: {session['id']}")
159
+ logging.info(f"📊 Resumo final: {self.status['total_tests']} testes em {total_time:.2f}s")
160
+ logging.info(f"🎯 Taxa geral de sucesso: {self.results['summary'].get('overall_success_rate', 0)}%")
161
+
162
+ return self.results
163
+
164
+ except Exception as e:
165
+ logging.error(f"❌ Erro na sessão de testes: {e}")
166
+ with self._lock:
167
+ self.status['current_status'] = 'error'
168
+ self.status['errors'].append(str(e))
169
+ raise
170
+
171
+ async def _run_group_tests(self, question: str, group: Dict[str, Any], validation_method: str, expected_content: str) -> Dict[str, Any]:
172
+ """
173
+ Executa testes de um grupo específico com paralelismo REAL
174
+
175
+ Args:
176
+ question: Pergunta do teste
177
+ group: Configuração do grupo
178
+ validation_method: Método de validação
179
+ expected_content: Conteúdo esperado
180
+
181
+ Returns:
182
+ Resultados do grupo
183
+ """
184
+ print(f"🔄 Executando {group['iterations']} testes em paralelo (máx {self.max_workers} simultâneos)")
185
+ logging.info(f"🔄 Executando {group['iterations']} testes para grupo {group['id']}")
186
+
187
+ # Cria semáforo para controle de concorrência
188
+ semaphore = asyncio.Semaphore(self.max_workers)
189
+
190
+ print(f"⚡ Iniciando {group['iterations']} testes com paralelismo REAL...")
191
+ start_time = time.time()
192
+
193
+ # VOLTA AO PARALELISMO ORIGINAL QUE FUNCIONAVA
194
+ print(f"🚀 Executando {group['iterations']} testes em paralelo (máx {self.max_workers} simultâneos)")
195
+
196
+ # Cria tasks para execução paralela (COMO ESTAVA ANTES)
197
+ semaphore = asyncio.Semaphore(self.max_workers)
198
+ tasks = []
199
+
200
+ print(f"⚡ Criando {group['iterations']} tasks paralelas...")
201
+ for iteration in range(group['iterations']):
202
+ task = self._run_single_test(
203
+ semaphore,
204
+ question,
205
+ group,
206
+ iteration + 1,
207
+ validation_method,
208
+ expected_content
209
+ )
210
+ tasks.append(task)
211
+
212
+ print(f"🚀 Executando {len(tasks)} testes em paralelo...")
213
+ start_time = time.time()
214
+
215
+ # Executa testes em paralelo (COMO ESTAVA ANTES)
216
+ individual_results = await asyncio.gather(*tasks, return_exceptions=True)
217
+
218
+ execution_time = time.time() - start_time
219
+ print(f"✅ Grupo {group['id']} concluído em {execution_time:.2f}s")
220
+
221
+ execution_time = time.time() - start_time
222
+ print(f"✅ Grupo {group['id']} concluído em {execution_time:.2f}s ({group['iterations']} testes)")
223
+
224
+ # Filtra resultados válidos
225
+ valid_results = []
226
+ errors = []
227
+
228
+ for result in individual_results:
229
+ if isinstance(result, Exception):
230
+ errors.append(str(result))
231
+ logging.error(f"Erro em teste individual: {result}")
232
+ else:
233
+ valid_results.append(result)
234
+ self.results['individual_results'].append(result)
235
+
236
+ # Calcula estatísticas do grupo
237
+ group_stats = self._calculate_group_stats(valid_results, group)
238
+ group_stats['errors'] = errors
239
+ group_stats['error_count'] = len(errors)
240
+
241
+ logging.info(f"✅ Grupo {group['id']} concluído: {len(valid_results)} sucessos, {len(errors)} erros")
242
+
243
+ return group_stats
244
+
245
+ async def _run_single_test(self, semaphore: asyncio.Semaphore, question: str, group: Dict[str, Any],
246
+ iteration: int, validation_method: str, expected_content: str) -> Dict[str, Any]:
247
+ """
248
+ Executa um teste individual com paralelismo real
249
+
250
+ Args:
251
+ semaphore: Semáforo para controle de concorrência
252
+ question: Pergunta do teste
253
+ group: Configuração do grupo
254
+ iteration: Número da iteração
255
+ validation_method: Método de validação
256
+ expected_content: Conteúdo esperado
257
+
258
+ Returns:
259
+ Resultado do teste individual
260
+ """
261
+ async with semaphore:
262
+ try:
263
+ start_time = time.time()
264
+
265
+ # Cria thread_id único para este teste
266
+ thread_id = f"test_{group['id']}_{iteration}_{uuid.uuid4().hex[:8]}"
267
+
268
+ # Registra teste como em execução
269
+ with self._lock:
270
+ self.status['running_tests'][thread_id] = {
271
+ 'start_time': start_time,
272
+ 'group_id': group['id'],
273
+ 'iteration': iteration,
274
+ 'question': question[:50] + '...' if len(question) > 50 else question
275
+ }
276
+ self.status['current_test'] = thread_id
277
+
278
+ print(f"🔄 [{datetime.now().strftime('%H:%M:%S')}] 🚀 INICIANDO {thread_id} (Worker {asyncio.current_task().get_name() if asyncio.current_task() else 'unknown'})")
279
+ logging.info(f"🔄 Iniciando teste {thread_id} - Grupo {group['id']}, Iteração {iteration}")
280
+
281
+ # Verifica se foi cancelado antes de começar
282
+ if thread_id in self.status['cancelled_tests']:
283
+ print(f"🚫 Teste {thread_id} cancelado antes de iniciar")
284
+ return self._create_cancelled_result(thread_id, group, iteration, start_time)
285
+
286
+ # Registra task para cancelamento (NOVO)
287
+ current_task = asyncio.current_task()
288
+ with self._lock:
289
+ self._active_futures[thread_id] = current_task
290
+
291
+ # Executa em thread separada para paralelismo real (COMO ESTAVA ANTES)
292
+ loop = asyncio.get_event_loop()
293
+
294
+ def run_sync_test():
295
+ """Executa teste de forma síncrona em thread separada"""
296
+ try:
297
+ # Verifica cancelamento antes de iniciar (NOVO)
298
+ if thread_id in self.status['cancelled_tests']:
299
+ return {'cancelled': True, 'reason': 'cancelled_before_start'}
300
+
301
+ # Cria novo loop para esta thread
302
+ new_loop = asyncio.new_event_loop()
303
+ asyncio.set_event_loop(new_loop)
304
+
305
+ # Inicializa AgentGraphManager para este teste
306
+ graph_manager = AgentGraphManager()
307
+
308
+ # Executa query
309
+ result = new_loop.run_until_complete(
310
+ graph_manager.process_query(
311
+ user_input=question,
312
+ selected_model=group['sql_model_name'],
313
+ processing_enabled=group['processing_enabled'],
314
+ processing_model=group['processing_model_name'] if group['processing_enabled'] else None,
315
+ thread_id=thread_id
316
+ )
317
+ )
318
+
319
+ new_loop.close()
320
+
321
+ # Verifica cancelamento após execução (NOVO)
322
+ if thread_id in self.status['cancelled_tests']:
323
+ return {'cancelled': True, 'reason': 'cancelled_after_execution'}
324
+
325
+ return result
326
+
327
+ except Exception as e:
328
+ logging.error(f"Erro em thread separada para {thread_id}: {e}")
329
+ return {'error': str(e)}
330
+
331
+ # Executa em ThreadPoolExecutor para paralelismo real (COMO ESTAVA ANTES)
332
+ with ThreadPoolExecutor(max_workers=1) as executor:
333
+ future = loop.run_in_executor(executor, run_sync_test)
334
+
335
+ # Aguarda com verificação de cancelamento (NOVO)
336
+ while not future.done():
337
+ await asyncio.sleep(0.1) # Verifica a cada 100ms
338
+ if thread_id in self.status['cancelled_tests']:
339
+ future.cancel()
340
+ print(f"🚫 Cancelando future do teste {thread_id}")
341
+ try:
342
+ await future
343
+ except:
344
+ pass
345
+ return self._create_cancelled_result(thread_id, group, iteration, start_time, 'user_cancelled')
346
+
347
+ result = await future
348
+
349
+ execution_time = time.time() - start_time
350
+
351
+ # Remove teste da lista de execução e limpa future
352
+ with self._lock:
353
+ if thread_id in self.status['running_tests']:
354
+ del self.status['running_tests'][thread_id]
355
+ if self.status['current_test'] == thread_id:
356
+ self.status['current_test'] = None
357
+ if thread_id in self._active_futures:
358
+ del self._active_futures[thread_id]
359
+
360
+ # Verifica tipo de resultado
361
+ if isinstance(result, dict):
362
+ if result.get('cancelled'):
363
+ print(f"🚫 [{datetime.now().strftime('%H:%M:%S')}] CANCELADO {thread_id} - {result.get('reason', 'unknown')}")
364
+ logging.info(f"🚫 Teste {thread_id} cancelado")
365
+ return self._create_cancelled_result(thread_id, group, iteration, start_time, result.get('reason'))
366
+ elif result.get('timeout'):
367
+ print(f"⏰ [{datetime.now().strftime('%H:%M:%S')}] TIMEOUT {thread_id} após {result.get('duration')}s")
368
+ logging.warning(f"⏰ Teste {thread_id} timeout")
369
+ return self._create_timeout_result(thread_id, group, iteration, start_time, result.get('duration'))
370
+ elif result.get('error'):
371
+ print(f"❌ [{datetime.now().strftime('%H:%M:%S')}] ERRO {thread_id}: {result['error']}")
372
+ logging.error(f"❌ Teste {thread_id} erro: {result['error']}")
373
+
374
+ print(f"✅ [{datetime.now().strftime('%H:%M:%S')}] 🎉 CONCLUÍDO {thread_id} em {execution_time:.2f}s")
375
+ logging.info(f"✅ Teste {thread_id} concluído em {execution_time:.2f}s")
376
+
377
+ # Valida resultado
378
+ validation_result = await self.validator.validate_result(
379
+ question=question,
380
+ sql_query=result.get('sql_query_extracted', ''),
381
+ response=result.get('response', ''),
382
+ method=validation_method,
383
+ expected_content=expected_content
384
+ )
385
+
386
+ # Monta resultado individual
387
+ individual_result = {
388
+ 'group_id': group['id'],
389
+ 'iteration': iteration,
390
+ 'thread_id': thread_id,
391
+ 'timestamp': datetime.now().isoformat(),
392
+ 'execution_time': round(execution_time, 2),
393
+ 'question': question,
394
+ 'sql_model': group['sql_model_name'],
395
+ 'processing_enabled': group['processing_enabled'],
396
+ 'processing_model': group['processing_model_name'],
397
+ 'sql_query': result.get('sql_query_extracted', ''),
398
+ 'response': result.get('response', ''),
399
+ 'error': result.get('error'),
400
+ 'success': not bool(result.get('error')),
401
+ 'validation': validation_result
402
+ }
403
+
404
+ # Atualiza progresso
405
+ with self._lock:
406
+ self.status['completed_tests'] += 1
407
+ progress = (self.status['completed_tests'] / self.status['total_tests']) * 100
408
+ self.status['progress'] = progress
409
+
410
+ # Estima tempo restante
411
+ if self.status['start_time']:
412
+ elapsed = time.time() - self.status['start_time']
413
+ if self.status['completed_tests'] > 0:
414
+ avg_time_per_test = elapsed / self.status['completed_tests']
415
+ remaining_tests = self.status['total_tests'] - self.status['completed_tests']
416
+ self.status['estimated_remaining'] = avg_time_per_test * remaining_tests
417
+
418
+ # Print visual do progresso
419
+ remaining_min = int(self.status['estimated_remaining'] // 60)
420
+ remaining_sec = int(self.status['estimated_remaining'] % 60)
421
+
422
+ print(f"📊 [{datetime.now().strftime('%H:%M:%S')}] Progresso: {self.status['completed_tests']}/{self.status['total_tests']} ({progress:.1f}%) - Restam ~{remaining_min}m{remaining_sec}s")
423
+
424
+ logging.info(f"📊 Progresso: {self.status['completed_tests']}/{self.status['total_tests']} ({progress:.1f}%)")
425
+
426
+ return individual_result
427
+
428
+ except Exception as e:
429
+ logging.error(f"❌ Erro em teste individual (grupo {group['id']}, iteração {iteration}): {e}")
430
+
431
+ # Atualiza progresso mesmo com erro
432
+ with self._lock:
433
+ self.status['completed_tests'] += 1
434
+ self.status['errors'].append(f"Grupo {group['id']}, Iteração {iteration}: {e}")
435
+
436
+ return {
437
+ 'group_id': group['id'],
438
+ 'iteration': iteration,
439
+ 'thread_id': f"error_{group['id']}_{iteration}",
440
+ 'timestamp': datetime.now().isoformat(),
441
+ 'execution_time': time.time() - start_time,
442
+ 'question': question,
443
+ 'sql_model': group['sql_model_name'],
444
+ 'processing_enabled': group['processing_enabled'],
445
+ 'processing_model': group['processing_model_name'],
446
+ 'sql_query': '',
447
+ 'response': '',
448
+ 'error': str(e),
449
+ 'success': False,
450
+ 'validation': {'valid': False, 'score': 0, 'reason': f'Erro de execução: {e}'}
451
+ }
452
+
453
+ def _calculate_group_stats(self, results: List[Dict[str, Any]], group: Dict[str, Any]) -> Dict[str, Any]:
454
+ """
455
+ Calcula estatísticas de um grupo
456
+
457
+ Args:
458
+ results: Resultados individuais do grupo
459
+ group: Configuração do grupo
460
+
461
+ Returns:
462
+ Estatísticas do grupo
463
+ """
464
+ if not results:
465
+ return {
466
+ 'group_id': group['id'],
467
+ 'group_config': group,
468
+ 'total_tests': 0,
469
+ 'success_rate': 0,
470
+ 'validation_rate': 0,
471
+ 'consistency_rate': 0,
472
+ 'avg_execution_time': 0,
473
+ 'individual_results': []
474
+ }
475
+
476
+ total_tests = len(results)
477
+ successful_tests = sum(1 for r in results if r.get('success', False))
478
+ valid_responses = sum(1 for r in results if r.get('validation', {}).get('valid', False))
479
+
480
+ # Calcula consistência (respostas similares)
481
+ responses = [r.get('response', '') for r in results if r.get('success', False)]
482
+ sql_queries = [r.get('sql_query', '') for r in results if r.get('success', False)]
483
+
484
+ response_consistency = self._calculate_consistency(responses)
485
+ sql_consistency = self._calculate_consistency(sql_queries)
486
+
487
+ avg_execution_time = sum(r.get('execution_time', 0) for r in results) / total_tests
488
+
489
+ return {
490
+ 'group_id': group['id'],
491
+ 'group_config': group,
492
+ 'total_tests': total_tests,
493
+ 'successful_tests': successful_tests,
494
+ 'valid_responses': valid_responses,
495
+ 'success_rate': round((successful_tests / total_tests) * 100, 2),
496
+ 'validation_rate': round((valid_responses / total_tests) * 100, 2),
497
+ 'response_consistency': round(response_consistency * 100, 2),
498
+ 'sql_consistency': round(sql_consistency * 100, 2),
499
+ 'avg_execution_time': round(avg_execution_time, 2),
500
+ 'individual_results': results
501
+ }
502
+
503
+ def _calculate_consistency(self, items: List[str]) -> float:
504
+ """
505
+ Calcula taxa de consistência entre itens
506
+
507
+ Args:
508
+ items: Lista de strings para comparar
509
+
510
+ Returns:
511
+ Taxa de consistência (0-1)
512
+ """
513
+ if len(items) <= 1:
514
+ return 1.0
515
+
516
+ # Conta ocorrências únicas
517
+ unique_items = set(items)
518
+ most_common_count = max(items.count(item) for item in unique_items)
519
+
520
+ return most_common_count / len(items)
521
+
522
+ def _generate_summary(self):
523
+ """Gera resumo geral dos testes"""
524
+ group_results = self.results.get('group_results', [])
525
+
526
+ if not group_results:
527
+ self.results['summary'] = {}
528
+ return
529
+
530
+ total_tests = sum(gr['total_tests'] for gr in group_results)
531
+ total_successful = sum(gr['successful_tests'] for gr in group_results)
532
+ total_valid = sum(gr['valid_responses'] for gr in group_results)
533
+
534
+ avg_success_rate = sum(gr['success_rate'] for gr in group_results) / len(group_results)
535
+ avg_validation_rate = sum(gr['validation_rate'] for gr in group_results) / len(group_results)
536
+ avg_response_consistency = sum(gr['response_consistency'] for gr in group_results) / len(group_results)
537
+ avg_sql_consistency = sum(gr['sql_consistency'] for gr in group_results) / len(group_results)
538
+
539
+ self.results['summary'] = {
540
+ 'total_groups': len(group_results),
541
+ 'total_tests': total_tests,
542
+ 'total_successful': total_successful,
543
+ 'total_valid': total_valid,
544
+ 'overall_success_rate': round((total_successful / total_tests) * 100, 2),
545
+ 'overall_validation_rate': round((total_valid / total_tests) * 100, 2),
546
+ 'avg_response_consistency': round(avg_response_consistency, 2),
547
+ 'avg_sql_consistency': round(avg_sql_consistency, 2),
548
+ 'best_performing_group': max(group_results, key=lambda x: x['validation_rate']),
549
+ 'most_consistent_group': max(group_results, key=lambda x: x['response_consistency'])
550
+ }
551
+
552
+ def get_status(self) -> Dict[str, Any]:
553
+ """Retorna status atual dos testes"""
554
+ with self._lock:
555
+ status = self.status.copy()
556
+ # Adiciona informações dos testes em execução
557
+ status['running_tests_count'] = len(self.status['running_tests'])
558
+ status['running_tests_details'] = list(self.status['running_tests'].values())
559
+ return status
560
+
561
+ def cancel_current_test(self, thread_id: str = None) -> bool:
562
+ """
563
+ Cancela teste específico ou o mais antigo em execução
564
+
565
+ Args:
566
+ thread_id: ID do teste específico para cancelar (opcional)
567
+
568
+ Returns:
569
+ True se cancelou algum teste
570
+ """
571
+ with self._lock:
572
+ if thread_id:
573
+ if thread_id in self.status['running_tests']:
574
+ self.status['cancelled_tests'].add(thread_id)
575
+ print(f"🚫 Teste {thread_id} marcado para cancelamento")
576
+ logging.info(f"Teste {thread_id} cancelado pelo usuário")
577
+ return True
578
+ else:
579
+ # Cancela o teste mais antigo
580
+ if self.status['running_tests']:
581
+ oldest_test = min(
582
+ self.status['running_tests'].items(),
583
+ key=lambda x: x[1]['start_time']
584
+ )
585
+ thread_id = oldest_test[0]
586
+ self.status['cancelled_tests'].add(thread_id)
587
+ print(f"🚫 Teste mais antigo {thread_id} marcado para cancelamento")
588
+ logging.info(f"Teste mais antigo {thread_id} cancelado pelo usuário")
589
+ return True
590
+ return False
591
+
592
+ def cancel_all_tests(self) -> int:
593
+ """
594
+ Cancela todos os testes em execução
595
+
596
+ Returns:
597
+ Número de testes cancelados
598
+ """
599
+ with self._lock:
600
+ running_count = len(self.status['running_tests'])
601
+ for thread_id in self.status['running_tests'].keys():
602
+ self.status['cancelled_tests'].add(thread_id)
603
+
604
+ print(f"🚫 {running_count} testes marcados para cancelamento")
605
+ logging.info(f"{running_count} testes cancelados pelo usuário")
606
+ return running_count
607
+
608
+ def skip_stuck_tests(self, max_duration: int = 120) -> int:
609
+ """
610
+ Marca testes travados (que excedem tempo limite) para cancelamento
611
+
612
+ Args:
613
+ max_duration: Tempo máximo em segundos
614
+
615
+ Returns:
616
+ Número de testes marcados como travados
617
+ """
618
+ current_time = time.time()
619
+ stuck_count = 0
620
+
621
+ with self._lock:
622
+ for thread_id, test_info in self.status['running_tests'].items():
623
+ if current_time - test_info['start_time'] > max_duration:
624
+ if thread_id not in self.status['cancelled_tests']:
625
+ self.status['timeout_tests'].add(thread_id)
626
+ self.status['cancelled_tests'].add(thread_id)
627
+ stuck_count += 1
628
+ print(f"⏰ Teste {thread_id} marcado como travado (>{max_duration}s)")
629
+ logging.warning(f"Teste {thread_id} travado - timeout após {max_duration}s")
630
+
631
+ return stuck_count
632
+
633
+ def _create_cancelled_result(self, thread_id: str, group: Dict[str, Any], iteration: int, start_time: float, reason: str = 'user_cancelled') -> Dict[str, Any]:
634
+ """Cria resultado para teste cancelado"""
635
+ execution_time = time.time() - start_time
636
+ return {
637
+ 'thread_id': thread_id,
638
+ 'group_id': group['id'],
639
+ 'iteration': iteration,
640
+ 'success': False,
641
+ 'cancelled': True,
642
+ 'cancel_reason': reason,
643
+ 'execution_time': execution_time,
644
+ 'sql_query': None,
645
+ 'final_response': f"Teste cancelado: {reason}",
646
+ 'validation_valid': False,
647
+ 'validation_score': 0,
648
+ 'error': None,
649
+ 'timestamp': datetime.now().isoformat()
650
+ }
651
+
652
+ def _create_timeout_result(self, thread_id: str, group: Dict[str, Any], iteration: int, start_time: float, duration: int) -> Dict[str, Any]:
653
+ """Cria resultado para teste com timeout"""
654
+ execution_time = time.time() - start_time
655
+ return {
656
+ 'thread_id': thread_id,
657
+ 'group_id': group['id'],
658
+ 'iteration': iteration,
659
+ 'success': False,
660
+ 'timeout': True,
661
+ 'timeout_duration': duration,
662
+ 'execution_time': execution_time,
663
+ 'sql_query': None,
664
+ 'final_response': f"Teste travado - timeout após {duration}s",
665
+ 'validation_valid': False,
666
+ 'validation_score': 0,
667
+ 'error': f"Timeout após {duration}s",
668
+ 'timestamp': datetime.now().isoformat()
669
+ }
670
+
671
+ def get_results(self) -> Dict[str, Any]:
672
+ """Retorna resultados dos testes"""
673
+ return self.results
testes/test_system.py ADDED
@@ -0,0 +1,287 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """
3
+ Script de teste do sistema de testes massivos
4
+ """
5
+ import sys
6
+ import os
7
+ import asyncio
8
+ import logging
9
+
10
+ # Adiciona path do projeto
11
+ sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
12
+
13
+ def test_imports():
14
+ """Testa se todos os imports funcionam"""
15
+ print("🔍 Testando imports...")
16
+
17
+ try:
18
+ from testes.test_runner import MassiveTestRunner
19
+ print(" ✅ MassiveTestRunner")
20
+
21
+ from testes.test_validator import TestValidator
22
+ print(" ✅ TestValidator")
23
+
24
+ from testes.report_generator import ReportGenerator
25
+ print(" ✅ ReportGenerator")
26
+
27
+ from utils.config import AVAILABLE_MODELS
28
+ print(" ✅ AVAILABLE_MODELS")
29
+
30
+ return True
31
+ except Exception as e:
32
+ print(f" ❌ Erro no import: {e}")
33
+ return False
34
+
35
+ def test_validator():
36
+ """Testa o sistema de validação"""
37
+ print("\n🔍 Testando validador...")
38
+
39
+ try:
40
+ from testes.test_validator import TestValidator
41
+
42
+ validator = TestValidator()
43
+ print(" ✅ Validator inicializado")
44
+
45
+ # Teste de validação por keyword
46
+ result = validator._validate_with_keyword(
47
+ "A resposta contém 150 usuários no total",
48
+ "150 usuários"
49
+ )
50
+
51
+ if result['valid'] and result['score'] == 100:
52
+ print(" ✅ Validação por keyword funcionando")
53
+ else:
54
+ print(f" ❌ Validação por keyword falhou: {result}")
55
+ return False
56
+
57
+ # Teste de sintaxe SQL
58
+ sql_result = validator.validate_sql_syntax("SELECT * FROM usuarios WHERE idade > 18")
59
+
60
+ if sql_result['valid']:
61
+ print(" ✅ Validação de sintaxe SQL funcionando")
62
+ else:
63
+ print(f" ❌ Validação SQL falhou: {sql_result}")
64
+ return False
65
+
66
+ return True
67
+ except Exception as e:
68
+ print(f" ❌ Erro no validator: {e}")
69
+ return False
70
+
71
+ def test_report_generator():
72
+ """Testa o gerador de relatórios"""
73
+ print("\n🔍 Testando gerador de relatórios...")
74
+
75
+ try:
76
+ from testes.report_generator import ReportGenerator
77
+
78
+ generator = ReportGenerator()
79
+ print(" ✅ ReportGenerator inicializado")
80
+
81
+ # Dados de teste
82
+ test_results = {
83
+ 'session_info': {
84
+ 'id': 'test_session',
85
+ 'question': 'Teste de pergunta',
86
+ 'validation_method': 'keyword'
87
+ },
88
+ 'group_results': [
89
+ {
90
+ 'group_id': 1,
91
+ 'group_config': {
92
+ 'sql_model_name': 'GPT-4o-mini',
93
+ 'processing_enabled': False,
94
+ 'processing_model_name': None
95
+ },
96
+ 'total_tests': 5,
97
+ 'successful_tests': 4,
98
+ 'valid_responses': 3,
99
+ 'success_rate': 80.0,
100
+ 'validation_rate': 60.0,
101
+ 'response_consistency': 75.0,
102
+ 'sql_consistency': 80.0,
103
+ 'avg_execution_time': 5.2
104
+ }
105
+ ],
106
+ 'individual_results': [
107
+ {
108
+ 'group_id': 1,
109
+ 'iteration': 1,
110
+ 'sql_model': 'GPT-4o-mini',
111
+ 'processing_enabled': False,
112
+ 'success': True,
113
+ 'validation': {'valid': True, 'score': 85}
114
+ }
115
+ ],
116
+ 'summary': {
117
+ 'total_groups': 1,
118
+ 'total_tests': 5,
119
+ 'overall_success_rate': 80.0,
120
+ 'overall_validation_rate': 60.0,
121
+ 'best_performing_group': {
122
+ 'group_id': 1,
123
+ 'group_config': {'sql_model_name': 'GPT-4o-mini'},
124
+ 'validation_rate': 60.0
125
+ },
126
+ 'most_consistent_group': {
127
+ 'group_id': 1,
128
+ 'group_config': {'sql_model_name': 'GPT-4o-mini'},
129
+ 'response_consistency': 75.0
130
+ }
131
+ }
132
+ }
133
+
134
+ # Testa criação de DataFrames
135
+ group_df = generator._create_group_summary_dataframe(test_results)
136
+ individual_df = generator._create_individual_results_dataframe(test_results)
137
+ general_df = generator._create_general_summary_dataframe(test_results)
138
+
139
+ if len(group_df) > 0 and len(individual_df) > 0 and len(general_df) > 0:
140
+ print(" ✅ DataFrames criados com sucesso")
141
+ else:
142
+ print(" ❌ Erro na criação de DataFrames")
143
+ return False
144
+
145
+ return True
146
+ except Exception as e:
147
+ print(f" ❌ Erro no report generator: {e}")
148
+ return False
149
+
150
+ async def test_runner_basic():
151
+ """Testa funcionalidades básicas do runner"""
152
+ print("\n🔍 Testando runner básico...")
153
+
154
+ try:
155
+ from testes.test_runner import MassiveTestRunner
156
+
157
+ runner = MassiveTestRunner(max_workers=2)
158
+ print(" ✅ MassiveTestRunner inicializado")
159
+
160
+ # Testa cálculo de consistência
161
+ items = ["resposta A", "resposta A", "resposta B", "resposta A"]
162
+ consistency = runner._calculate_consistency(items)
163
+
164
+ expected = 3/4 # 3 "resposta A" de 4 total
165
+ if abs(consistency - expected) < 0.01:
166
+ print(" ✅ Cálculo de consistência funcionando")
167
+ else:
168
+ print(f" ❌ Consistência incorreta: esperado {expected}, obtido {consistency}")
169
+ return False
170
+
171
+ # Testa status
172
+ status = runner.get_status()
173
+ if 'current_status' in status and status['current_status'] == 'idle':
174
+ print(" ✅ Status funcionando")
175
+ else:
176
+ print(f" ❌ Status incorreto: {status}")
177
+ return False
178
+
179
+ return True
180
+ except Exception as e:
181
+ print(f" ❌ Erro no runner: {e}")
182
+ return False
183
+
184
+ def test_flask_app():
185
+ """Testa se o app Flask pode ser importado"""
186
+ print("\n🔍 Testando Flask app...")
187
+
188
+ try:
189
+ from testes.app_teste import app
190
+ print(" ✅ Flask app importado")
191
+
192
+ # Testa se as rotas estão definidas
193
+ routes = [rule.rule for rule in app.url_map.iter_rules()]
194
+ expected_routes = ['/', '/api/models', '/api/create_test_session']
195
+
196
+ for route in expected_routes:
197
+ if route in routes:
198
+ print(f" ✅ Rota {route} definida")
199
+ else:
200
+ print(f" ❌ Rota {route} não encontrada")
201
+ return False
202
+
203
+ return True
204
+ except Exception as e:
205
+ print(f" ❌ Erro no Flask app: {e}")
206
+ return False
207
+
208
+ def test_agentgraph_integration():
209
+ """Testa integração com AgentGraph"""
210
+ print("\n🔍 Testando integração com AgentGraph...")
211
+
212
+ try:
213
+ from utils.config import AVAILABLE_MODELS, validate_config
214
+
215
+ # Testa se modelos estão disponíveis
216
+ if len(AVAILABLE_MODELS) > 0:
217
+ print(f" ✅ {len(AVAILABLE_MODELS)} modelos disponíveis")
218
+ else:
219
+ print(" ❌ Nenhum modelo disponível")
220
+ return False
221
+
222
+ # Testa validação de config (pode falhar se APIs não configuradas)
223
+ try:
224
+ validate_config()
225
+ print(" ✅ Configuração válida")
226
+ except Exception as e:
227
+ print(f" ⚠️ Configuração incompleta: {e}")
228
+ print(" 💡 Configure as APIs no .env para funcionalidade completa")
229
+
230
+ return True
231
+ except Exception as e:
232
+ print(f" ❌ Erro na integração: {e}")
233
+ return False
234
+
235
+ async def main():
236
+ """Função principal de teste"""
237
+ print("🧪 TESTE DO SISTEMA DE TESTES MASSIVOS")
238
+ print("=" * 50)
239
+
240
+ tests = [
241
+ ("Imports", test_imports),
242
+ ("Validator", test_validator),
243
+ ("Report Generator", test_report_generator),
244
+ ("Runner Básico", test_runner_basic),
245
+ ("Flask App", test_flask_app),
246
+ ("Integração AgentGraph", test_agentgraph_integration)
247
+ ]
248
+
249
+ passed = 0
250
+ total = len(tests)
251
+
252
+ for test_name, test_func in tests:
253
+ print(f"\n📋 {test_name}")
254
+ print("-" * 30)
255
+
256
+ try:
257
+ if asyncio.iscoroutinefunction(test_func):
258
+ result = await test_func()
259
+ else:
260
+ result = test_func()
261
+
262
+ if result:
263
+ passed += 1
264
+ print(f"✅ {test_name} PASSOU")
265
+ else:
266
+ print(f"❌ {test_name} FALHOU")
267
+ except Exception as e:
268
+ print(f"❌ {test_name} ERRO: {e}")
269
+
270
+ print("\n" + "=" * 50)
271
+ print(f"📊 RESULTADO FINAL: {passed}/{total} testes passaram")
272
+
273
+ if passed == total:
274
+ print("🎉 TODOS OS TESTES PASSARAM!")
275
+ print("🚀 Sistema pronto para uso!")
276
+ print("💡 Execute: python testes/run_tests.py")
277
+ else:
278
+ print("⚠️ Alguns testes falharam")
279
+ print("🔧 Verifique os erros acima")
280
+
281
+ print("=" * 50)
282
+
283
+ return passed == total
284
+
285
+ if __name__ == '__main__':
286
+ success = asyncio.run(main())
287
+ sys.exit(0 if success else 1)
testes/test_validator.py ADDED
@@ -0,0 +1,334 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """
3
+ Sistema de validação de resultados de testes
4
+ """
5
+ import logging
6
+ import re
7
+ import asyncio
8
+ from typing import Dict, Any, Optional
9
+ import sys
10
+ import os
11
+
12
+ # Adiciona path do projeto
13
+ sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
14
+
15
+ from langchain_openai import ChatOpenAI
16
+ from langchain_anthropic import ChatAnthropic
17
+ from langchain.schema import HumanMessage
18
+ from utils.config import OPENAI_MODELS, ANTHROPIC_MODELS
19
+
20
+ class TestValidator:
21
+ """
22
+ Validador de resultados de testes usando LLM ou keywords
23
+ """
24
+
25
+ def __init__(self, validator_model: str = "gpt-4o-mini"):
26
+ """
27
+ Inicializa o validador
28
+
29
+ Args:
30
+ validator_model: Modelo LLM para validação
31
+ """
32
+ self.validator_model = validator_model
33
+ self.llm = self._initialize_validator_llm()
34
+
35
+ def _initialize_validator_llm(self):
36
+ """Inicializa LLM para validação"""
37
+ try:
38
+ if self.validator_model in OPENAI_MODELS:
39
+ return ChatOpenAI(
40
+ model=self.validator_model,
41
+ temperature=0.1, # Baixa temperatura para consistência
42
+ max_tokens=1000
43
+ )
44
+ elif self.validator_model in ANTHROPIC_MODELS:
45
+ return ChatAnthropic(
46
+ model=self.validator_model,
47
+ temperature=0.1,
48
+ max_tokens=1000
49
+ )
50
+ else:
51
+ # Fallback para GPT-4o-mini
52
+ logging.warning(f"Modelo {self.validator_model} não suportado, usando gpt-4o-mini")
53
+ return ChatOpenAI(
54
+ model="gpt-4o-mini",
55
+ temperature=0.1,
56
+ max_tokens=1000
57
+ )
58
+ except Exception as e:
59
+ logging.error(f"Erro ao inicializar LLM validador: {e}")
60
+ return None
61
+
62
+ async def validate_result(self, question: str, sql_query: str, response: str,
63
+ method: str = 'llm', expected_content: str = None) -> Dict[str, Any]:
64
+ """
65
+ Valida resultado de um teste
66
+
67
+ Args:
68
+ question: Pergunta original
69
+ sql_query: Query SQL gerada
70
+ response: Resposta final do agente
71
+ method: Método de validação ('llm' ou 'keyword')
72
+ expected_content: Conteúdo esperado (para método keyword)
73
+
74
+ Returns:
75
+ Resultado da validação
76
+ """
77
+ try:
78
+ if method == 'llm':
79
+ return await self._validate_with_llm(question, sql_query, response)
80
+ elif method == 'keyword':
81
+ return self._validate_with_keyword(response, expected_content)
82
+ else:
83
+ return {
84
+ 'valid': False,
85
+ 'score': 0,
86
+ 'reason': f'Método de validação inválido: {method}',
87
+ 'method': method
88
+ }
89
+ except Exception as e:
90
+ logging.error(f"Erro na validação: {e}")
91
+ return {
92
+ 'valid': False,
93
+ 'score': 0,
94
+ 'reason': f'Erro na validação: {e}',
95
+ 'method': method
96
+ }
97
+
98
+ async def _validate_with_llm(self, question: str, sql_query: str, response: str) -> Dict[str, Any]:
99
+ """
100
+ Valida usando LLM
101
+
102
+ Args:
103
+ question: Pergunta original
104
+ sql_query: Query SQL gerada
105
+ response: Resposta final
106
+
107
+ Returns:
108
+ Resultado da validação
109
+ """
110
+ if not self.llm:
111
+ return {
112
+ 'valid': False,
113
+ 'score': 0,
114
+ 'reason': 'LLM validador não disponível',
115
+ 'method': 'llm'
116
+ }
117
+
118
+ try:
119
+ # Prompt para validação
120
+ validation_prompt = f"""
121
+ Você é um especialista em SQL e análise de dados. Sua tarefa é avaliar se uma resposta gerada por um agente SQL está correta e adequada.
122
+
123
+ PERGUNTA ORIGINAL:
124
+ {question}
125
+
126
+ QUERY SQL GERADA:
127
+ {sql_query}
128
+
129
+ RESPOSTA FINAL:
130
+ {response}
131
+
132
+ CRITÉRIOS DE AVALIAÇÃO:
133
+ 1. A query SQL está sintaticamente correta?
134
+ 2. A query SQL responde adequadamente à pergunta?
135
+ 3. A resposta final é coerente com a query e a pergunta?
136
+ 4. A resposta contém informações relevantes e úteis?
137
+ 5. Há erros evidentes na lógica ou execução?
138
+
139
+ INSTRUÇÕES:
140
+ - Analise cuidadosamente cada critério
141
+ - Dê uma pontuação de 0 a 100
142
+ - Considere válida (True) se pontuação >= 70
143
+ - Seja rigoroso mas justo na avaliação
144
+
145
+ RESPONDA EXATAMENTE NESTE FORMATO:
146
+ PONTUAÇÃO: [número de 0 a 100]
147
+ VÁLIDA: [True ou False]
148
+ RAZÃO: [explicação breve da avaliação]
149
+ """
150
+
151
+ # Executa validação
152
+ message = HumanMessage(content=validation_prompt)
153
+ response_llm = await self.llm.ainvoke([message])
154
+
155
+ # Extrai resultado
156
+ return self._parse_llm_validation(response_llm.content)
157
+
158
+ except Exception as e:
159
+ logging.error(f"Erro na validação LLM: {e}")
160
+ return {
161
+ 'valid': False,
162
+ 'score': 0,
163
+ 'reason': f'Erro na validação LLM: {e}',
164
+ 'method': 'llm'
165
+ }
166
+
167
+ def _parse_llm_validation(self, llm_response: str) -> Dict[str, Any]:
168
+ """
169
+ Extrai resultado da validação LLM
170
+
171
+ Args:
172
+ llm_response: Resposta do LLM
173
+
174
+ Returns:
175
+ Resultado parseado
176
+ """
177
+ try:
178
+ # Padrões para extrair informações
179
+ score_pattern = r'PONTUAÇÃO:\s*(\d+)'
180
+ valid_pattern = r'VÁLIDA:\s*(True|False)'
181
+ reason_pattern = r'RAZÃO:\s*(.+?)(?:\n|$)'
182
+
183
+ # Extrai pontuação
184
+ score_match = re.search(score_pattern, llm_response, re.IGNORECASE)
185
+ score = int(score_match.group(1)) if score_match else 0
186
+
187
+ # Extrai validade
188
+ valid_match = re.search(valid_pattern, llm_response, re.IGNORECASE)
189
+ valid = valid_match.group(1).lower() == 'true' if valid_match else False
190
+
191
+ # Extrai razão
192
+ reason_match = re.search(reason_pattern, llm_response, re.IGNORECASE | re.DOTALL)
193
+ reason = reason_match.group(1).strip() if reason_match else 'Sem razão fornecida'
194
+
195
+ return {
196
+ 'valid': valid,
197
+ 'score': score,
198
+ 'reason': reason,
199
+ 'method': 'llm',
200
+ 'raw_response': llm_response
201
+ }
202
+
203
+ except Exception as e:
204
+ logging.error(f"Erro ao parsear validação LLM: {e}")
205
+ return {
206
+ 'valid': False,
207
+ 'score': 0,
208
+ 'reason': f'Erro ao parsear resposta: {e}',
209
+ 'method': 'llm',
210
+ 'raw_response': llm_response
211
+ }
212
+
213
+ def _validate_with_keyword(self, response: str, expected_content: str) -> Dict[str, Any]:
214
+ """
215
+ Valida usando palavras-chave
216
+
217
+ Args:
218
+ response: Resposta para validar
219
+ expected_content: Conteúdo esperado
220
+
221
+ Returns:
222
+ Resultado da validação
223
+ """
224
+ if not expected_content:
225
+ return {
226
+ 'valid': False,
227
+ 'score': 0,
228
+ 'reason': 'Conteúdo esperado não fornecido',
229
+ 'method': 'keyword'
230
+ }
231
+
232
+ try:
233
+ # Normaliza textos
234
+ response_normalized = response.lower().strip()
235
+ expected_normalized = expected_content.lower().strip()
236
+
237
+ # Verifica se contém o conteúdo esperado
238
+ contains_expected = expected_normalized in response_normalized
239
+
240
+ # Calcula score baseado na presença
241
+ score = 100 if contains_expected else 0
242
+
243
+ # Verifica se há erro explícito
244
+ error_keywords = ['erro', 'error', 'falha', 'exception', 'não foi possível']
245
+ has_error = any(keyword in response_normalized for keyword in error_keywords)
246
+
247
+ if has_error:
248
+ score = max(0, score - 50) # Penaliza erros
249
+
250
+ return {
251
+ 'valid': contains_expected and not has_error,
252
+ 'score': score,
253
+ 'reason': f'Conteúdo {"encontrado" if contains_expected else "não encontrado"}. {"Erro detectado" if has_error else "Sem erros"}',
254
+ 'method': 'keyword',
255
+ 'expected_content': expected_content,
256
+ 'contains_expected': contains_expected,
257
+ 'has_error': has_error
258
+ }
259
+
260
+ except Exception as e:
261
+ logging.error(f"Erro na validação por keyword: {e}")
262
+ return {
263
+ 'valid': False,
264
+ 'score': 0,
265
+ 'reason': f'Erro na validação: {e}',
266
+ 'method': 'keyword'
267
+ }
268
+
269
+ def validate_sql_syntax(self, sql_query: str) -> Dict[str, Any]:
270
+ """
271
+ Valida sintaxe SQL básica
272
+
273
+ Args:
274
+ sql_query: Query SQL para validar
275
+
276
+ Returns:
277
+ Resultado da validação de sintaxe
278
+ """
279
+ try:
280
+ if not sql_query or not sql_query.strip():
281
+ return {
282
+ 'valid': False,
283
+ 'reason': 'Query SQL vazia'
284
+ }
285
+
286
+ sql_normalized = sql_query.strip().upper()
287
+
288
+ # Verificações básicas
289
+ checks = {
290
+ 'has_select': 'SELECT' in sql_normalized,
291
+ 'has_from': 'FROM' in sql_normalized,
292
+ 'balanced_parentheses': sql_query.count('(') == sql_query.count(')'),
293
+ 'no_obvious_errors': not any(error in sql_normalized for error in ['ERROR', 'SYNTAX ERROR', 'INVALID'])
294
+ }
295
+
296
+ all_valid = all(checks.values())
297
+
298
+ return {
299
+ 'valid': all_valid,
300
+ 'checks': checks,
301
+ 'reason': 'Sintaxe SQL válida' if all_valid else 'Problemas de sintaxe detectados'
302
+ }
303
+
304
+ except Exception as e:
305
+ return {
306
+ 'valid': False,
307
+ 'reason': f'Erro na validação de sintaxe: {e}'
308
+ }
309
+
310
+ async def batch_validate(self, results: list, method: str = 'llm', expected_content: str = None) -> list:
311
+ """
312
+ Valida múltiplos resultados em lote
313
+
314
+ Args:
315
+ results: Lista de resultados para validar
316
+ method: Método de validação
317
+ expected_content: Conteúdo esperado
318
+
319
+ Returns:
320
+ Lista de validações
321
+ """
322
+ tasks = []
323
+
324
+ for result in results:
325
+ task = self.validate_result(
326
+ question=result.get('question', ''),
327
+ sql_query=result.get('sql_query', ''),
328
+ response=result.get('response', ''),
329
+ method=method,
330
+ expected_content=expected_content
331
+ )
332
+ tasks.append(task)
333
+
334
+ return await asyncio.gather(*tasks, return_exceptions=True)
utils/config.py CHANGED
@@ -12,6 +12,7 @@ load_dotenv()
12
  HUGGINGFACE_API_KEY = os.getenv("HUGGINGFACE_API_KEY")
13
  OPENAI_API_KEY = os.getenv("OPENAI_API_KEY")
14
  ANTHROPIC_API_KEY = os.getenv("ANTHROPIC_API_KEY")
 
15
 
16
  # Configurações do LangSmith (observabilidade)
17
  LANGSMITH_API_KEY = os.getenv("LANGSMITH_API_KEY")
@@ -30,7 +31,9 @@ AVAILABLE_MODELS = {
30
  "GPT-o3-mini": "o3-mini",
31
  "GPT-4o-mini": "gpt-4o-mini",
32
  "GPT-4o": "gpt-4o",
33
- "Claude-3.5-Sonnet": "claude-3-5-sonnet-20241022"
 
 
34
  }
35
 
36
  # Modelos para refinamento (apenas uso interno)
@@ -52,7 +55,9 @@ MAX_TOKENS_MAP = {
52
  "o3-mini": 4096,
53
  "gpt-4o-mini": 4096,
54
  "gpt-4o": 4096,
55
- "claude-3-5-sonnet-20241022": 1024
 
 
56
  }
57
 
58
  # Modelos que usam OpenAI (GPT)
@@ -67,6 +72,12 @@ ANTHROPIC_MODELS = {
67
  "claude-3-5-sonnet-20241022"
68
  }
69
 
 
 
 
 
 
 
70
  # Modelos que usam HuggingFace (para refinamento)
71
  HUGGINGFACE_MODELS = {
72
  "meta-llama/Llama-3.3-70B-Instruct",
@@ -99,6 +110,10 @@ os.makedirs(UPLOAD_DIR, exist_ok=True)
99
  if OPENAI_API_KEY:
100
  os.environ["OPENAI_API_KEY"] = OPENAI_API_KEY
101
 
 
 
 
 
102
  # Configuração das variáveis de ambiente para Anthropic
103
  if ANTHROPIC_API_KEY:
104
  os.environ["ANTHROPIC_API_KEY"] = ANTHROPIC_API_KEY
 
12
  HUGGINGFACE_API_KEY = os.getenv("HUGGINGFACE_API_KEY")
13
  OPENAI_API_KEY = os.getenv("OPENAI_API_KEY")
14
  ANTHROPIC_API_KEY = os.getenv("ANTHROPIC_API_KEY")
15
+ GOOGLE_API_KEY = os.getenv("GOOGLE_API_KEY")
16
 
17
  # Configurações do LangSmith (observabilidade)
18
  LANGSMITH_API_KEY = os.getenv("LANGSMITH_API_KEY")
 
31
  "GPT-o3-mini": "o3-mini",
32
  "GPT-4o-mini": "gpt-4o-mini",
33
  "GPT-4o": "gpt-4o",
34
+ "Claude-3.5-Sonnet": "claude-3-5-sonnet-20241022",
35
+ "Gemini-1.5-Pro": "gemini-1.5-pro",
36
+ "Gemini-2.0-Flash": "gemini-2.0-flash"
37
  }
38
 
39
  # Modelos para refinamento (apenas uso interno)
 
55
  "o3-mini": 4096,
56
  "gpt-4o-mini": 4096,
57
  "gpt-4o": 4096,
58
+ "claude-3-5-sonnet-20241022": 1024,
59
+ "gemini-1.5-pro": 4096,
60
+ "gemini-2.0-flash": 4096
61
  }
62
 
63
  # Modelos que usam OpenAI (GPT)
 
72
  "claude-3-5-sonnet-20241022"
73
  }
74
 
75
+ # Modelos que usam Google (Gemini)
76
+ GOOGLE_MODELS = {
77
+ "gemini-1.5-pro",
78
+ "gemini-2.0-flash"
79
+ }
80
+
81
  # Modelos que usam HuggingFace (para refinamento)
82
  HUGGINGFACE_MODELS = {
83
  "meta-llama/Llama-3.3-70B-Instruct",
 
110
  if OPENAI_API_KEY:
111
  os.environ["OPENAI_API_KEY"] = OPENAI_API_KEY
112
 
113
+ # Configuração das variáveis de ambiente para Google
114
+ if GOOGLE_API_KEY:
115
+ os.environ["GOOGLE_API_KEY"] = GOOGLE_API_KEY
116
+
117
  # Configuração das variáveis de ambiente para Anthropic
118
  if ANTHROPIC_API_KEY:
119
  os.environ["ANTHROPIC_API_KEY"] = ANTHROPIC_API_KEY
utils/database.py CHANGED
@@ -88,6 +88,3 @@ def validate_database(engine) -> bool:
88
  logging.error(f"Erro na validação do banco: {e}")
89
  return False
90
 
91
- # FUNÇÃO REMOVIDA: async_create_engine_and_load_db
92
- # Esta função foi removida junto com create_engine_and_load_db
93
- # Use a nova arquitetura de nós: csv_processing_node.py + database_node.py
 
88
  logging.error(f"Erro na validação do banco: {e}")
89
  return False
90
 
 
 
 
utils/object_manager.py CHANGED
@@ -19,6 +19,8 @@ class ObjectManager:
19
  self._cache_managers: Dict[str, Any] = {}
20
  # Mapeamento para relacionar agentes com seus bancos
21
  self._agent_db_mapping: Dict[str, str] = {}
 
 
22
 
23
  def store_sql_agent(self, agent: Any, db_id: str = None) -> str:
24
  """Armazena agente SQL e retorna ID"""
@@ -128,8 +130,45 @@ class ObjectManager:
128
  self._databases.clear()
129
  self._cache_managers.clear()
130
  self._agent_db_mapping.clear()
 
131
  logging.info("Todos os objetos foram limpos do gerenciador")
132
-
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
133
  def get_stats(self) -> Dict[str, int]:
134
  """Retorna estatísticas dos objetos armazenados"""
135
  return {
@@ -138,7 +177,8 @@ class ObjectManager:
138
  "databases": len(self._databases),
139
  "cache_managers": len(self._cache_managers),
140
  "general_objects": len(self._objects),
141
- "agent_db_mappings": len(self._agent_db_mapping)
 
142
  }
143
 
144
  # Instância global do gerenciador
 
19
  self._cache_managers: Dict[str, Any] = {}
20
  # Mapeamento para relacionar agentes com seus bancos
21
  self._agent_db_mapping: Dict[str, str] = {}
22
+ # Metadados de conexões (CSV/PostgreSQL)
23
+ self._connection_metadata: Dict[str, Dict[str, Any]] = {}
24
 
25
  def store_sql_agent(self, agent: Any, db_id: str = None) -> str:
26
  """Armazena agente SQL e retorna ID"""
 
130
  self._databases.clear()
131
  self._cache_managers.clear()
132
  self._agent_db_mapping.clear()
133
+ self._connection_metadata.clear()
134
  logging.info("Todos os objetos foram limpos do gerenciador")
135
+
136
+ def store_connection_metadata(self, connection_id: str, metadata: Dict[str, Any]) -> str:
137
+ """
138
+ Armazena metadados de conexão
139
+
140
+ Args:
141
+ connection_id: ID da conexão
142
+ metadata: Metadados da conexão
143
+
144
+ Returns:
145
+ ID dos metadados armazenados
146
+ """
147
+ self._connection_metadata[connection_id] = metadata
148
+ logging.info(f"Metadados de conexão armazenados com ID: {connection_id}")
149
+ return connection_id
150
+
151
+ def get_connection_metadata(self, connection_id: str) -> Optional[Dict[str, Any]]:
152
+ """
153
+ Recupera metadados de conexão pelo ID
154
+
155
+ Args:
156
+ connection_id: ID da conexão
157
+
158
+ Returns:
159
+ Metadados da conexão ou None se não encontrado
160
+ """
161
+ return self._connection_metadata.get(connection_id)
162
+
163
+ def get_all_connection_metadata(self) -> Dict[str, Dict[str, Any]]:
164
+ """
165
+ Retorna todos os metadados de conexão
166
+
167
+ Returns:
168
+ Dicionário com todos os metadados
169
+ """
170
+ return self._connection_metadata.copy()
171
+
172
  def get_stats(self) -> Dict[str, int]:
173
  """Retorna estatísticas dos objetos armazenados"""
174
  return {
 
177
  "databases": len(self._databases),
178
  "cache_managers": len(self._cache_managers),
179
  "general_objects": len(self._objects),
180
+ "agent_db_mappings": len(self._agent_db_mapping),
181
+ "connection_metadata": len(self._connection_metadata)
182
  }
183
 
184
  # Instância global do gerenciador
utils/validation.py ADDED
@@ -0,0 +1,299 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Utilitários de validação para o sistema AgentGraph
3
+ """
4
+ import re
5
+ import logging
6
+ from typing import Dict, Any, Tuple, Optional
7
+
8
+
9
+ def validate_postgresql_config(config: Dict[str, Any]) -> Tuple[bool, Optional[str]]:
10
+ """
11
+ Valida configuração postgresql completa
12
+
13
+ Args:
14
+ config: Dicionário com configuração postgresql
15
+
16
+ Returns:
17
+ Tupla (válido, mensagem_erro)
18
+ """
19
+ try:
20
+ # Campos obrigatórios
21
+ required_fields = ["host", "port", "database", "username", "password"]
22
+
23
+ for field in required_fields:
24
+ if field not in config or not config[field]:
25
+ return False, f"Campo obrigatório ausente ou vazio: {field}"
26
+
27
+ # Validação específica do host
28
+ host = str(config["host"]).strip()
29
+ if not host:
30
+ return False, "Host não pode estar vazio"
31
+
32
+ # Validação básica de formato de host
33
+ if not _is_valid_host(host):
34
+ return False, "Formato de host inválido"
35
+
36
+ # Validação da porta
37
+ try:
38
+ port = int(config["port"])
39
+ if port < 1 or port > 65535:
40
+ return False, "Porta deve estar entre 1 e 65535"
41
+ except (ValueError, TypeError):
42
+ return False, "Porta deve ser um número válido"
43
+
44
+ # Validação do nome do banco
45
+ database = str(config["database"]).strip()
46
+ if not database:
47
+ return False, "Nome do banco não pode estar vazio"
48
+
49
+ if not _is_valid_database_name(database):
50
+ return False, "Nome do banco contém caracteres inválidos"
51
+
52
+ # Validação do usuário
53
+ username = str(config["username"]).strip()
54
+ if not username:
55
+ return False, "Nome de usuário não pode estar vazio"
56
+
57
+ if not _is_valid_username(username):
58
+ return False, "Nome de usuário contém caracteres inválidos"
59
+
60
+ # Validação da senha (básica)
61
+ password = str(config["password"])
62
+ if not password:
63
+ return False, "Senha não pode estar vazia"
64
+
65
+ return True, None
66
+
67
+ except Exception as e:
68
+ return False, f"Erro na validação: {e}"
69
+
70
+
71
+ def _is_valid_host(host: str) -> bool:
72
+ """
73
+ Valida formato de host (IP ou hostname)
74
+
75
+ Args:
76
+ host: Host a validar
77
+
78
+ Returns:
79
+ True se válido
80
+ """
81
+ # Regex para IPv4
82
+ ipv4_pattern = r'^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$'
83
+
84
+ # Regex para hostname/FQDN
85
+ hostname_pattern = r'^(?:[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?\.)*[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?$'
86
+
87
+ # Permite localhost
88
+ if host.lower() == 'localhost':
89
+ return True
90
+
91
+ # Valida IPv4
92
+ if re.match(ipv4_pattern, host):
93
+ return True
94
+
95
+ # Valida hostname
96
+ if re.match(hostname_pattern, host):
97
+ return True
98
+
99
+ return False
100
+
101
+
102
+ def _is_valid_database_name(database: str) -> bool:
103
+ """
104
+ Valida nome de banco postgresql
105
+
106
+ Args:
107
+ database: Nome do banco
108
+
109
+ Returns:
110
+ True se válido
111
+ """
112
+ # postgresql: deve começar com letra ou underscore,
113
+ # pode conter letras, números, underscores e hífens
114
+ pattern = r'^[a-zA-Z_][a-zA-Z0-9_-]*$'
115
+
116
+ # Comprimento máximo típico
117
+ if len(database) > 63:
118
+ return False
119
+
120
+ return bool(re.match(pattern, database))
121
+
122
+
123
+ def _is_valid_username(username: str) -> bool:
124
+ """
125
+ Valida nome de usuário postgresql
126
+
127
+ Args:
128
+ username: Nome de usuário
129
+
130
+ Returns:
131
+ True se válido
132
+ """
133
+ # Similar ao nome do banco
134
+ pattern = r'^[a-zA-Z_][a-zA-Z0-9_-]*$'
135
+
136
+ # Comprimento máximo típico
137
+ if len(username) > 63:
138
+ return False
139
+
140
+ return bool(re.match(pattern, username))
141
+
142
+
143
+ def validate_csv_file_path(file_path: str) -> Tuple[bool, Optional[str]]:
144
+ """
145
+ Valida caminho de arquivo csv
146
+
147
+ Args:
148
+ file_path: Caminho do arquivo
149
+
150
+ Returns:
151
+ Tupla (válido, mensagem_erro)
152
+ """
153
+ try:
154
+ import os
155
+
156
+ if not file_path:
157
+ return False, "Caminho do arquivo não pode estar vazio"
158
+
159
+ if not os.path.exists(file_path):
160
+ return False, f"Arquivo não encontrado: {file_path}"
161
+
162
+ if not file_path.lower().endswith('.csv'):
163
+ return False, "Arquivo deve ter extensão .csv"
164
+
165
+ # Verifica se é um arquivo (não diretório)
166
+ if not os.path.isfile(file_path):
167
+ return False, "Caminho deve apontar para um arquivo"
168
+
169
+ # Verifica tamanho do arquivo
170
+ file_size = os.path.getsize(file_path)
171
+ if file_size == 0:
172
+ return False, "Arquivo csv está vazio"
173
+
174
+ # Limite de 5GB
175
+ if file_size > 5 * 1024 * 1024 * 1024:
176
+ return False, "Arquivo muito grande (máximo 5GB)"
177
+
178
+ return True, None
179
+
180
+ except Exception as e:
181
+ return False, f"Erro na validação do arquivo: {e}"
182
+
183
+
184
+ def validate_connection_state(state: Dict[str, Any]) -> Tuple[bool, Optional[str]]:
185
+ """
186
+ Valida estado de conexão completo
187
+
188
+ Args:
189
+ state: Estado da conexão
190
+
191
+ Returns:
192
+ Tupla (válido, mensagem_erro)
193
+ """
194
+ try:
195
+ connection_type = state.get("connection_type", "csv")
196
+
197
+ if connection_type.lower() not in ["csv", "postgresql"]:
198
+ return False, f"Tipo de conexão inválido: {connection_type}"
199
+
200
+ if connection_type.lower() == "postgresql":
201
+ postgresql_config = state.get("postgresql_config")
202
+ if not postgresql_config:
203
+ return False, "Configuração postgresql ausente"
204
+
205
+ return validate_postgresql_config(postgresql_config)
206
+
207
+ elif connection_type.lower() == "csv":
208
+ file_path = state.get("file_path")
209
+ if file_path:
210
+ return validate_csv_file_path(file_path)
211
+ else:
212
+ # Verifica se há banco existente
213
+ import os
214
+ from utils.config import SQL_DB_PATH
215
+
216
+ if not os.path.exists(SQL_DB_PATH):
217
+ return False, "Nenhum arquivo csv fornecido e nenhum banco existente"
218
+
219
+ return True, None
220
+
221
+ return True, None
222
+
223
+ except Exception as e:
224
+ return False, f"Erro na validação do estado: {e}"
225
+
226
+
227
+ def sanitize_postgresql_config(config: Dict[str, Any]) -> Dict[str, Any]:
228
+ """
229
+ Sanitiza configuração postgresql removendo espaços e normalizando
230
+
231
+ Args:
232
+ config: Configuração original
233
+
234
+ Returns:
235
+ Configuração sanitizada
236
+ """
237
+ try:
238
+ sanitized = {}
239
+
240
+ # Host
241
+ sanitized["host"] = str(config.get("host", "")).strip()
242
+
243
+ # Porta
244
+ try:
245
+ sanitized["port"] = int(config.get("port", 5432))
246
+ except (ValueError, TypeError):
247
+ sanitized["port"] = 5432
248
+
249
+ # Database
250
+ sanitized["database"] = str(config.get("database", "")).strip()
251
+
252
+ # Username
253
+ sanitized["username"] = str(config.get("username", "")).strip()
254
+
255
+ # Password (não remove espaços - pode ser intencional)
256
+ sanitized["password"] = str(config.get("password", ""))
257
+
258
+ return sanitized
259
+
260
+ except Exception as e:
261
+ logging.error(f"Erro ao sanitizar configuração postgresql: {e}")
262
+ return config
263
+
264
+
265
+ def get_connection_error_message(error: Exception) -> str:
266
+ """
267
+ Converte erro de conexão em mensagem amigável
268
+
269
+ Args:
270
+ error: Exceção capturada
271
+
272
+ Returns:
273
+ Mensagem de erro amigável
274
+ """
275
+ error_str = str(error).lower()
276
+
277
+ if "password authentication failed" in error_str:
278
+ return "❌ Falha na autenticação: Usuário ou senha incorretos"
279
+
280
+ elif "could not connect to server" in error_str:
281
+ return "❌ Não foi possível conectar ao servidor: Verifique host e porta"
282
+
283
+ elif "database" in error_str and "does not exist" in error_str:
284
+ return "❌ Banco de dados não existe: Verifique o nome do banco"
285
+
286
+ elif "connection refused" in error_str:
287
+ return "❌ Conexão recusada: Servidor postgresql pode estar desligado"
288
+
289
+ elif "timeout" in error_str:
290
+ return "❌ Timeout na conexão: Servidor demorou muito para responder"
291
+
292
+ elif "permission denied" in error_str:
293
+ return "❌ Permissão negada: Usuário não tem acesso ao banco"
294
+
295
+ elif "too many connections" in error_str:
296
+ return "❌ Muitas conexões: Servidor postgresql está sobrecarregado"
297
+
298
+ else:
299
+ return f"❌ Erro de conexão: {str(error)}"