ss
Browse files- agents/processing_agent.py +13 -1
- agents/sql_agent.py +55 -16
- agents/tools.py +107 -60
- app.py +647 -43
- graphs/main_graph.py +139 -10
- nodes/agent_node.py +18 -3
- nodes/connection_selection_node.py +161 -0
- nodes/custom_nodes.py +2 -2
- nodes/database_node.py +56 -1
- nodes/postgresql_connection_node.py +293 -0
- nodes/processing_node.py +280 -33
- nodes/query_node.py +31 -2
- requirements.txt +2 -0
- testes/README.md +275 -0
- testes/__init__.py +1 -0
- testes/app_teste.py +494 -0
- testes/report_generator.py +335 -0
- testes/reports/.gitkeep +1 -0
- testes/requirements.txt +5 -0
- testes/static/js/app.js +748 -0
- testes/templates/index.html +578 -0
- testes/test_runner.py +673 -0
- testes/test_system.py +287 -0
- testes/test_validator.py +334 -0
- utils/config.py +17 -2
- utils/database.py +0 -3
- utils/object_manager.py +42 -2
- utils/validation.py +299 -0
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=
|
| 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 |
-
|
|
|
|
| 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,
|
| 106 |
"""
|
| 107 |
-
Prepara o contexto inicial para o Processing Agent
|
|
|
|
|
|
|
|
|
|
| 108 |
|
| 109 |
Args:
|
| 110 |
user_query: Pergunta do usuário
|
| 111 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 112 |
|
| 113 |
Returns:
|
| 114 |
Contexto formatado para o Processing Agent
|
| 115 |
"""
|
| 116 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 139 |
|
| 140 |
-
|
| 141 |
|
| 142 |
-
|
| 143 |
-
|
| 144 |
-
|
| 145 |
-
|
| 146 |
-
|
| 147 |
-
|
| 148 |
-
|
| 149 |
-
|
| 150 |
-
|
| 151 |
-
|
| 152 |
-
|
| 153 |
-
|
| 154 |
-
|
| 155 |
-
|
| 156 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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.
|
| 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 |
-
|
|
|
|
| 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 já 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
|
| 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
|
| 177 |
if not file.name.lower().endswith('.csv'):
|
| 178 |
-
return "❌ Por favor, selecione um arquivo
|
| 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
|
| 308 |
|
| 309 |
Args:
|
| 310 |
-
file: Arquivo
|
| 311 |
|
| 312 |
Returns:
|
| 313 |
-
Tupla com (feedback, chat_limpo, grafico_limpo)
|
| 314 |
"""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 315 |
feedback = handle_csv_upload(file)
|
| 316 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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
|
| 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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 365 |
|
| 366 |
with gr.Column(scale=4):
|
| 367 |
-
gr.Markdown("##
|
| 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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
| 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 |
-
"
|
| 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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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
|
| 136 |
-
return "
|
|
|
|
| 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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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
|
| 45 |
obj_manager = get_object_manager()
|
| 46 |
|
| 47 |
-
# Usa os IDs do GraphManager (que são globais)
|
| 48 |
try:
|
| 49 |
-
#
|
| 50 |
-
|
| 51 |
-
|
| 52 |
-
|
| 53 |
-
|
| 54 |
-
|
| 55 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 56 |
return state
|
| 57 |
|
| 58 |
-
|
| 59 |
-
|
| 60 |
-
|
| 61 |
-
|
| 62 |
-
|
| 63 |
-
|
| 64 |
-
|
| 65 |
-
|
| 66 |
-
|
| 67 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
| 76 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 77 |
|
| 78 |
-
|
| 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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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)}"
|