Delete nodes
Browse files- nodes/agent_node.py +0 -126
- nodes/cache_node.py +0 -230
- nodes/csv_processing_node.py +0 -815
- nodes/custom_nodes.py +0 -297
- nodes/database_node.py +0 -226
- nodes/graph_generation_node.py +0 -1015
- nodes/graph_selection_node.py +0 -147
- nodes/query_node.py +0 -232
- nodes/refinement_node.py +0 -141
nodes/agent_node.py
DELETED
|
@@ -1,126 +0,0 @@
|
|
| 1 |
-
"""
|
| 2 |
-
Nó principal do agente SQL para LangGraph - Versão refatorada
|
| 3 |
-
"""
|
| 4 |
-
import logging
|
| 5 |
-
from typing import Dict, Any, TypedDict, Optional
|
| 6 |
-
|
| 7 |
-
from utils.object_manager import get_object_manager
|
| 8 |
-
|
| 9 |
-
class AgentState(TypedDict):
|
| 10 |
-
"""Estado do agente LangGraph - apenas dados serializáveis"""
|
| 11 |
-
user_input: str
|
| 12 |
-
selected_model: str
|
| 13 |
-
response: str
|
| 14 |
-
advanced_mode: bool
|
| 15 |
-
execution_time: float
|
| 16 |
-
error: Optional[str]
|
| 17 |
-
intermediate_steps: list
|
| 18 |
-
# Dados serializáveis do banco
|
| 19 |
-
db_sample_dict: dict
|
| 20 |
-
# IDs para recuperar objetos não-serializáveis
|
| 21 |
-
agent_id: str
|
| 22 |
-
engine_id: str
|
| 23 |
-
cache_id: str
|
| 24 |
-
# Campos relacionados a gráficos
|
| 25 |
-
query_type: str # 'sql_query', 'sql_query_graphic', 'prediction'
|
| 26 |
-
sql_query_extracted: Optional[str] # Query SQL extraída da resposta do agente
|
| 27 |
-
graph_type: Optional[str] # Tipo de gráfico escolhido pela LLM
|
| 28 |
-
graph_data: Optional[dict] # Dados preparados para o gráfico (serializável)
|
| 29 |
-
graph_image_id: Optional[str] # ID da imagem do gráfico no ObjectManager
|
| 30 |
-
graph_generated: bool # Se o gráfico foi gerado com sucesso
|
| 31 |
-
graph_error: Optional[str] # Erro na geração do gráfico, se houver
|
| 32 |
-
|
| 33 |
-
async def initialize_agent_components_node(state: Dict[str, Any]) -> Dict[str, Any]:
|
| 34 |
-
"""
|
| 35 |
-
Nó para inicializar componentes do agente
|
| 36 |
-
|
| 37 |
-
Args:
|
| 38 |
-
state: Estado inicial
|
| 39 |
-
|
| 40 |
-
Returns:
|
| 41 |
-
Estado com componentes inicializados
|
| 42 |
-
"""
|
| 43 |
-
try:
|
| 44 |
-
obj_manager = get_object_manager()
|
| 45 |
-
|
| 46 |
-
# Verifica se os IDs necessários estão presentes
|
| 47 |
-
required_ids = ["agent_id", "engine_id", "cache_id"]
|
| 48 |
-
for id_name in required_ids:
|
| 49 |
-
if not state.get(id_name):
|
| 50 |
-
raise ValueError(f"ID necessário não encontrado: {id_name}")
|
| 51 |
-
|
| 52 |
-
# Verifica se os objetos existem
|
| 53 |
-
sql_agent = obj_manager.get_sql_agent(state["agent_id"])
|
| 54 |
-
engine = obj_manager.get_engine(state["engine_id"])
|
| 55 |
-
cache_manager = obj_manager.get_cache_manager(state["cache_id"])
|
| 56 |
-
|
| 57 |
-
if not all([sql_agent, engine, cache_manager]):
|
| 58 |
-
raise ValueError("Um ou mais componentes não foram encontrados")
|
| 59 |
-
|
| 60 |
-
state["components_ready"] = True
|
| 61 |
-
logging.info("[AGENT] Componentes inicializados com sucesso")
|
| 62 |
-
|
| 63 |
-
except Exception as e:
|
| 64 |
-
error_msg = f"Erro ao inicializar componentes: {e}"
|
| 65 |
-
logging.error(f"[AGENT] {error_msg}")
|
| 66 |
-
state["error"] = error_msg
|
| 67 |
-
state["components_ready"] = False
|
| 68 |
-
|
| 69 |
-
return state
|
| 70 |
-
|
| 71 |
-
def should_refine_response(state: Dict[str, Any]) -> str:
|
| 72 |
-
"""
|
| 73 |
-
Função condicional para determinar se deve refinar a resposta
|
| 74 |
-
|
| 75 |
-
Args:
|
| 76 |
-
state: Estado atual do agente
|
| 77 |
-
|
| 78 |
-
Returns:
|
| 79 |
-
Nome do próximo nó
|
| 80 |
-
"""
|
| 81 |
-
if state.get("advanced_mode", False) and not state.get("error"):
|
| 82 |
-
return "refine_response"
|
| 83 |
-
else:
|
| 84 |
-
return "cache_response"
|
| 85 |
-
|
| 86 |
-
def should_generate_graph(state: Dict[str, Any]) -> str:
|
| 87 |
-
"""
|
| 88 |
-
Função condicional para determinar se deve gerar gráfico
|
| 89 |
-
|
| 90 |
-
Args:
|
| 91 |
-
state: Estado atual do agente
|
| 92 |
-
|
| 93 |
-
Returns:
|
| 94 |
-
Nome do próximo nó
|
| 95 |
-
"""
|
| 96 |
-
query_type = state.get("query_type", "sql_query")
|
| 97 |
-
has_error = state.get("error") is not None
|
| 98 |
-
|
| 99 |
-
# Só gera gráfico se for sql_query_graphic e não houver erro
|
| 100 |
-
if query_type == "sql_query_graphic" and not has_error:
|
| 101 |
-
return "graph_selection"
|
| 102 |
-
else:
|
| 103 |
-
# Pula para refinamento ou cache dependendo do modo avançado
|
| 104 |
-
return should_refine_response(state)
|
| 105 |
-
|
| 106 |
-
class AgentNodeManager:
|
| 107 |
-
"""
|
| 108 |
-
Gerenciador dos nós do agente - versão refatorada
|
| 109 |
-
"""
|
| 110 |
-
|
| 111 |
-
def __init__(self):
|
| 112 |
-
self.node_functions = {
|
| 113 |
-
"initialize_components": initialize_agent_components_node
|
| 114 |
-
}
|
| 115 |
-
self.conditional_functions = {
|
| 116 |
-
"should_refine": should_refine_response,
|
| 117 |
-
"should_generate_graph": should_generate_graph
|
| 118 |
-
}
|
| 119 |
-
|
| 120 |
-
def get_node_function(self, node_name: str):
|
| 121 |
-
"""Retorna função do nó pelo nome"""
|
| 122 |
-
return self.node_functions.get(node_name)
|
| 123 |
-
|
| 124 |
-
def get_conditional_function(self, condition_name: str):
|
| 125 |
-
"""Retorna função condicional pelo nome"""
|
| 126 |
-
return self.conditional_functions.get(condition_name)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
nodes/cache_node.py
DELETED
|
@@ -1,230 +0,0 @@
|
|
| 1 |
-
"""
|
| 2 |
-
Nó para gerenciamento de cache e histórico
|
| 3 |
-
"""
|
| 4 |
-
import logging
|
| 5 |
-
from typing import Dict, Any
|
| 6 |
-
|
| 7 |
-
from utils.object_manager import get_object_manager
|
| 8 |
-
|
| 9 |
-
async def update_history_node(state: Dict[str, Any]) -> Dict[str, Any]:
|
| 10 |
-
"""
|
| 11 |
-
Nó para atualizar histórico e logs
|
| 12 |
-
|
| 13 |
-
Args:
|
| 14 |
-
state: Estado atual do agente
|
| 15 |
-
|
| 16 |
-
Returns:
|
| 17 |
-
Estado atualizado
|
| 18 |
-
"""
|
| 19 |
-
try:
|
| 20 |
-
obj_manager = get_object_manager()
|
| 21 |
-
cache_id = state.get("cache_id")
|
| 22 |
-
|
| 23 |
-
if not cache_id:
|
| 24 |
-
logging.warning("[HISTORY] ID do cache não encontrado")
|
| 25 |
-
return state
|
| 26 |
-
|
| 27 |
-
cache_manager = obj_manager.get_cache_manager(cache_id)
|
| 28 |
-
if not cache_manager:
|
| 29 |
-
logging.warning("[HISTORY] Cache manager não encontrado")
|
| 30 |
-
return state
|
| 31 |
-
|
| 32 |
-
# Adiciona ao histórico de logs
|
| 33 |
-
history_entry = {
|
| 34 |
-
"Modelo AgentSQL": state.get("selected_model", ""),
|
| 35 |
-
"Pergunta": state.get("user_input", ""),
|
| 36 |
-
"Resposta": state.get("response", ""),
|
| 37 |
-
"Tempo de Resposta (s)": round(state.get("execution_time", 0.0), 2),
|
| 38 |
-
"Modo Avançado": state.get("advanced_mode", False),
|
| 39 |
-
"Refinado": state.get("refined", False),
|
| 40 |
-
"Erro": state.get("error"),
|
| 41 |
-
"Tipo de Query": state.get("query_type", "sql_query")
|
| 42 |
-
}
|
| 43 |
-
cache_manager.add_to_history(history_entry)
|
| 44 |
-
|
| 45 |
-
# Atualiza histórico recente
|
| 46 |
-
cache_manager.update_recent_history(
|
| 47 |
-
state.get("user_input", ""),
|
| 48 |
-
state.get("response", "")
|
| 49 |
-
)
|
| 50 |
-
|
| 51 |
-
state["history_updated"] = True
|
| 52 |
-
logging.info("[HISTORY] Histórico atualizado")
|
| 53 |
-
|
| 54 |
-
except Exception as e:
|
| 55 |
-
error_msg = f"Erro ao atualizar histórico: {e}"
|
| 56 |
-
logging.error(f"[HISTORY] {error_msg}")
|
| 57 |
-
state["history_error"] = error_msg
|
| 58 |
-
|
| 59 |
-
return state
|
| 60 |
-
|
| 61 |
-
async def cache_response_node(state: Dict[str, Any]) -> Dict[str, Any]:
|
| 62 |
-
"""
|
| 63 |
-
Nó para armazenar resposta no cache
|
| 64 |
-
|
| 65 |
-
Args:
|
| 66 |
-
state: Estado com resposta a ser cacheada
|
| 67 |
-
|
| 68 |
-
Returns:
|
| 69 |
-
Estado atualizado
|
| 70 |
-
"""
|
| 71 |
-
try:
|
| 72 |
-
obj_manager = get_object_manager()
|
| 73 |
-
cache_id = state.get("cache_id")
|
| 74 |
-
|
| 75 |
-
if not cache_id:
|
| 76 |
-
logging.warning("[CACHE] ID do cache não encontrado")
|
| 77 |
-
return state
|
| 78 |
-
|
| 79 |
-
cache_manager = obj_manager.get_cache_manager(cache_id)
|
| 80 |
-
if not cache_manager:
|
| 81 |
-
logging.warning("[CACHE] Cache manager não encontrado")
|
| 82 |
-
return state
|
| 83 |
-
|
| 84 |
-
user_input = state.get("user_input", "")
|
| 85 |
-
response = state.get("response", "")
|
| 86 |
-
|
| 87 |
-
if user_input and response and not state.get("error"):
|
| 88 |
-
cache_manager.cache_response(user_input, response)
|
| 89 |
-
state["cached"] = True
|
| 90 |
-
logging.info(f"[CACHE] Resposta cacheada para: {user_input[:50]}...")
|
| 91 |
-
else:
|
| 92 |
-
state["cached"] = False
|
| 93 |
-
logging.info("[CACHE] Resposta não cacheada (erro ou dados insuficientes)")
|
| 94 |
-
|
| 95 |
-
except Exception as e:
|
| 96 |
-
error_msg = f"Erro ao cachear resposta: {e}"
|
| 97 |
-
logging.error(f"[CACHE] {error_msg}")
|
| 98 |
-
state["cache_error"] = error_msg
|
| 99 |
-
|
| 100 |
-
return state
|
| 101 |
-
|
| 102 |
-
async def get_cache_stats_node(state: Dict[str, Any]) -> Dict[str, Any]:
|
| 103 |
-
"""
|
| 104 |
-
Nó para obter estatísticas do cache
|
| 105 |
-
|
| 106 |
-
Args:
|
| 107 |
-
state: Estado atual
|
| 108 |
-
|
| 109 |
-
Returns:
|
| 110 |
-
Estado com estatísticas do cache
|
| 111 |
-
"""
|
| 112 |
-
try:
|
| 113 |
-
obj_manager = get_object_manager()
|
| 114 |
-
cache_id = state.get("cache_id")
|
| 115 |
-
|
| 116 |
-
if not cache_id:
|
| 117 |
-
state["cache_stats"] = {}
|
| 118 |
-
return state
|
| 119 |
-
|
| 120 |
-
cache_manager = obj_manager.get_cache_manager(cache_id)
|
| 121 |
-
if not cache_manager:
|
| 122 |
-
state["cache_stats"] = {}
|
| 123 |
-
return state
|
| 124 |
-
|
| 125 |
-
# Coleta estatísticas
|
| 126 |
-
cache_stats = {
|
| 127 |
-
"cached_queries": len(cache_manager.query_cache),
|
| 128 |
-
"history_entries": len(cache_manager.history_log),
|
| 129 |
-
"recent_history_size": len(cache_manager.recent_history),
|
| 130 |
-
"cache_hit_rate": 0.0 # Seria calculado com mais dados históricos
|
| 131 |
-
}
|
| 132 |
-
|
| 133 |
-
# Calcula taxa de acerto aproximada
|
| 134 |
-
if cache_stats["history_entries"] > 0:
|
| 135 |
-
# Estimativa simples baseada em queries repetidas
|
| 136 |
-
unique_queries = len(set(entry.get("Pergunta", "") for entry in cache_manager.history_log))
|
| 137 |
-
if unique_queries > 0:
|
| 138 |
-
cache_stats["cache_hit_rate"] = max(0, 1 - (unique_queries / cache_stats["history_entries"]))
|
| 139 |
-
|
| 140 |
-
state["cache_stats"] = cache_stats
|
| 141 |
-
logging.info(f"[CACHE] Estatísticas coletadas: {cache_stats}")
|
| 142 |
-
|
| 143 |
-
except Exception as e:
|
| 144 |
-
error_msg = f"Erro ao obter estatísticas do cache: {e}"
|
| 145 |
-
logging.error(f"[CACHE] {error_msg}")
|
| 146 |
-
state["cache_stats"] = {}
|
| 147 |
-
|
| 148 |
-
return state
|
| 149 |
-
|
| 150 |
-
async def clear_cache_node(state: Dict[str, Any]) -> Dict[str, Any]:
|
| 151 |
-
"""
|
| 152 |
-
Nó para limpar cache
|
| 153 |
-
|
| 154 |
-
Args:
|
| 155 |
-
state: Estado atual
|
| 156 |
-
|
| 157 |
-
Returns:
|
| 158 |
-
Estado atualizado
|
| 159 |
-
"""
|
| 160 |
-
try:
|
| 161 |
-
obj_manager = get_object_manager()
|
| 162 |
-
cache_id = state.get("cache_id")
|
| 163 |
-
|
| 164 |
-
if not cache_id:
|
| 165 |
-
state["cache_cleared"] = False
|
| 166 |
-
return state
|
| 167 |
-
|
| 168 |
-
cache_manager = obj_manager.get_cache_manager(cache_id)
|
| 169 |
-
if not cache_manager:
|
| 170 |
-
state["cache_cleared"] = False
|
| 171 |
-
return state
|
| 172 |
-
|
| 173 |
-
# Limpa cache
|
| 174 |
-
cache_manager.clear_cache()
|
| 175 |
-
state["cache_cleared"] = True
|
| 176 |
-
|
| 177 |
-
logging.info("[CACHE] Cache limpo")
|
| 178 |
-
|
| 179 |
-
except Exception as e:
|
| 180 |
-
error_msg = f"Erro ao limpar cache: {e}"
|
| 181 |
-
logging.error(f"[CACHE] {error_msg}")
|
| 182 |
-
state["cache_cleared"] = False
|
| 183 |
-
state["cache_error"] = error_msg
|
| 184 |
-
|
| 185 |
-
return state
|
| 186 |
-
|
| 187 |
-
async def check_cache_node(state: Dict[str, Any]) -> Dict[str, Any]:
|
| 188 |
-
"""
|
| 189 |
-
Nó para verificar se existe resposta em cache
|
| 190 |
-
|
| 191 |
-
Args:
|
| 192 |
-
state: Estado com consulta do usuário
|
| 193 |
-
|
| 194 |
-
Returns:
|
| 195 |
-
Estado com resultado da verificação de cache
|
| 196 |
-
"""
|
| 197 |
-
try:
|
| 198 |
-
obj_manager = get_object_manager()
|
| 199 |
-
cache_id = state.get("cache_id")
|
| 200 |
-
user_input = state.get("user_input", "")
|
| 201 |
-
|
| 202 |
-
if not cache_id or not user_input:
|
| 203 |
-
state["cache_hit"] = False
|
| 204 |
-
return state
|
| 205 |
-
|
| 206 |
-
cache_manager = obj_manager.get_cache_manager(cache_id)
|
| 207 |
-
if not cache_manager:
|
| 208 |
-
state["cache_hit"] = False
|
| 209 |
-
return state
|
| 210 |
-
|
| 211 |
-
# Verifica cache
|
| 212 |
-
cached_response = cache_manager.get_cached_response(user_input)
|
| 213 |
-
|
| 214 |
-
if cached_response:
|
| 215 |
-
state["cache_hit"] = True
|
| 216 |
-
state["response"] = cached_response
|
| 217 |
-
state["execution_time"] = 0.0
|
| 218 |
-
state["error"] = None
|
| 219 |
-
logging.info(f"[CACHE] Hit para: {user_input[:50]}...")
|
| 220 |
-
else:
|
| 221 |
-
state["cache_hit"] = False
|
| 222 |
-
logging.info(f"[CACHE] Miss para: {user_input[:50]}...")
|
| 223 |
-
|
| 224 |
-
except Exception as e:
|
| 225 |
-
error_msg = f"Erro ao verificar cache: {e}"
|
| 226 |
-
logging.error(f"[CACHE] {error_msg}")
|
| 227 |
-
state["cache_hit"] = False
|
| 228 |
-
state["cache_error"] = error_msg
|
| 229 |
-
|
| 230 |
-
return state
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
nodes/csv_processing_node.py
DELETED
|
@@ -1,815 +0,0 @@
|
|
| 1 |
-
"""
|
| 2 |
-
Nó para processamento de arquivos CSV
|
| 3 |
-
"""
|
| 4 |
-
import os
|
| 5 |
-
import shutil
|
| 6 |
-
import logging
|
| 7 |
-
import time
|
| 8 |
-
import pandas as pd
|
| 9 |
-
import numpy as np
|
| 10 |
-
from typing import Dict, Any, TypedDict, List, Optional
|
| 11 |
-
from sqlalchemy.types import DateTime, Integer, Float, String, Boolean
|
| 12 |
-
from concurrent.futures import ThreadPoolExecutor, ProcessPoolExecutor
|
| 13 |
-
import multiprocessing as mp
|
| 14 |
-
|
| 15 |
-
from utils.config import UPLOADED_CSV_PATH
|
| 16 |
-
from utils.object_manager import get_object_manager
|
| 17 |
-
import numpy as np
|
| 18 |
-
|
| 19 |
-
def analyze_numeric_column(sample_values: pd.Series) -> Dict[str, Any]:
|
| 20 |
-
"""
|
| 21 |
-
Análise otimizada para detectar se coluna é numérica
|
| 22 |
-
|
| 23 |
-
Args:
|
| 24 |
-
sample_values: Amostra dos valores da coluna
|
| 25 |
-
|
| 26 |
-
Returns:
|
| 27 |
-
Dicionário com análise numérica
|
| 28 |
-
"""
|
| 29 |
-
analysis = {
|
| 30 |
-
"is_numeric": False,
|
| 31 |
-
"is_integer": False,
|
| 32 |
-
"numeric_ratio": 0.0,
|
| 33 |
-
"has_decimals": False
|
| 34 |
-
}
|
| 35 |
-
|
| 36 |
-
if len(sample_values) == 0:
|
| 37 |
-
return analysis
|
| 38 |
-
|
| 39 |
-
# Converte para string e limpa valores
|
| 40 |
-
str_values = sample_values.astype(str).str.strip()
|
| 41 |
-
|
| 42 |
-
# Remove valores vazios e nulos
|
| 43 |
-
clean_values = str_values[
|
| 44 |
-
~str_values.isin(['', 'nan', 'null', 'none', '-', 'NaN', 'NULL'])
|
| 45 |
-
]
|
| 46 |
-
|
| 47 |
-
if len(clean_values) == 0:
|
| 48 |
-
return analysis
|
| 49 |
-
|
| 50 |
-
# Tenta conversão numérica vetorizada
|
| 51 |
-
try:
|
| 52 |
-
# Substitui vírgulas por pontos para formato brasileiro
|
| 53 |
-
numeric_values = clean_values.str.replace(',', '.', regex=False)
|
| 54 |
-
|
| 55 |
-
# Tenta conversão para float
|
| 56 |
-
converted = pd.to_numeric(numeric_values, errors='coerce')
|
| 57 |
-
|
| 58 |
-
# Conta valores válidos
|
| 59 |
-
valid_count = converted.notna().sum()
|
| 60 |
-
total_count = len(clean_values)
|
| 61 |
-
|
| 62 |
-
analysis["numeric_ratio"] = valid_count / total_count if total_count > 0 else 0
|
| 63 |
-
|
| 64 |
-
# Se mais de 80% são números válidos, considera numérico
|
| 65 |
-
if analysis["numeric_ratio"] > 0.8:
|
| 66 |
-
analysis["is_numeric"] = True
|
| 67 |
-
|
| 68 |
-
# Verifica se são inteiros
|
| 69 |
-
valid_numbers = converted.dropna()
|
| 70 |
-
if len(valid_numbers) > 0:
|
| 71 |
-
# Verifica se todos os números válidos são inteiros
|
| 72 |
-
analysis["is_integer"] = all(
|
| 73 |
-
float(x).is_integer() for x in valid_numbers
|
| 74 |
-
if not pd.isna(x) and abs(x) < 1e15 # Evita overflow
|
| 75 |
-
)
|
| 76 |
-
analysis["has_decimals"] = not analysis["is_integer"]
|
| 77 |
-
|
| 78 |
-
except Exception as e:
|
| 79 |
-
logging.debug(f"Erro na análise numérica: {e}")
|
| 80 |
-
analysis["is_numeric"] = False
|
| 81 |
-
|
| 82 |
-
return analysis
|
| 83 |
-
|
| 84 |
-
def detect_date_format(date_string: str) -> str:
|
| 85 |
-
"""
|
| 86 |
-
Detecta o formato mais provável de uma string de data
|
| 87 |
-
|
| 88 |
-
Args:
|
| 89 |
-
date_string: String para analisar
|
| 90 |
-
|
| 91 |
-
Returns:
|
| 92 |
-
'iso', 'american', 'brazilian' ou 'auto'
|
| 93 |
-
"""
|
| 94 |
-
date_str = str(date_string).strip()
|
| 95 |
-
|
| 96 |
-
# Formato ISO (YYYY-MM-DD ou YYYY/MM/DD)
|
| 97 |
-
if len(date_str) >= 10 and date_str[4] in ['-', '/', '.'] and date_str[7] in ['-', '/', '.']:
|
| 98 |
-
if date_str[:4].isdigit() and int(date_str[:4]) > 1900:
|
| 99 |
-
return 'iso'
|
| 100 |
-
|
| 101 |
-
# Verifica se pode ser formato americano (MM/DD/YYYY)
|
| 102 |
-
if '/' in date_str:
|
| 103 |
-
parts = date_str.split('/')
|
| 104 |
-
if len(parts) == 3:
|
| 105 |
-
try:
|
| 106 |
-
month, day, year = int(parts[0]), int(parts[1]), int(parts[2])
|
| 107 |
-
# Se o primeiro número é > 12, provavelmente é DD/MM/YYYY
|
| 108 |
-
if month > 12:
|
| 109 |
-
return 'brazilian'
|
| 110 |
-
# Se o segundo número é > 12, provavelmente é MM/DD/YYYY
|
| 111 |
-
elif day > 12:
|
| 112 |
-
return 'american'
|
| 113 |
-
# Se ambos <= 12, é ambíguo, assume brasileiro por padrão
|
| 114 |
-
else:
|
| 115 |
-
return 'brazilian'
|
| 116 |
-
except:
|
| 117 |
-
pass
|
| 118 |
-
|
| 119 |
-
# Formato brasileiro por padrão (DD/MM/YYYY, DD-MM-YYYY, DD.MM.YYYY)
|
| 120 |
-
return 'brazilian'
|
| 121 |
-
|
| 122 |
-
def smart_date_conversion(date_string: str):
|
| 123 |
-
"""
|
| 124 |
-
Converte string para data usando detecção inteligente de formato
|
| 125 |
-
|
| 126 |
-
Args:
|
| 127 |
-
date_string: String da data
|
| 128 |
-
|
| 129 |
-
Returns:
|
| 130 |
-
Timestamp do pandas ou levanta exceção
|
| 131 |
-
"""
|
| 132 |
-
format_type = detect_date_format(date_string)
|
| 133 |
-
|
| 134 |
-
if format_type == 'iso':
|
| 135 |
-
return pd.to_datetime(date_string, errors='raise')
|
| 136 |
-
elif format_type == 'american':
|
| 137 |
-
return pd.to_datetime(date_string, format='%m/%d/%Y', errors='raise')
|
| 138 |
-
elif format_type == 'brazilian':
|
| 139 |
-
return pd.to_datetime(date_string, dayfirst=True, errors='raise')
|
| 140 |
-
else:
|
| 141 |
-
# Fallback para detecção automática
|
| 142 |
-
return pd.to_datetime(date_string, errors='raise')
|
| 143 |
-
|
| 144 |
-
async def process_dates_advanced(series: pd.Series) -> pd.Series:
|
| 145 |
-
"""
|
| 146 |
-
Processa datas com múltiplos formatos de forma robusta
|
| 147 |
-
|
| 148 |
-
Args:
|
| 149 |
-
series: Série pandas com datas em formato texto
|
| 150 |
-
|
| 151 |
-
Returns:
|
| 152 |
-
Série com datas convertidas para datetime
|
| 153 |
-
"""
|
| 154 |
-
# Formatos de data para tentar em ordem de prioridade
|
| 155 |
-
date_formats = [
|
| 156 |
-
'%d/%m/%Y', # 01/12/2024
|
| 157 |
-
'%d-%m-%Y', # 01-12-2024
|
| 158 |
-
'%Y-%m-%d', # 2024-12-01
|
| 159 |
-
'%d/%m/%y', # 01/12/24
|
| 160 |
-
'%d-%m-%y', # 01-12-24
|
| 161 |
-
'%Y/%m/%d', # 2024/12/01
|
| 162 |
-
'%d.%m.%Y', # 01.12.2024
|
| 163 |
-
'%Y.%m.%d', # 2024.12.01
|
| 164 |
-
'%d/%m/%Y %H:%M:%S', # 01/12/2024 14:30:00
|
| 165 |
-
'%Y-%m-%d %H:%M:%S', # 2024-12-01 14:30:00
|
| 166 |
-
]
|
| 167 |
-
|
| 168 |
-
result_series = pd.Series(index=series.index, dtype='datetime64[ns]')
|
| 169 |
-
|
| 170 |
-
for idx, value in series.items():
|
| 171 |
-
if pd.isna(value) or str(value).strip() in ['', 'nan', 'null', 'none', '-']:
|
| 172 |
-
result_series[idx] = pd.NaT
|
| 173 |
-
continue
|
| 174 |
-
|
| 175 |
-
value_str = str(value).strip()
|
| 176 |
-
converted = False
|
| 177 |
-
|
| 178 |
-
# Tenta conversão automática com detecção inteligente de formato
|
| 179 |
-
try:
|
| 180 |
-
result_series[idx] = smart_date_conversion(value_str)
|
| 181 |
-
converted = True
|
| 182 |
-
except:
|
| 183 |
-
pass
|
| 184 |
-
|
| 185 |
-
# Se não funcionou, tenta formatos específicos
|
| 186 |
-
if not converted:
|
| 187 |
-
for fmt in date_formats:
|
| 188 |
-
try:
|
| 189 |
-
result_series[idx] = pd.to_datetime(value_str, format=fmt, errors='raise')
|
| 190 |
-
converted = True
|
| 191 |
-
break
|
| 192 |
-
except:
|
| 193 |
-
continue
|
| 194 |
-
|
| 195 |
-
# Se ainda não converteu, marca como NaT
|
| 196 |
-
if not converted:
|
| 197 |
-
result_series[idx] = pd.NaT
|
| 198 |
-
logging.warning(f"Não foi possível converter '{value_str}' para data")
|
| 199 |
-
|
| 200 |
-
return result_series
|
| 201 |
-
|
| 202 |
-
class CSVProcessingState(TypedDict):
|
| 203 |
-
"""Estado para processamento de CSV"""
|
| 204 |
-
file_path: str
|
| 205 |
-
success: bool
|
| 206 |
-
message: str
|
| 207 |
-
csv_data_sample: dict
|
| 208 |
-
column_info: dict
|
| 209 |
-
processing_stats: dict
|
| 210 |
-
|
| 211 |
-
async def detect_column_types(df: pd.DataFrame, sample_size: int = 1000) -> Dict[str, Any]:
|
| 212 |
-
"""
|
| 213 |
-
Detecta automaticamente os tipos de colunas de forma genérica e otimizada
|
| 214 |
-
|
| 215 |
-
Args:
|
| 216 |
-
df: DataFrame do pandas
|
| 217 |
-
sample_size: Número de linhas para amostragem (otimização)
|
| 218 |
-
|
| 219 |
-
Returns:
|
| 220 |
-
Dicionário com informações dos tipos detectados
|
| 221 |
-
"""
|
| 222 |
-
column_info = {
|
| 223 |
-
"detected_types": {},
|
| 224 |
-
"sql_types": {},
|
| 225 |
-
"date_columns": [],
|
| 226 |
-
"numeric_columns": [],
|
| 227 |
-
"text_columns": [],
|
| 228 |
-
"processing_rules": {}
|
| 229 |
-
}
|
| 230 |
-
|
| 231 |
-
# Usa amostra para otimizar performance em datasets grandes
|
| 232 |
-
sample_df = df.sample(n=min(sample_size, len(df)), random_state=42) if len(df) > sample_size else df
|
| 233 |
-
logging.info(f"[OPTIMIZATION] Usando amostra de {len(sample_df)} linhas para detecção de tipos")
|
| 234 |
-
|
| 235 |
-
for col in df.columns:
|
| 236 |
-
# Detecta tipo original
|
| 237 |
-
original_type = str(df[col].dtype)
|
| 238 |
-
column_info["detected_types"][col] = original_type
|
| 239 |
-
|
| 240 |
-
# Usa amostra para análise
|
| 241 |
-
sample_col = sample_df[col] if col in sample_df.columns else df[col]
|
| 242 |
-
|
| 243 |
-
# Detecta números já convertidos pelo pandas
|
| 244 |
-
if sample_col.dtype in ['int64', 'Int64', 'float64', 'Float64']:
|
| 245 |
-
if 'int' in str(sample_col.dtype).lower():
|
| 246 |
-
column_info["numeric_columns"].append(col)
|
| 247 |
-
column_info["sql_types"][col] = Integer()
|
| 248 |
-
column_info["processing_rules"][col] = "keep_as_int"
|
| 249 |
-
else:
|
| 250 |
-
column_info["numeric_columns"].append(col)
|
| 251 |
-
column_info["sql_types"][col] = Float()
|
| 252 |
-
column_info["processing_rules"][col] = "keep_as_float"
|
| 253 |
-
continue
|
| 254 |
-
|
| 255 |
-
# Tenta detectar datas de forma mais robusta
|
| 256 |
-
if sample_col.dtype == 'object':
|
| 257 |
-
# Tenta detectar datas com múltiplos formatos
|
| 258 |
-
sample_values = sample_col.dropna().head(20)
|
| 259 |
-
date_success_count = 0
|
| 260 |
-
|
| 261 |
-
# Formatos de data comuns para testar
|
| 262 |
-
date_formats = [
|
| 263 |
-
'%d/%m/%Y', # 01/12/2024
|
| 264 |
-
'%d-%m-%Y', # 01-12-2024
|
| 265 |
-
'%Y-%m-%d', # 2024-12-01
|
| 266 |
-
'%d/%m/%y', # 01/12/24
|
| 267 |
-
'%d-%m-%y', # 01-12-24
|
| 268 |
-
'%Y/%m/%d', # 2024/12/01
|
| 269 |
-
'%d.%m.%Y', # 01.12.2024
|
| 270 |
-
'%Y.%m.%d', # 2024.12.01
|
| 271 |
-
]
|
| 272 |
-
|
| 273 |
-
for val in sample_values:
|
| 274 |
-
val_str = str(val).strip()
|
| 275 |
-
if not val_str or val_str.lower() in ['nan', 'null', 'none', '-']:
|
| 276 |
-
continue
|
| 277 |
-
|
| 278 |
-
# Tenta conversão automática com detecção inteligente
|
| 279 |
-
try:
|
| 280 |
-
smart_date_conversion(val_str)
|
| 281 |
-
date_success_count += 1
|
| 282 |
-
continue
|
| 283 |
-
except:
|
| 284 |
-
pass
|
| 285 |
-
|
| 286 |
-
# Tenta formatos específicos
|
| 287 |
-
for fmt in date_formats:
|
| 288 |
-
try:
|
| 289 |
-
pd.to_datetime(val_str, format=fmt, errors='raise')
|
| 290 |
-
date_success_count += 1
|
| 291 |
-
break
|
| 292 |
-
except:
|
| 293 |
-
continue
|
| 294 |
-
|
| 295 |
-
# Se mais de 70% dos valores são datas válidas, considera como coluna de data
|
| 296 |
-
if len(sample_values) > 0 and date_success_count / len(sample_values) > 0.7:
|
| 297 |
-
column_info["date_columns"].append(col)
|
| 298 |
-
column_info["sql_types"][col] = DateTime()
|
| 299 |
-
column_info["processing_rules"][col] = "parse_dates_advanced"
|
| 300 |
-
continue
|
| 301 |
-
|
| 302 |
-
# Tenta detectar números em colunas de texto (otimizado)
|
| 303 |
-
elif sample_col.dtype == 'object':
|
| 304 |
-
# Análise otimizada de números em texto
|
| 305 |
-
sample_values = sample_col.dropna().head(50) # Aumenta amostra para melhor precisão
|
| 306 |
-
|
| 307 |
-
if len(sample_values) == 0:
|
| 308 |
-
column_info["text_columns"].append(col)
|
| 309 |
-
column_info["sql_types"][col] = String()
|
| 310 |
-
column_info["processing_rules"][col] = "keep_as_text"
|
| 311 |
-
continue
|
| 312 |
-
|
| 313 |
-
# Análise vetorizada para performance
|
| 314 |
-
numeric_analysis = analyze_numeric_column(sample_values)
|
| 315 |
-
|
| 316 |
-
if numeric_analysis["is_numeric"]:
|
| 317 |
-
if numeric_analysis["is_integer"]:
|
| 318 |
-
column_info["numeric_columns"].append(col)
|
| 319 |
-
column_info["sql_types"][col] = Integer()
|
| 320 |
-
column_info["processing_rules"][col] = "convert_text_to_int_safe"
|
| 321 |
-
else:
|
| 322 |
-
column_info["numeric_columns"].append(col)
|
| 323 |
-
column_info["sql_types"][col] = Float()
|
| 324 |
-
column_info["processing_rules"][col] = "convert_text_to_float_safe"
|
| 325 |
-
else:
|
| 326 |
-
# Mantém como texto
|
| 327 |
-
column_info["text_columns"].append(col)
|
| 328 |
-
column_info["sql_types"][col] = String()
|
| 329 |
-
column_info["processing_rules"][col] = "keep_as_text"
|
| 330 |
-
|
| 331 |
-
# Tenta detectar números em colunas de texto
|
| 332 |
-
elif df[col].dtype == 'object':
|
| 333 |
-
# Verifica se pode ser convertido para número
|
| 334 |
-
sample_values = df[col].dropna().head(20)
|
| 335 |
-
numeric_count = 0
|
| 336 |
-
|
| 337 |
-
for val in sample_values:
|
| 338 |
-
try:
|
| 339 |
-
# Remove caracteres comuns e tenta converter
|
| 340 |
-
clean_val = str(val).replace(',', '.').replace('-', '').strip()
|
| 341 |
-
if clean_val:
|
| 342 |
-
float(clean_val)
|
| 343 |
-
numeric_count += 1
|
| 344 |
-
except:
|
| 345 |
-
pass
|
| 346 |
-
|
| 347 |
-
# Se mais de 70% são números, trata como numérico
|
| 348 |
-
if len(sample_values) > 0 and numeric_count / len(sample_values) > 0.7:
|
| 349 |
-
# Verifica se são inteiros ou floats
|
| 350 |
-
has_decimal = any('.' in str(val) or ',' in str(val) for val in sample_values)
|
| 351 |
-
if has_decimal:
|
| 352 |
-
column_info["numeric_columns"].append(col)
|
| 353 |
-
column_info["sql_types"][col] = Float()
|
| 354 |
-
column_info["processing_rules"][col] = "convert_text_to_float"
|
| 355 |
-
else:
|
| 356 |
-
column_info["numeric_columns"].append(col)
|
| 357 |
-
column_info["sql_types"][col] = Integer()
|
| 358 |
-
column_info["processing_rules"][col] = "convert_text_to_int"
|
| 359 |
-
else:
|
| 360 |
-
# Mantém como texto
|
| 361 |
-
column_info["text_columns"].append(col)
|
| 362 |
-
column_info["sql_types"][col] = String()
|
| 363 |
-
column_info["processing_rules"][col] = "keep_as_text"
|
| 364 |
-
else:
|
| 365 |
-
# Outros tipos mantém como texto
|
| 366 |
-
column_info["text_columns"].append(col)
|
| 367 |
-
column_info["sql_types"][col] = String()
|
| 368 |
-
column_info["processing_rules"][col] = "keep_as_text"
|
| 369 |
-
|
| 370 |
-
return column_info
|
| 371 |
-
|
| 372 |
-
async def process_dataframe_generic(df: pd.DataFrame, column_info: Dict[str, Any]) -> pd.DataFrame:
|
| 373 |
-
"""
|
| 374 |
-
Processa DataFrame com OTIMIZAÇÕES EXTREMAS para performance máxima
|
| 375 |
-
|
| 376 |
-
Args:
|
| 377 |
-
df: DataFrame original
|
| 378 |
-
column_info: Informações dos tipos detectados
|
| 379 |
-
|
| 380 |
-
Returns:
|
| 381 |
-
DataFrame processado
|
| 382 |
-
"""
|
| 383 |
-
logging.info(f"[ULTRA_OPTIMIZATION] Iniciando processamento ULTRA-OTIMIZADO de {len(df)} linhas")
|
| 384 |
-
start_time = time.time()
|
| 385 |
-
|
| 386 |
-
# OTIMIZAÇÃO 1: Evita cópia desnecessária - modifica in-place quando possível
|
| 387 |
-
processed_df = df
|
| 388 |
-
|
| 389 |
-
# OTIMIZAÇÃO 2: Agrupa colunas por tipo de processamento
|
| 390 |
-
processing_groups = {
|
| 391 |
-
'dates': [],
|
| 392 |
-
'keep_numeric': [],
|
| 393 |
-
'convert_numeric': [],
|
| 394 |
-
'text': []
|
| 395 |
-
}
|
| 396 |
-
|
| 397 |
-
for col, rule in column_info["processing_rules"].items():
|
| 398 |
-
if col not in processed_df.columns:
|
| 399 |
-
continue
|
| 400 |
-
|
| 401 |
-
if 'date' in rule:
|
| 402 |
-
processing_groups['dates'].append((col, rule))
|
| 403 |
-
elif 'keep_as' in rule:
|
| 404 |
-
processing_groups['keep_numeric'].append((col, rule))
|
| 405 |
-
elif 'convert' in rule:
|
| 406 |
-
processing_groups['convert_numeric'].append((col, rule))
|
| 407 |
-
else:
|
| 408 |
-
processing_groups['text'].append((col, rule))
|
| 409 |
-
|
| 410 |
-
# OTIMIZAÇÃO 3: Processamento paralelo por grupos
|
| 411 |
-
await process_groups_parallel(processed_df, processing_groups)
|
| 412 |
-
|
| 413 |
-
total_time = time.time() - start_time
|
| 414 |
-
logging.info(f"[ULTRA_OPTIMIZATION] Processamento ULTRA-OTIMIZADO concluído em {total_time:.2f}s")
|
| 415 |
-
|
| 416 |
-
return processed_df
|
| 417 |
-
|
| 418 |
-
async def process_groups_parallel(df: pd.DataFrame, groups: Dict[str, List]):
|
| 419 |
-
"""
|
| 420 |
-
Processa grupos de colunas em paralelo para máxima performance
|
| 421 |
-
"""
|
| 422 |
-
tasks = []
|
| 423 |
-
|
| 424 |
-
# Processa cada grupo
|
| 425 |
-
for group_name, columns in groups.items():
|
| 426 |
-
if not columns:
|
| 427 |
-
continue
|
| 428 |
-
|
| 429 |
-
if group_name == 'dates':
|
| 430 |
-
tasks.append(process_date_columns_batch(df, columns))
|
| 431 |
-
elif group_name == 'keep_numeric':
|
| 432 |
-
tasks.append(process_keep_numeric_batch(df, columns))
|
| 433 |
-
elif group_name == 'convert_numeric':
|
| 434 |
-
tasks.append(process_convert_numeric_batch(df, columns))
|
| 435 |
-
# text não precisa processamento
|
| 436 |
-
|
| 437 |
-
# Executa todos os grupos em paralelo
|
| 438 |
-
if tasks:
|
| 439 |
-
import asyncio
|
| 440 |
-
await asyncio.gather(*tasks)
|
| 441 |
-
|
| 442 |
-
async def process_date_columns_batch(df: pd.DataFrame, date_columns: List[tuple]):
|
| 443 |
-
"""Processa colunas de data em lote"""
|
| 444 |
-
for col, rule in date_columns:
|
| 445 |
-
try:
|
| 446 |
-
if rule == "parse_dates_advanced":
|
| 447 |
-
# OTIMIZAÇÃO: Processamento vetorizado de datas
|
| 448 |
-
df[col] = process_dates_vectorized(df[col])
|
| 449 |
-
else:
|
| 450 |
-
df[col] = pd.to_datetime(df[col], dayfirst=True, errors='coerce')
|
| 451 |
-
except Exception as e:
|
| 452 |
-
logging.warning(f"Erro ao processar data {col}: {e}")
|
| 453 |
-
|
| 454 |
-
async def process_keep_numeric_batch(df: pd.DataFrame, numeric_columns: List[tuple]):
|
| 455 |
-
"""Processa colunas numéricas que já estão no tipo correto"""
|
| 456 |
-
for col, rule in numeric_columns:
|
| 457 |
-
try:
|
| 458 |
-
if rule == "keep_as_int" and df[col].dtype != 'Int64':
|
| 459 |
-
df[col] = df[col].astype("Int64")
|
| 460 |
-
elif rule == "keep_as_float" and df[col].dtype != 'float64':
|
| 461 |
-
df[col] = df[col].astype("float64")
|
| 462 |
-
except Exception as e:
|
| 463 |
-
logging.warning(f"Erro ao manter tipo {col}: {e}")
|
| 464 |
-
|
| 465 |
-
async def process_convert_numeric_batch(df: pd.DataFrame, convert_columns: List[tuple]):
|
| 466 |
-
"""Processa conversões numéricas em lote com máxima otimização"""
|
| 467 |
-
for col, rule in convert_columns:
|
| 468 |
-
try:
|
| 469 |
-
if rule == "convert_text_to_int_safe":
|
| 470 |
-
df[col] = convert_to_int_ultra_optimized(df[col])
|
| 471 |
-
elif rule == "convert_text_to_float_safe":
|
| 472 |
-
df[col] = convert_to_float_ultra_optimized(df[col])
|
| 473 |
-
except Exception as e:
|
| 474 |
-
logging.warning(f"Erro ao converter {col}: {e}")
|
| 475 |
-
if rule == "parse_dates":
|
| 476 |
-
processed_df[col] = pd.to_datetime(
|
| 477 |
-
processed_df[col],
|
| 478 |
-
dayfirst=True,
|
| 479 |
-
errors='coerce'
|
| 480 |
-
)
|
| 481 |
-
|
| 482 |
-
elif rule == "parse_dates_advanced":
|
| 483 |
-
# Processamento avançado de datas com múltiplos formatos
|
| 484 |
-
processed_df[col] = await process_dates_advanced(processed_df[col])
|
| 485 |
-
|
| 486 |
-
elif rule == "keep_as_int":
|
| 487 |
-
# Já é inteiro, apenas garante tipo correto
|
| 488 |
-
if processed_df[col].dtype != 'Int64':
|
| 489 |
-
processed_df[col] = processed_df[col].astype("Int64")
|
| 490 |
-
|
| 491 |
-
elif rule == "keep_as_float":
|
| 492 |
-
# Já é float, apenas garante tipo correto
|
| 493 |
-
if processed_df[col].dtype != 'float64':
|
| 494 |
-
processed_df[col] = processed_df[col].astype("float64")
|
| 495 |
-
|
| 496 |
-
elif rule == "convert_text_to_int_safe":
|
| 497 |
-
# Conversão otimizada e segura para inteiros
|
| 498 |
-
processed_df[col] = convert_to_int_optimized(processed_df[col])
|
| 499 |
-
|
| 500 |
-
elif rule == "convert_text_to_float_safe":
|
| 501 |
-
# Conversão otimizada e segura para floats
|
| 502 |
-
processed_df[col] = convert_to_float_optimized(processed_df[col])
|
| 503 |
-
|
| 504 |
-
elif rule == "keep_as_text":
|
| 505 |
-
# Mantém como texto, apenas garante que é string
|
| 506 |
-
processed_df[col] = processed_df[col].astype(str)
|
| 507 |
-
|
| 508 |
-
except Exception as e:
|
| 509 |
-
logging.warning(f"Erro ao processar coluna {col} com regra {rule}: {e}")
|
| 510 |
-
# Em caso de erro, mantém coluna original
|
| 511 |
-
continue
|
| 512 |
-
|
| 513 |
-
col_time = time.time() - col_start_time
|
| 514 |
-
logging.debug(f"[OPTIMIZATION] Coluna {col} processada em {col_time:.2f}s")
|
| 515 |
-
|
| 516 |
-
total_time = time.time() - start_time
|
| 517 |
-
logging.info(f"[OPTIMIZATION] Processamento concluído em {total_time:.2f}s")
|
| 518 |
-
|
| 519 |
-
return processed_df
|
| 520 |
-
|
| 521 |
-
def convert_to_int_optimized(series: pd.Series) -> pd.Series:
|
| 522 |
-
"""
|
| 523 |
-
Conversão otimizada para inteiros
|
| 524 |
-
|
| 525 |
-
Args:
|
| 526 |
-
series: Série para converter
|
| 527 |
-
|
| 528 |
-
Returns:
|
| 529 |
-
Série convertida para Int64
|
| 530 |
-
"""
|
| 531 |
-
try:
|
| 532 |
-
# Operações vetorizadas para performance
|
| 533 |
-
cleaned = series.astype(str).str.strip()
|
| 534 |
-
|
| 535 |
-
# Remove valores inválidos
|
| 536 |
-
cleaned = cleaned.replace(['', 'nan', 'null', 'none', '-', 'NaN', 'NULL'], np.nan)
|
| 537 |
-
|
| 538 |
-
# Substitui vírgulas por pontos
|
| 539 |
-
cleaned = cleaned.str.replace(',', '.', regex=False)
|
| 540 |
-
|
| 541 |
-
# Converte para numérico
|
| 542 |
-
numeric = pd.to_numeric(cleaned, errors='coerce')
|
| 543 |
-
|
| 544 |
-
# Verifica se pode ser convertido para inteiro sem perda
|
| 545 |
-
# Só converte se todos os valores válidos são inteiros
|
| 546 |
-
valid_mask = numeric.notna()
|
| 547 |
-
if valid_mask.any():
|
| 548 |
-
valid_numbers = numeric[valid_mask]
|
| 549 |
-
# Verifica se são inteiros (sem parte decimal significativa)
|
| 550 |
-
is_integer_mask = np.abs(valid_numbers - np.round(valid_numbers)) < 1e-10
|
| 551 |
-
|
| 552 |
-
if is_integer_mask.all():
|
| 553 |
-
# Todos são inteiros, pode converter
|
| 554 |
-
result = numeric.round().astype("Int64")
|
| 555 |
-
else:
|
| 556 |
-
# Tem decimais, mantém como float mas avisa
|
| 557 |
-
logging.warning(f"Coluna contém decimais, mantendo como float")
|
| 558 |
-
result = numeric.astype("Float64")
|
| 559 |
-
else:
|
| 560 |
-
# Nenhum valor válido
|
| 561 |
-
result = pd.Series([pd.NA] * len(series), dtype="Int64")
|
| 562 |
-
|
| 563 |
-
return result
|
| 564 |
-
|
| 565 |
-
except Exception as e:
|
| 566 |
-
logging.error(f"Erro na conversão otimizada para int: {e}")
|
| 567 |
-
return series
|
| 568 |
-
|
| 569 |
-
def convert_to_float_optimized(series: pd.Series) -> pd.Series:
|
| 570 |
-
"""
|
| 571 |
-
Conversão otimizada para floats
|
| 572 |
-
|
| 573 |
-
Args:
|
| 574 |
-
series: Série para converter
|
| 575 |
-
|
| 576 |
-
Returns:
|
| 577 |
-
Série convertida para float64
|
| 578 |
-
"""
|
| 579 |
-
try:
|
| 580 |
-
# Operações vetorizadas para performance
|
| 581 |
-
cleaned = series.astype(str).str.strip()
|
| 582 |
-
|
| 583 |
-
# Remove valores inválidos
|
| 584 |
-
cleaned = cleaned.replace(['', 'nan', 'null', 'none', '-', 'NaN', 'NULL'], np.nan)
|
| 585 |
-
|
| 586 |
-
# Substitui vírgulas por pontos (formato brasileiro)
|
| 587 |
-
cleaned = cleaned.str.replace(',', '.', regex=False)
|
| 588 |
-
|
| 589 |
-
# Converte para numérico
|
| 590 |
-
result = pd.to_numeric(cleaned, errors='coerce')
|
| 591 |
-
|
| 592 |
-
return result
|
| 593 |
-
|
| 594 |
-
except Exception as e:
|
| 595 |
-
logging.error(f"Erro na conversão otimizada para float: {e}")
|
| 596 |
-
return series
|
| 597 |
-
|
| 598 |
-
def convert_to_int_ultra_optimized(series: pd.Series) -> pd.Series:
|
| 599 |
-
"""
|
| 600 |
-
Conversão ULTRA-OTIMIZADA para inteiros usando NumPy puro
|
| 601 |
-
"""
|
| 602 |
-
try:
|
| 603 |
-
# OTIMIZAÇÃO EXTREMA: Usa NumPy diretamente
|
| 604 |
-
values = series.values
|
| 605 |
-
|
| 606 |
-
# Se já é numérico, converte diretamente
|
| 607 |
-
if pd.api.types.is_numeric_dtype(series):
|
| 608 |
-
return pd.Series(values, dtype="Int64")
|
| 609 |
-
|
| 610 |
-
# Para strings, usa operações vetorizadas do NumPy
|
| 611 |
-
str_values = np.asarray(series.astype(str))
|
| 612 |
-
|
| 613 |
-
# Máscara para valores válidos
|
| 614 |
-
valid_mask = ~np.isin(str_values, ['', 'nan', 'null', 'none', '-', 'NaN', 'NULL'])
|
| 615 |
-
|
| 616 |
-
# Inicializa resultado
|
| 617 |
-
result = np.full(len(series), pd.NA, dtype=object)
|
| 618 |
-
|
| 619 |
-
if valid_mask.any():
|
| 620 |
-
valid_values = str_values[valid_mask]
|
| 621 |
-
|
| 622 |
-
# Remove vírgulas e converte
|
| 623 |
-
cleaned = np.char.replace(valid_values, ',', '.')
|
| 624 |
-
|
| 625 |
-
# Conversão vetorizada
|
| 626 |
-
try:
|
| 627 |
-
numeric_values = pd.to_numeric(cleaned, errors='coerce')
|
| 628 |
-
# Só converte se são realmente inteiros
|
| 629 |
-
int_mask = np.abs(numeric_values - np.round(numeric_values)) < 1e-10
|
| 630 |
-
int_values = np.round(numeric_values[int_mask]).astype('Int64')
|
| 631 |
-
|
| 632 |
-
# Atribui valores convertidos
|
| 633 |
-
valid_indices = np.where(valid_mask)[0]
|
| 634 |
-
int_indices = valid_indices[int_mask]
|
| 635 |
-
result[int_indices] = int_values
|
| 636 |
-
|
| 637 |
-
except Exception:
|
| 638 |
-
pass
|
| 639 |
-
|
| 640 |
-
return pd.Series(result, dtype="Int64")
|
| 641 |
-
|
| 642 |
-
except Exception as e:
|
| 643 |
-
logging.error(f"Erro na conversão ultra-otimizada para int: {e}")
|
| 644 |
-
return series
|
| 645 |
-
|
| 646 |
-
def convert_to_float_ultra_optimized(series: pd.Series) -> pd.Series:
|
| 647 |
-
"""
|
| 648 |
-
Conversão ULTRA-OTIMIZADA para floats usando NumPy puro
|
| 649 |
-
"""
|
| 650 |
-
try:
|
| 651 |
-
# OTIMIZAÇÃO EXTREMA: Usa NumPy diretamente
|
| 652 |
-
values = series.values
|
| 653 |
-
|
| 654 |
-
# Se já é numérico, retorna diretamente
|
| 655 |
-
if pd.api.types.is_numeric_dtype(series):
|
| 656 |
-
return series.astype('float64')
|
| 657 |
-
|
| 658 |
-
# Para strings, usa operações vetorizadas do NumPy
|
| 659 |
-
str_values = np.asarray(series.astype(str))
|
| 660 |
-
|
| 661 |
-
# Máscara para valores válidos
|
| 662 |
-
valid_mask = ~np.isin(str_values, ['', 'nan', 'null', 'none', '-', 'NaN', 'NULL'])
|
| 663 |
-
|
| 664 |
-
# Inicializa resultado
|
| 665 |
-
result = np.full(len(series), np.nan, dtype='float64')
|
| 666 |
-
|
| 667 |
-
if valid_mask.any():
|
| 668 |
-
valid_values = str_values[valid_mask]
|
| 669 |
-
|
| 670 |
-
# Remove vírgulas (formato brasileiro)
|
| 671 |
-
cleaned = np.char.replace(valid_values, ',', '.')
|
| 672 |
-
|
| 673 |
-
# Conversão vetorizada ultra-rápida
|
| 674 |
-
numeric_values = pd.to_numeric(cleaned, errors='coerce')
|
| 675 |
-
result[valid_mask] = numeric_values
|
| 676 |
-
|
| 677 |
-
return pd.Series(result, dtype='float64')
|
| 678 |
-
|
| 679 |
-
except Exception as e:
|
| 680 |
-
logging.error(f"Erro na conversão ultra-otimizada para float: {e}")
|
| 681 |
-
return series
|
| 682 |
-
|
| 683 |
-
def process_dates_vectorized(series: pd.Series) -> pd.Series:
|
| 684 |
-
"""
|
| 685 |
-
Processamento vetorizado ULTRA-OTIMIZADO de datas
|
| 686 |
-
"""
|
| 687 |
-
try:
|
| 688 |
-
# OTIMIZAÇÃO: Tenta conversão direta primeiro
|
| 689 |
-
try:
|
| 690 |
-
return pd.to_datetime(series, dayfirst=True, errors='coerce')
|
| 691 |
-
except:
|
| 692 |
-
pass
|
| 693 |
-
|
| 694 |
-
# Se falhou, usa abordagem mais robusta mas ainda otimizada
|
| 695 |
-
str_values = series.astype(str)
|
| 696 |
-
|
| 697 |
-
# Detecta formato mais comum na amostra
|
| 698 |
-
sample = str_values.dropna().head(100)
|
| 699 |
-
if len(sample) > 0:
|
| 700 |
-
first_val = sample.iloc[0]
|
| 701 |
-
|
| 702 |
-
# Detecta formato baseado no primeiro valor
|
| 703 |
-
if len(first_val) >= 10 and first_val[4] in ['-', '/']:
|
| 704 |
-
# Formato ISO
|
| 705 |
-
return pd.to_datetime(series, errors='coerce')
|
| 706 |
-
else:
|
| 707 |
-
# Formato brasileiro
|
| 708 |
-
return pd.to_datetime(series, dayfirst=True, errors='coerce')
|
| 709 |
-
|
| 710 |
-
return pd.to_datetime(series, errors='coerce')
|
| 711 |
-
|
| 712 |
-
except Exception as e:
|
| 713 |
-
logging.error(f"Erro no processamento vetorizado de datas: {e}")
|
| 714 |
-
return series
|
| 715 |
-
|
| 716 |
-
async def csv_processing_node(state: CSVProcessingState) -> CSVProcessingState:
|
| 717 |
-
"""
|
| 718 |
-
Nó principal para processamento de CSV
|
| 719 |
-
|
| 720 |
-
Args:
|
| 721 |
-
state: Estado do processamento CSV
|
| 722 |
-
|
| 723 |
-
Returns:
|
| 724 |
-
Estado atualizado
|
| 725 |
-
"""
|
| 726 |
-
try:
|
| 727 |
-
file_path = state["file_path"]
|
| 728 |
-
|
| 729 |
-
# Copia arquivo para diretório de upload
|
| 730 |
-
shutil.copy(file_path, UPLOADED_CSV_PATH)
|
| 731 |
-
logging.info(f"[CSV_PROCESSING] Arquivo copiado para: {UPLOADED_CSV_PATH}")
|
| 732 |
-
|
| 733 |
-
# OTIMIZAÇÃO EXTREMA: Leitura de CSV ultra-otimizada
|
| 734 |
-
separators = [';', ',', '\t', '|']
|
| 735 |
-
df = None
|
| 736 |
-
used_separator = None
|
| 737 |
-
|
| 738 |
-
# Detecta separador com amostra mínima
|
| 739 |
-
for sep in separators:
|
| 740 |
-
try:
|
| 741 |
-
test_df = pd.read_csv(file_path, sep=sep, nrows=3, engine='c') # Engine C é mais rápido
|
| 742 |
-
if len(test_df.columns) > 1:
|
| 743 |
-
# OTIMIZAÇÃO: Lê com configurações de performance máxima
|
| 744 |
-
df = pd.read_csv(
|
| 745 |
-
file_path,
|
| 746 |
-
sep=sep,
|
| 747 |
-
encoding='utf-8',
|
| 748 |
-
on_bad_lines="skip",
|
| 749 |
-
engine='c', # Engine C para máxima performance
|
| 750 |
-
low_memory=False, # Evita warnings de tipos mistos
|
| 751 |
-
dtype=str # Lê tudo como string primeiro (mais rápido)
|
| 752 |
-
)
|
| 753 |
-
used_separator = sep
|
| 754 |
-
break
|
| 755 |
-
except:
|
| 756 |
-
continue
|
| 757 |
-
|
| 758 |
-
if df is None:
|
| 759 |
-
raise ValueError("Não foi possível detectar o formato do CSV")
|
| 760 |
-
|
| 761 |
-
logging.info(f"[CSV_PROCESSING] CSV lido com separador '{used_separator}', {len(df)} linhas, {len(df.columns)} colunas")
|
| 762 |
-
|
| 763 |
-
# Detecta tipos de colunas automaticamente
|
| 764 |
-
column_info = await detect_column_types(df)
|
| 765 |
-
|
| 766 |
-
# Processa DataFrame
|
| 767 |
-
processed_df = await process_dataframe_generic(df, column_info)
|
| 768 |
-
|
| 769 |
-
# Estatísticas do processamento
|
| 770 |
-
processing_stats = {
|
| 771 |
-
"original_rows": len(df),
|
| 772 |
-
"processed_rows": len(processed_df),
|
| 773 |
-
"original_columns": len(df.columns),
|
| 774 |
-
"processed_columns": len(processed_df.columns),
|
| 775 |
-
"separator_used": used_separator,
|
| 776 |
-
"date_columns_detected": len(column_info["date_columns"]),
|
| 777 |
-
"numeric_columns_detected": len(column_info["numeric_columns"]),
|
| 778 |
-
"text_columns_detected": len(column_info["text_columns"])
|
| 779 |
-
}
|
| 780 |
-
|
| 781 |
-
# Amostra dos dados para o estado
|
| 782 |
-
csv_data_sample = {
|
| 783 |
-
"head": processed_df.head(5).to_dict(),
|
| 784 |
-
"dtypes": processed_df.dtypes.astype(str).to_dict(),
|
| 785 |
-
"columns": list(processed_df.columns)
|
| 786 |
-
}
|
| 787 |
-
|
| 788 |
-
# Armazena DataFrame processado no gerenciador de objetos
|
| 789 |
-
obj_manager = get_object_manager()
|
| 790 |
-
df_id = obj_manager.store_object(processed_df, "processed_dataframe")
|
| 791 |
-
|
| 792 |
-
# Atualiza estado
|
| 793 |
-
state.update({
|
| 794 |
-
"success": True,
|
| 795 |
-
"message": f"✅ CSV processado com sucesso! {processing_stats['processed_rows']} linhas, {processing_stats['processed_columns']} colunas",
|
| 796 |
-
"csv_data_sample": csv_data_sample,
|
| 797 |
-
"column_info": column_info,
|
| 798 |
-
"processing_stats": processing_stats,
|
| 799 |
-
"dataframe_id": df_id
|
| 800 |
-
})
|
| 801 |
-
|
| 802 |
-
logging.info(f"[CSV_PROCESSING] Processamento concluído: {processing_stats}")
|
| 803 |
-
|
| 804 |
-
except Exception as e:
|
| 805 |
-
error_msg = f"❌ Erro ao processar CSV: {e}"
|
| 806 |
-
logging.error(f"[CSV_PROCESSING] {error_msg}")
|
| 807 |
-
state.update({
|
| 808 |
-
"success": False,
|
| 809 |
-
"message": error_msg,
|
| 810 |
-
"csv_data_sample": {},
|
| 811 |
-
"column_info": {},
|
| 812 |
-
"processing_stats": {}
|
| 813 |
-
})
|
| 814 |
-
|
| 815 |
-
return state
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
nodes/custom_nodes.py
DELETED
|
@@ -1,297 +0,0 @@
|
|
| 1 |
-
"""
|
| 2 |
-
Nós personalizados para funcionalidades específicas
|
| 3 |
-
"""
|
| 4 |
-
import os
|
| 5 |
-
import shutil
|
| 6 |
-
import logging
|
| 7 |
-
from typing import Dict, Any, TypedDict
|
| 8 |
-
|
| 9 |
-
from utils.database import create_sql_database
|
| 10 |
-
from utils.config import UPLOADED_CSV_PATH, SQL_DB_PATH, DEFAULT_CSV_PATH
|
| 11 |
-
from agents.sql_agent import SQLAgentManager
|
| 12 |
-
from nodes.csv_processing_node import csv_processing_node
|
| 13 |
-
from nodes.database_node import create_database_from_dataframe_node, load_existing_database_node
|
| 14 |
-
|
| 15 |
-
class FileUploadState(TypedDict):
|
| 16 |
-
"""Estado para upload de arquivos"""
|
| 17 |
-
file_path: str
|
| 18 |
-
success: bool
|
| 19 |
-
message: str
|
| 20 |
-
engine: Any
|
| 21 |
-
sql_agent: SQLAgentManager
|
| 22 |
-
cache_manager: Any
|
| 23 |
-
|
| 24 |
-
class ResetState(TypedDict):
|
| 25 |
-
"""Estado para reset do sistema"""
|
| 26 |
-
success: bool
|
| 27 |
-
message: str
|
| 28 |
-
engine: Any
|
| 29 |
-
sql_agent: SQLAgentManager
|
| 30 |
-
cache_manager: Any
|
| 31 |
-
|
| 32 |
-
async def handle_csv_upload_node(state: FileUploadState) -> FileUploadState:
|
| 33 |
-
"""
|
| 34 |
-
Nó para processar upload de CSV
|
| 35 |
-
|
| 36 |
-
Args:
|
| 37 |
-
state: Estado do upload
|
| 38 |
-
|
| 39 |
-
Returns:
|
| 40 |
-
Estado atualizado
|
| 41 |
-
"""
|
| 42 |
-
try:
|
| 43 |
-
file_path = state["file_path"]
|
| 44 |
-
|
| 45 |
-
# Etapa 1: Processa CSV usando nova arquitetura
|
| 46 |
-
csv_state = {
|
| 47 |
-
"file_path": file_path,
|
| 48 |
-
"success": False,
|
| 49 |
-
"message": "",
|
| 50 |
-
"csv_data_sample": {},
|
| 51 |
-
"column_info": {},
|
| 52 |
-
"processing_stats": {}
|
| 53 |
-
}
|
| 54 |
-
|
| 55 |
-
csv_result = await csv_processing_node(csv_state)
|
| 56 |
-
if not csv_result["success"]:
|
| 57 |
-
raise Exception(csv_result["message"])
|
| 58 |
-
|
| 59 |
-
# Etapa 2: Cria banco de dados
|
| 60 |
-
db_result = await create_database_from_dataframe_node(csv_result)
|
| 61 |
-
if not db_result["success"]:
|
| 62 |
-
raise Exception(db_result["message"])
|
| 63 |
-
|
| 64 |
-
# Recupera objetos criados
|
| 65 |
-
from utils.object_manager import get_object_manager
|
| 66 |
-
obj_manager = get_object_manager()
|
| 67 |
-
|
| 68 |
-
engine = obj_manager.get_engine(db_result["engine_id"])
|
| 69 |
-
db = obj_manager.get_object(db_result["db_id"])
|
| 70 |
-
|
| 71 |
-
logging.info("[UPLOAD] Novo banco carregado e DB atualizado usando nova arquitetura.")
|
| 72 |
-
|
| 73 |
-
# Recria agente SQL
|
| 74 |
-
sql_agent = SQLAgentManager(db)
|
| 75 |
-
|
| 76 |
-
# Limpa cache
|
| 77 |
-
state["cache_manager"].clear_cache()
|
| 78 |
-
|
| 79 |
-
# Atualiza estado
|
| 80 |
-
state["engine"] = engine
|
| 81 |
-
state["sql_agent"] = sql_agent
|
| 82 |
-
state["success"] = True
|
| 83 |
-
state["message"] = "✅ CSV carregado com sucesso!"
|
| 84 |
-
|
| 85 |
-
logging.info("[UPLOAD] Novo banco carregado e agente recriado. Cache limpo.")
|
| 86 |
-
|
| 87 |
-
except Exception as e:
|
| 88 |
-
error_msg = f"❌ Erro ao processar CSV: {e}"
|
| 89 |
-
logging.error(f"[ERRO] Falha ao processar novo CSV: {e}")
|
| 90 |
-
state["success"] = False
|
| 91 |
-
state["message"] = error_msg
|
| 92 |
-
|
| 93 |
-
return state
|
| 94 |
-
|
| 95 |
-
async def reset_system_node(state: Dict[str, Any]) -> Dict[str, Any]:
|
| 96 |
-
"""
|
| 97 |
-
Nó para resetar o sistema ao estado inicial
|
| 98 |
-
|
| 99 |
-
Args:
|
| 100 |
-
state: Estado do reset
|
| 101 |
-
|
| 102 |
-
Returns:
|
| 103 |
-
Estado atualizado
|
| 104 |
-
"""
|
| 105 |
-
try:
|
| 106 |
-
from utils.object_manager import get_object_manager
|
| 107 |
-
from agents.sql_agent import SQLAgentManager
|
| 108 |
-
|
| 109 |
-
obj_manager = get_object_manager()
|
| 110 |
-
|
| 111 |
-
# Remove CSV personalizado se existir
|
| 112 |
-
if os.path.exists(UPLOADED_CSV_PATH):
|
| 113 |
-
os.remove(UPLOADED_CSV_PATH)
|
| 114 |
-
logging.info("[RESET] CSV personalizado removido.")
|
| 115 |
-
|
| 116 |
-
# Recria banco com CSV padrão usando nova arquitetura
|
| 117 |
-
csv_state = {
|
| 118 |
-
"file_path": DEFAULT_CSV_PATH,
|
| 119 |
-
"success": False,
|
| 120 |
-
"message": "",
|
| 121 |
-
"csv_data_sample": {},
|
| 122 |
-
"column_info": {},
|
| 123 |
-
"processing_stats": {}
|
| 124 |
-
}
|
| 125 |
-
|
| 126 |
-
csv_result = await csv_processing_node(csv_state)
|
| 127 |
-
if not csv_result["success"]:
|
| 128 |
-
raise Exception(csv_result["message"])
|
| 129 |
-
|
| 130 |
-
# Cria banco de dados
|
| 131 |
-
db_result = await create_database_from_dataframe_node(csv_result)
|
| 132 |
-
if not db_result["success"]:
|
| 133 |
-
raise Exception(db_result["message"])
|
| 134 |
-
|
| 135 |
-
# Recupera objetos criados
|
| 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)
|
| 144 |
-
agent_id = obj_manager.store_sql_agent(sql_agent)
|
| 145 |
-
|
| 146 |
-
# Limpa cache se disponível
|
| 147 |
-
cache_id = state.get("cache_id")
|
| 148 |
-
if cache_id:
|
| 149 |
-
cache_manager = obj_manager.get_cache_manager(cache_id)
|
| 150 |
-
if cache_manager:
|
| 151 |
-
cache_manager.clear_cache()
|
| 152 |
-
|
| 153 |
-
# Atualiza estado
|
| 154 |
-
state.update({
|
| 155 |
-
"engine_id": engine_id,
|
| 156 |
-
"agent_id": agent_id,
|
| 157 |
-
"success": True,
|
| 158 |
-
"message": "🔄 Sistema resetado para o estado inicial."
|
| 159 |
-
})
|
| 160 |
-
|
| 161 |
-
logging.info("[RESET] Sistema resetado com sucesso.")
|
| 162 |
-
|
| 163 |
-
except Exception as e:
|
| 164 |
-
error_msg = f"❌ Erro ao resetar: {e}"
|
| 165 |
-
logging.error(f"[ERRO] Falha ao resetar sistema: {e}")
|
| 166 |
-
state.update({
|
| 167 |
-
"success": False,
|
| 168 |
-
"message": error_msg
|
| 169 |
-
})
|
| 170 |
-
|
| 171 |
-
return state
|
| 172 |
-
|
| 173 |
-
async def validate_system_node(state: Dict[str, Any]) -> Dict[str, Any]:
|
| 174 |
-
"""
|
| 175 |
-
Nó para validar o estado do sistema
|
| 176 |
-
|
| 177 |
-
Args:
|
| 178 |
-
state: Estado atual do sistema
|
| 179 |
-
|
| 180 |
-
Returns:
|
| 181 |
-
Estado com informações de validação
|
| 182 |
-
"""
|
| 183 |
-
validation_results = {
|
| 184 |
-
"database_valid": False,
|
| 185 |
-
"agent_valid": False,
|
| 186 |
-
"cache_valid": False,
|
| 187 |
-
"overall_valid": False
|
| 188 |
-
}
|
| 189 |
-
|
| 190 |
-
try:
|
| 191 |
-
# Valida banco de dados
|
| 192 |
-
if state.get("engine"):
|
| 193 |
-
from utils.database import validate_database
|
| 194 |
-
validation_results["database_valid"] = validate_database(state["engine"])
|
| 195 |
-
|
| 196 |
-
# Valida agente SQL
|
| 197 |
-
if state.get("sql_agent"):
|
| 198 |
-
validation_results["agent_valid"] = state["sql_agent"].validate_agent()
|
| 199 |
-
|
| 200 |
-
# Valida cache
|
| 201 |
-
if state.get("cache_manager"):
|
| 202 |
-
validation_results["cache_valid"] = True # Cache sempre válido se existe
|
| 203 |
-
|
| 204 |
-
# Validação geral
|
| 205 |
-
validation_results["overall_valid"] = all([
|
| 206 |
-
validation_results["database_valid"],
|
| 207 |
-
validation_results["agent_valid"],
|
| 208 |
-
validation_results["cache_valid"]
|
| 209 |
-
])
|
| 210 |
-
|
| 211 |
-
state["validation"] = validation_results
|
| 212 |
-
logging.info(f"[VALIDATION] Sistema válido: {validation_results['overall_valid']}")
|
| 213 |
-
|
| 214 |
-
except Exception as e:
|
| 215 |
-
logging.error(f"[VALIDATION] Erro na validação: {e}")
|
| 216 |
-
state["validation"] = validation_results
|
| 217 |
-
|
| 218 |
-
return state
|
| 219 |
-
|
| 220 |
-
async def get_system_info_node(state: Dict[str, Any]) -> Dict[str, Any]:
|
| 221 |
-
"""
|
| 222 |
-
Nó para obter informações do sistema
|
| 223 |
-
|
| 224 |
-
Args:
|
| 225 |
-
state: Estado atual do sistema
|
| 226 |
-
|
| 227 |
-
Returns:
|
| 228 |
-
Estado com informações do sistema
|
| 229 |
-
"""
|
| 230 |
-
system_info = {
|
| 231 |
-
"csv_active": None,
|
| 232 |
-
"database_path": SQL_DB_PATH,
|
| 233 |
-
"agent_info": None,
|
| 234 |
-
"cache_stats": None
|
| 235 |
-
}
|
| 236 |
-
|
| 237 |
-
try:
|
| 238 |
-
# Informações do CSV ativo
|
| 239 |
-
from utils.config import get_active_csv_path
|
| 240 |
-
system_info["csv_active"] = get_active_csv_path()
|
| 241 |
-
|
| 242 |
-
# Informações do agente
|
| 243 |
-
if state.get("sql_agent"):
|
| 244 |
-
system_info["agent_info"] = state["sql_agent"].get_agent_info()
|
| 245 |
-
|
| 246 |
-
# Estatísticas do cache
|
| 247 |
-
if state.get("cache_manager"):
|
| 248 |
-
cache_manager = state["cache_manager"]
|
| 249 |
-
system_info["cache_stats"] = {
|
| 250 |
-
"cached_queries": len(cache_manager.query_cache),
|
| 251 |
-
"history_entries": len(cache_manager.history_log),
|
| 252 |
-
"recent_history_size": len(cache_manager.recent_history)
|
| 253 |
-
}
|
| 254 |
-
|
| 255 |
-
state["system_info"] = system_info
|
| 256 |
-
logging.info("[SYSTEM_INFO] Informações do sistema coletadas")
|
| 257 |
-
|
| 258 |
-
except Exception as e:
|
| 259 |
-
logging.error(f"[SYSTEM_INFO] Erro ao coletar informações: {e}")
|
| 260 |
-
state["system_info"] = system_info
|
| 261 |
-
|
| 262 |
-
return state
|
| 263 |
-
|
| 264 |
-
class CustomNodeManager:
|
| 265 |
-
"""
|
| 266 |
-
Gerenciador dos nós personalizados
|
| 267 |
-
"""
|
| 268 |
-
|
| 269 |
-
def __init__(self):
|
| 270 |
-
self.node_functions = {
|
| 271 |
-
"csv_upload": handle_csv_upload_node,
|
| 272 |
-
"system_reset": reset_system_node,
|
| 273 |
-
"system_validation": validate_system_node,
|
| 274 |
-
"system_info": get_system_info_node
|
| 275 |
-
}
|
| 276 |
-
|
| 277 |
-
def get_node_function(self, node_name: str):
|
| 278 |
-
"""Retorna função do nó pelo nome"""
|
| 279 |
-
return self.node_functions.get(node_name)
|
| 280 |
-
|
| 281 |
-
async def execute_node(self, node_name: str, state: Dict[str, Any]) -> Dict[str, Any]:
|
| 282 |
-
"""
|
| 283 |
-
Executa um nó específico
|
| 284 |
-
|
| 285 |
-
Args:
|
| 286 |
-
node_name: Nome do nó
|
| 287 |
-
state: Estado atual
|
| 288 |
-
|
| 289 |
-
Returns:
|
| 290 |
-
Estado atualizado
|
| 291 |
-
"""
|
| 292 |
-
node_function = self.get_node_function(node_name)
|
| 293 |
-
if node_function:
|
| 294 |
-
return await node_function(state)
|
| 295 |
-
else:
|
| 296 |
-
logging.error(f"Nó não encontrado: {node_name}")
|
| 297 |
-
return state
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
nodes/database_node.py
DELETED
|
@@ -1,226 +0,0 @@
|
|
| 1 |
-
"""
|
| 2 |
-
Nó para operações de banco de dados
|
| 3 |
-
"""
|
| 4 |
-
import os
|
| 5 |
-
import logging
|
| 6 |
-
import pandas as pd
|
| 7 |
-
from typing import Dict, Any, TypedDict, Optional
|
| 8 |
-
from sqlalchemy import create_engine
|
| 9 |
-
|
| 10 |
-
from utils.config import SQL_DB_PATH
|
| 11 |
-
from utils.database import create_sql_database, validate_database
|
| 12 |
-
from utils.object_manager import get_object_manager
|
| 13 |
-
|
| 14 |
-
class DatabaseState(TypedDict):
|
| 15 |
-
"""Estado para operações de banco de dados"""
|
| 16 |
-
success: bool
|
| 17 |
-
message: str
|
| 18 |
-
database_info: dict
|
| 19 |
-
engine_id: str
|
| 20 |
-
db_id: str
|
| 21 |
-
|
| 22 |
-
async def create_database_from_dataframe_node(state: Dict[str, Any]) -> Dict[str, Any]:
|
| 23 |
-
"""
|
| 24 |
-
Nó para criar banco de dados a partir de DataFrame processado
|
| 25 |
-
|
| 26 |
-
Args:
|
| 27 |
-
state: Estado contendo informações do DataFrame processado
|
| 28 |
-
|
| 29 |
-
Returns:
|
| 30 |
-
Estado atualizado com informações do banco
|
| 31 |
-
"""
|
| 32 |
-
try:
|
| 33 |
-
obj_manager = get_object_manager()
|
| 34 |
-
|
| 35 |
-
# Recupera DataFrame processado
|
| 36 |
-
df_id = state.get("dataframe_id")
|
| 37 |
-
if not df_id:
|
| 38 |
-
raise ValueError("ID do DataFrame não encontrado no estado")
|
| 39 |
-
|
| 40 |
-
processed_df = obj_manager.get_object(df_id)
|
| 41 |
-
if processed_df is None:
|
| 42 |
-
raise ValueError("DataFrame processado não encontrado")
|
| 43 |
-
|
| 44 |
-
# Recupera informações das colunas
|
| 45 |
-
column_info = state.get("column_info", {})
|
| 46 |
-
sql_types = column_info.get("sql_types", {})
|
| 47 |
-
|
| 48 |
-
# Cria engine do banco
|
| 49 |
-
engine = create_engine(f"sqlite:///{SQL_DB_PATH}")
|
| 50 |
-
|
| 51 |
-
# Salva DataFrame no banco
|
| 52 |
-
processed_df.to_sql(
|
| 53 |
-
"tabela",
|
| 54 |
-
engine,
|
| 55 |
-
index=False,
|
| 56 |
-
if_exists="replace",
|
| 57 |
-
dtype=sql_types
|
| 58 |
-
)
|
| 59 |
-
|
| 60 |
-
logging.info(f"[DATABASE] Banco criado com {len(processed_df)} registros")
|
| 61 |
-
|
| 62 |
-
# Cria objeto SQLDatabase do LangChain
|
| 63 |
-
db = create_sql_database(engine)
|
| 64 |
-
|
| 65 |
-
# Valida banco
|
| 66 |
-
is_valid = validate_database(engine)
|
| 67 |
-
|
| 68 |
-
# Armazena objetos no gerenciador
|
| 69 |
-
engine_id = obj_manager.store_engine(engine)
|
| 70 |
-
db_id = obj_manager.store_database(db)
|
| 71 |
-
|
| 72 |
-
# Informações do banco
|
| 73 |
-
database_info = {
|
| 74 |
-
"path": SQL_DB_PATH,
|
| 75 |
-
"table_name": "tabela",
|
| 76 |
-
"total_records": len(processed_df),
|
| 77 |
-
"columns": list(processed_df.columns),
|
| 78 |
-
"column_types": {col: str(dtype) for col, dtype in processed_df.dtypes.items()},
|
| 79 |
-
"is_valid": is_valid,
|
| 80 |
-
"sql_types_used": {col: str(sql_type) for col, sql_type in sql_types.items()}
|
| 81 |
-
}
|
| 82 |
-
|
| 83 |
-
# Atualiza estado
|
| 84 |
-
state.update({
|
| 85 |
-
"success": True,
|
| 86 |
-
"message": f"✅ Banco de dados criado com sucesso! {len(processed_df)} registros salvos",
|
| 87 |
-
"database_info": database_info,
|
| 88 |
-
"engine_id": engine_id,
|
| 89 |
-
"db_id": db_id
|
| 90 |
-
})
|
| 91 |
-
|
| 92 |
-
logging.info(f"[DATABASE] Banco criado e validado: {database_info}")
|
| 93 |
-
|
| 94 |
-
except Exception as e:
|
| 95 |
-
error_msg = f"❌ Erro ao criar banco de dados: {e}"
|
| 96 |
-
logging.error(f"[DATABASE] {error_msg}")
|
| 97 |
-
state.update({
|
| 98 |
-
"success": False,
|
| 99 |
-
"message": error_msg,
|
| 100 |
-
"database_info": {},
|
| 101 |
-
"engine_id": "",
|
| 102 |
-
"db_id": ""
|
| 103 |
-
})
|
| 104 |
-
|
| 105 |
-
return state
|
| 106 |
-
|
| 107 |
-
async def load_existing_database_node(state: Dict[str, Any]) -> Dict[str, Any]:
|
| 108 |
-
"""
|
| 109 |
-
Nó para carregar banco de dados existente
|
| 110 |
-
|
| 111 |
-
Args:
|
| 112 |
-
state: Estado atual
|
| 113 |
-
|
| 114 |
-
Returns:
|
| 115 |
-
Estado atualizado com informações do banco existente
|
| 116 |
-
"""
|
| 117 |
-
try:
|
| 118 |
-
if not os.path.exists(SQL_DB_PATH):
|
| 119 |
-
raise ValueError("Banco de dados não encontrado")
|
| 120 |
-
|
| 121 |
-
# Cria engine
|
| 122 |
-
engine = create_engine(f"sqlite:///{SQL_DB_PATH}")
|
| 123 |
-
|
| 124 |
-
# Cria objeto SQLDatabase
|
| 125 |
-
db = create_sql_database(engine)
|
| 126 |
-
|
| 127 |
-
# Valida banco
|
| 128 |
-
is_valid = validate_database(engine)
|
| 129 |
-
|
| 130 |
-
# Obtém informações do banco
|
| 131 |
-
try:
|
| 132 |
-
sample_df = pd.read_sql_query("SELECT * FROM tabela LIMIT 5", engine)
|
| 133 |
-
total_records_df = pd.read_sql_query("SELECT COUNT(*) as count FROM tabela", engine)
|
| 134 |
-
total_records = total_records_df.iloc[0]['count']
|
| 135 |
-
|
| 136 |
-
database_info = {
|
| 137 |
-
"path": SQL_DB_PATH,
|
| 138 |
-
"table_name": "tabela",
|
| 139 |
-
"total_records": total_records,
|
| 140 |
-
"columns": list(sample_df.columns),
|
| 141 |
-
"column_types": {col: str(dtype) for col, dtype in sample_df.dtypes.items()},
|
| 142 |
-
"is_valid": is_valid,
|
| 143 |
-
"sample_data": sample_df.head(3).to_dict()
|
| 144 |
-
}
|
| 145 |
-
except Exception as e:
|
| 146 |
-
logging.warning(f"Erro ao obter informações detalhadas do banco: {e}")
|
| 147 |
-
database_info = {
|
| 148 |
-
"path": SQL_DB_PATH,
|
| 149 |
-
"table_name": "tabela",
|
| 150 |
-
"is_valid": is_valid,
|
| 151 |
-
"error": str(e)
|
| 152 |
-
}
|
| 153 |
-
|
| 154 |
-
# Armazena objetos no gerenciador
|
| 155 |
-
obj_manager = get_object_manager()
|
| 156 |
-
engine_id = obj_manager.store_engine(engine)
|
| 157 |
-
db_id = obj_manager.store_database(db)
|
| 158 |
-
|
| 159 |
-
# Atualiza estado
|
| 160 |
-
state.update({
|
| 161 |
-
"success": True,
|
| 162 |
-
"message": "✅ Banco de dados existente carregado com sucesso",
|
| 163 |
-
"database_info": database_info,
|
| 164 |
-
"engine_id": engine_id,
|
| 165 |
-
"db_id": db_id
|
| 166 |
-
})
|
| 167 |
-
|
| 168 |
-
logging.info(f"[DATABASE] Banco existente carregado: {database_info}")
|
| 169 |
-
|
| 170 |
-
except Exception as e:
|
| 171 |
-
error_msg = f"❌ Erro ao carregar banco existente: {e}"
|
| 172 |
-
logging.error(f"[DATABASE] {error_msg}")
|
| 173 |
-
state.update({
|
| 174 |
-
"success": False,
|
| 175 |
-
"message": error_msg,
|
| 176 |
-
"database_info": {},
|
| 177 |
-
"engine_id": "",
|
| 178 |
-
"db_id": ""
|
| 179 |
-
})
|
| 180 |
-
|
| 181 |
-
return state
|
| 182 |
-
|
| 183 |
-
async def get_database_sample_node(state: Dict[str, Any]) -> Dict[str, Any]:
|
| 184 |
-
"""
|
| 185 |
-
Nó para obter amostra dos dados do banco
|
| 186 |
-
|
| 187 |
-
Args:
|
| 188 |
-
state: Estado contendo ID da engine
|
| 189 |
-
|
| 190 |
-
Returns:
|
| 191 |
-
Estado atualizado com amostra dos dados
|
| 192 |
-
"""
|
| 193 |
-
try:
|
| 194 |
-
obj_manager = get_object_manager()
|
| 195 |
-
|
| 196 |
-
# Recupera engine
|
| 197 |
-
engine_id = state.get("engine_id")
|
| 198 |
-
if not engine_id:
|
| 199 |
-
raise ValueError("ID da engine não encontrado")
|
| 200 |
-
|
| 201 |
-
engine = obj_manager.get_engine(engine_id)
|
| 202 |
-
if not engine:
|
| 203 |
-
raise ValueError("Engine não encontrada")
|
| 204 |
-
|
| 205 |
-
# Obtém amostra dos dados
|
| 206 |
-
sample_df = pd.read_sql_query("SELECT * FROM tabela LIMIT 10", engine)
|
| 207 |
-
|
| 208 |
-
# Converte para formato serializável
|
| 209 |
-
db_sample_dict = {
|
| 210 |
-
"data": sample_df.to_dict('records'),
|
| 211 |
-
"columns": list(sample_df.columns),
|
| 212 |
-
"dtypes": sample_df.dtypes.astype(str).to_dict(),
|
| 213 |
-
"shape": sample_df.shape
|
| 214 |
-
}
|
| 215 |
-
|
| 216 |
-
state["db_sample_dict"] = db_sample_dict
|
| 217 |
-
|
| 218 |
-
logging.info(f"[DATABASE] Amostra obtida: {sample_df.shape[0]} registros")
|
| 219 |
-
|
| 220 |
-
except Exception as e:
|
| 221 |
-
error_msg = f"Erro ao obter amostra do banco: {e}"
|
| 222 |
-
logging.error(f"[DATABASE] {error_msg}")
|
| 223 |
-
state["db_sample_dict"] = {}
|
| 224 |
-
state["error"] = error_msg
|
| 225 |
-
|
| 226 |
-
return state
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
nodes/graph_generation_node.py
DELETED
|
@@ -1,1015 +0,0 @@
|
|
| 1 |
-
"""
|
| 2 |
-
Nó para geração de gráficos
|
| 3 |
-
"""
|
| 4 |
-
import io
|
| 5 |
-
import logging
|
| 6 |
-
import numpy as np
|
| 7 |
-
import pandas as pd
|
| 8 |
-
import matplotlib.pyplot as plt
|
| 9 |
-
import matplotlib.dates as mdates
|
| 10 |
-
from PIL import Image
|
| 11 |
-
from typing import Dict, Any, Optional
|
| 12 |
-
|
| 13 |
-
from utils.object_manager import get_object_manager
|
| 14 |
-
|
| 15 |
-
async def graph_generation_node(state: Dict[str, Any]) -> Dict[str, Any]:
|
| 16 |
-
"""
|
| 17 |
-
Nó para geração de gráficos baseado no tipo selecionado
|
| 18 |
-
|
| 19 |
-
Args:
|
| 20 |
-
state: Estado atual do agente
|
| 21 |
-
|
| 22 |
-
Returns:
|
| 23 |
-
Estado atualizado com gráfico gerado
|
| 24 |
-
"""
|
| 25 |
-
try:
|
| 26 |
-
logging.info("[GRAPH_GENERATION] Iniciando geração de gráfico")
|
| 27 |
-
|
| 28 |
-
# Verifica se há tipo de gráfico selecionado
|
| 29 |
-
graph_type = state.get("graph_type")
|
| 30 |
-
if not graph_type:
|
| 31 |
-
logging.info("[GRAPH_GENERATION] Nenhum tipo de gráfico selecionado, pulando geração")
|
| 32 |
-
return state
|
| 33 |
-
|
| 34 |
-
# Verifica se há erro anterior
|
| 35 |
-
if state.get("graph_error"):
|
| 36 |
-
logging.info("[GRAPH_GENERATION] Erro anterior detectado, pulando geração")
|
| 37 |
-
return state
|
| 38 |
-
|
| 39 |
-
# Recupera dados do gráfico
|
| 40 |
-
graph_data = state.get("graph_data", {})
|
| 41 |
-
data_id = graph_data.get("data_id")
|
| 42 |
-
|
| 43 |
-
if not data_id:
|
| 44 |
-
error_msg = "ID dos dados do gráfico não encontrado"
|
| 45 |
-
logging.error(f"[GRAPH_GENERATION] {error_msg}")
|
| 46 |
-
state.update({
|
| 47 |
-
"graph_error": error_msg,
|
| 48 |
-
"graph_generated": False
|
| 49 |
-
})
|
| 50 |
-
return state
|
| 51 |
-
|
| 52 |
-
# Recupera DataFrame dos dados
|
| 53 |
-
obj_manager = get_object_manager()
|
| 54 |
-
df = obj_manager.get_object(data_id)
|
| 55 |
-
|
| 56 |
-
if df is None or df.empty:
|
| 57 |
-
error_msg = "Dados do gráfico não encontrados ou vazios"
|
| 58 |
-
logging.error(f"[GRAPH_GENERATION] {error_msg}")
|
| 59 |
-
state.update({
|
| 60 |
-
"graph_error": error_msg,
|
| 61 |
-
"graph_generated": False
|
| 62 |
-
})
|
| 63 |
-
return state
|
| 64 |
-
|
| 65 |
-
# Gera título do gráfico baseado na pergunta do usuário
|
| 66 |
-
user_query = state.get("user_input", "")
|
| 67 |
-
title = f"Visualização: {user_query[:50]}..." if len(user_query) > 50 else f"Visualização: {user_query}"
|
| 68 |
-
|
| 69 |
-
# Gera o gráfico
|
| 70 |
-
graph_image = await generate_graph(df, graph_type, title, user_query)
|
| 71 |
-
|
| 72 |
-
if graph_image is None:
|
| 73 |
-
error_msg = f"Falha ao gerar gráfico do tipo {graph_type}"
|
| 74 |
-
logging.error(f"[GRAPH_GENERATION] {error_msg}")
|
| 75 |
-
state.update({
|
| 76 |
-
"graph_error": error_msg,
|
| 77 |
-
"graph_generated": False
|
| 78 |
-
})
|
| 79 |
-
return state
|
| 80 |
-
|
| 81 |
-
# Armazena imagem do gráfico no ObjectManager
|
| 82 |
-
graph_image_id = obj_manager.store_object(graph_image, "graph_image")
|
| 83 |
-
|
| 84 |
-
# Atualiza estado
|
| 85 |
-
state.update({
|
| 86 |
-
"graph_image_id": graph_image_id,
|
| 87 |
-
"graph_generated": True,
|
| 88 |
-
"graph_error": None
|
| 89 |
-
})
|
| 90 |
-
|
| 91 |
-
logging.info(f"[GRAPH_GENERATION] Gráfico gerado com sucesso: {graph_type}")
|
| 92 |
-
|
| 93 |
-
except Exception as e:
|
| 94 |
-
error_msg = f"Erro na geração de gráfico: {e}"
|
| 95 |
-
logging.error(f"[GRAPH_GENERATION] {error_msg}")
|
| 96 |
-
state.update({
|
| 97 |
-
"graph_error": error_msg,
|
| 98 |
-
"graph_generated": False
|
| 99 |
-
})
|
| 100 |
-
|
| 101 |
-
return state
|
| 102 |
-
|
| 103 |
-
async def generate_graph(df: pd.DataFrame, graph_type: str, title: str = None, user_query: str = None) -> Optional[Image.Image]:
|
| 104 |
-
"""
|
| 105 |
-
Gera um gráfico com base no DataFrame e tipo especificado
|
| 106 |
-
|
| 107 |
-
Args:
|
| 108 |
-
df: DataFrame com os dados
|
| 109 |
-
graph_type: Tipo de gráfico a ser gerado
|
| 110 |
-
title: Título do gráfico
|
| 111 |
-
user_query: Pergunta original do usuário
|
| 112 |
-
|
| 113 |
-
Returns:
|
| 114 |
-
Imagem PIL do gráfico ou None se falhar
|
| 115 |
-
"""
|
| 116 |
-
logging.info(f"[GRAPH_GENERATION] Gerando gráfico tipo {graph_type}. DataFrame: {len(df)} linhas")
|
| 117 |
-
|
| 118 |
-
if df.empty:
|
| 119 |
-
logging.warning("[GRAPH_GENERATION] DataFrame vazio")
|
| 120 |
-
return None
|
| 121 |
-
|
| 122 |
-
try:
|
| 123 |
-
# Preparar dados usando lógica UNIFICADA
|
| 124 |
-
prepared_df = prepare_data_for_graph_unified(df, graph_type, user_query)
|
| 125 |
-
if prepared_df.empty:
|
| 126 |
-
logging.warning("[GRAPH_GENERATION] DataFrame preparado está vazio")
|
| 127 |
-
return None
|
| 128 |
-
|
| 129 |
-
# Configurações gerais
|
| 130 |
-
plt.style.use('default')
|
| 131 |
-
colors = plt.cm.tab10.colors
|
| 132 |
-
|
| 133 |
-
# Gerar gráfico baseado no tipo
|
| 134 |
-
if graph_type == 'line_simple':
|
| 135 |
-
return await generate_line_simple(prepared_df, title, colors)
|
| 136 |
-
elif graph_type == 'multiline':
|
| 137 |
-
return await generate_multiline(prepared_df, title, colors)
|
| 138 |
-
elif graph_type == 'area':
|
| 139 |
-
return await generate_area(prepared_df, title, colors)
|
| 140 |
-
elif graph_type == 'bar_vertical':
|
| 141 |
-
return await generate_bar_vertical(prepared_df, title, colors)
|
| 142 |
-
elif graph_type == 'bar_horizontal':
|
| 143 |
-
return await generate_bar_horizontal(prepared_df, title, colors)
|
| 144 |
-
elif graph_type == 'bar_grouped':
|
| 145 |
-
return await generate_bar_grouped(prepared_df, title, colors)
|
| 146 |
-
elif graph_type == 'bar_stacked':
|
| 147 |
-
return await generate_bar_stacked(prepared_df, title, colors)
|
| 148 |
-
elif graph_type == 'pie':
|
| 149 |
-
return await generate_pie(prepared_df, title, colors)
|
| 150 |
-
elif graph_type == 'donut':
|
| 151 |
-
return await generate_donut(prepared_df, title, colors)
|
| 152 |
-
elif graph_type == 'pie_multiple':
|
| 153 |
-
return await generate_pie_multiple(prepared_df, title, colors)
|
| 154 |
-
else:
|
| 155 |
-
logging.warning(f"[GRAPH_GENERATION] Tipo '{graph_type}' não reconhecido, usando bar_vertical")
|
| 156 |
-
return await generate_bar_vertical(prepared_df, title, colors)
|
| 157 |
-
|
| 158 |
-
except Exception as e:
|
| 159 |
-
logging.error(f"[GRAPH_GENERATION] Erro ao gerar gráfico: {e}")
|
| 160 |
-
return None
|
| 161 |
-
|
| 162 |
-
def analyze_dataframe_structure(df: pd.DataFrame) -> Dict[str, Any]:
|
| 163 |
-
"""
|
| 164 |
-
Analisa a estrutura do DataFrame e retorna informações detalhadas
|
| 165 |
-
|
| 166 |
-
Args:
|
| 167 |
-
df: DataFrame a ser analisado
|
| 168 |
-
|
| 169 |
-
Returns:
|
| 170 |
-
Dicionário com informações sobre tipos de colunas e estrutura
|
| 171 |
-
"""
|
| 172 |
-
if df.empty:
|
| 173 |
-
return {
|
| 174 |
-
'numeric_cols': [],
|
| 175 |
-
'date_cols': [],
|
| 176 |
-
'categorical_cols': [],
|
| 177 |
-
'total_cols': 0,
|
| 178 |
-
'has_multiple_numerics': False,
|
| 179 |
-
'has_multiple_categoricals': False,
|
| 180 |
-
'is_suitable_for_grouping': False
|
| 181 |
-
}
|
| 182 |
-
|
| 183 |
-
# Analisar tipos de colunas de forma mais robusta
|
| 184 |
-
numeric_cols = []
|
| 185 |
-
date_cols = []
|
| 186 |
-
categorical_cols = []
|
| 187 |
-
|
| 188 |
-
for col in df.columns:
|
| 189 |
-
col_data = df[col]
|
| 190 |
-
|
| 191 |
-
# Verificar se é numérico (incluindo strings que representam números)
|
| 192 |
-
if pd.api.types.is_numeric_dtype(col_data):
|
| 193 |
-
numeric_cols.append(col)
|
| 194 |
-
elif col_data.dtype == 'object':
|
| 195 |
-
# Tentar converter para numérico
|
| 196 |
-
try:
|
| 197 |
-
test_numeric = pd.to_numeric(col_data.astype(str).str.replace(',', '.'), errors='coerce')
|
| 198 |
-
if test_numeric.notna().sum() > len(col_data) * 0.8: # 80% são números válidos
|
| 199 |
-
numeric_cols.append(col)
|
| 200 |
-
else:
|
| 201 |
-
# Verificar se é data
|
| 202 |
-
if any(date_indicator in col.lower() for date_indicator in ['data', 'date', 'time', 'dia', 'mes', 'ano']):
|
| 203 |
-
try:
|
| 204 |
-
pd.to_datetime(col_data.head(3), errors='raise')
|
| 205 |
-
date_cols.append(col)
|
| 206 |
-
except:
|
| 207 |
-
categorical_cols.append(col)
|
| 208 |
-
else:
|
| 209 |
-
categorical_cols.append(col)
|
| 210 |
-
except:
|
| 211 |
-
categorical_cols.append(col)
|
| 212 |
-
elif pd.api.types.is_datetime64_any_dtype(col_data):
|
| 213 |
-
date_cols.append(col)
|
| 214 |
-
else:
|
| 215 |
-
categorical_cols.append(col)
|
| 216 |
-
|
| 217 |
-
return {
|
| 218 |
-
'numeric_cols': numeric_cols,
|
| 219 |
-
'date_cols': date_cols,
|
| 220 |
-
'categorical_cols': categorical_cols,
|
| 221 |
-
'total_cols': len(df.columns),
|
| 222 |
-
'has_multiple_numerics': len(numeric_cols) >= 2,
|
| 223 |
-
'has_multiple_categoricals': len(categorical_cols) >= 2,
|
| 224 |
-
'is_suitable_for_grouping': len(categorical_cols) >= 2 or (len(categorical_cols) >= 1 and len(numeric_cols) >= 2)
|
| 225 |
-
}
|
| 226 |
-
|
| 227 |
-
def prepare_data_for_graph_unified(df: pd.DataFrame, graph_type: str, user_query: str = None) -> pd.DataFrame:
|
| 228 |
-
"""
|
| 229 |
-
FUNÇÃO UNIFICADA para preparação de dados - substitui lógica duplicada
|
| 230 |
-
|
| 231 |
-
Args:
|
| 232 |
-
df: DataFrame original
|
| 233 |
-
graph_type: Tipo de gráfico
|
| 234 |
-
user_query: Pergunta do usuário
|
| 235 |
-
|
| 236 |
-
Returns:
|
| 237 |
-
DataFrame preparado com colunas adequadas para o tipo de gráfico
|
| 238 |
-
"""
|
| 239 |
-
logging.info(f"[GRAPH_GENERATION] 🔧 Preparação UNIFICADA para {graph_type}")
|
| 240 |
-
|
| 241 |
-
if df.empty:
|
| 242 |
-
logging.warning("[GRAPH_GENERATION] DataFrame vazio")
|
| 243 |
-
return df
|
| 244 |
-
|
| 245 |
-
# Fazer cópia para não modificar original
|
| 246 |
-
prepared_df = df.copy()
|
| 247 |
-
|
| 248 |
-
# Analisar estrutura do DataFrame
|
| 249 |
-
structure = analyze_dataframe_structure(prepared_df)
|
| 250 |
-
numeric_cols = structure['numeric_cols']
|
| 251 |
-
date_cols = structure['date_cols']
|
| 252 |
-
categorical_cols = structure['categorical_cols']
|
| 253 |
-
|
| 254 |
-
logging.info(f"[GRAPH_GENERATION] 📊 Estrutura: {len(numeric_cols)} numéricas, {len(date_cols)} datas, {len(categorical_cols)} categóricas")
|
| 255 |
-
|
| 256 |
-
# Preparação específica por tipo de gráfico
|
| 257 |
-
if graph_type in ['line_simple', 'area']:
|
| 258 |
-
return _prepare_for_temporal_graphs(prepared_df, date_cols, numeric_cols, categorical_cols)
|
| 259 |
-
|
| 260 |
-
elif graph_type in ['bar_vertical', 'bar_horizontal']:
|
| 261 |
-
return _prepare_for_simple_bar_graphs(prepared_df, categorical_cols, numeric_cols, graph_type)
|
| 262 |
-
|
| 263 |
-
elif graph_type in ['bar_grouped', 'bar_stacked']:
|
| 264 |
-
return _prepare_for_grouped_graphs(prepared_df, structure, graph_type)
|
| 265 |
-
|
| 266 |
-
elif graph_type in ['pie', 'donut', 'pie_multiple']:
|
| 267 |
-
return _prepare_for_pie_graphs(prepared_df, categorical_cols, numeric_cols, graph_type)
|
| 268 |
-
|
| 269 |
-
elif graph_type == 'multiline':
|
| 270 |
-
return _prepare_for_multiline_graphs(prepared_df, structure)
|
| 271 |
-
|
| 272 |
-
else:
|
| 273 |
-
logging.warning(f"[GRAPH_GENERATION] Tipo {graph_type} não reconhecido, usando preparação básica")
|
| 274 |
-
return _prepare_basic_fallback(prepared_df, categorical_cols, numeric_cols)
|
| 275 |
-
|
| 276 |
-
def _prepare_for_temporal_graphs(df: pd.DataFrame, date_cols: list, numeric_cols: list, categorical_cols: list) -> pd.DataFrame:
|
| 277 |
-
"""Prepara dados para gráficos temporais (linha, área)"""
|
| 278 |
-
if date_cols and numeric_cols:
|
| 279 |
-
# Usar primeira coluna de data e primeira numérica
|
| 280 |
-
x_col, y_col = date_cols[0], numeric_cols[0]
|
| 281 |
-
result_df = df[[x_col, y_col]].sort_values(by=x_col)
|
| 282 |
-
logging.info(f"[GRAPH_GENERATION] 📅 Temporal: {x_col} (data) + {y_col} (numérica)")
|
| 283 |
-
return result_df
|
| 284 |
-
elif categorical_cols and numeric_cols:
|
| 285 |
-
# Usar primeira categórica e primeira numérica
|
| 286 |
-
x_col, y_col = categorical_cols[0], numeric_cols[0]
|
| 287 |
-
result_df = df[[x_col, y_col]].sort_values(by=y_col)
|
| 288 |
-
logging.info(f"[GRAPH_GENERATION] 📊 Categórico: {x_col} + {y_col}")
|
| 289 |
-
return result_df
|
| 290 |
-
else:
|
| 291 |
-
logging.warning("[GRAPH_GENERATION] Dados insuficientes para gráfico temporal")
|
| 292 |
-
return df
|
| 293 |
-
|
| 294 |
-
def _prepare_for_simple_bar_graphs(df: pd.DataFrame, categorical_cols: list, numeric_cols: list, graph_type: str) -> pd.DataFrame:
|
| 295 |
-
"""Prepara dados para gráficos de barras simples"""
|
| 296 |
-
if categorical_cols and numeric_cols:
|
| 297 |
-
x_col, y_col = categorical_cols[0], numeric_cols[0]
|
| 298 |
-
result_df = df[[x_col, y_col]].sort_values(by=y_col, ascending=False)
|
| 299 |
-
|
| 300 |
-
# Limitar categorias para barras verticais
|
| 301 |
-
if graph_type == 'bar_vertical' and len(result_df) > 15:
|
| 302 |
-
result_df = result_df.head(15)
|
| 303 |
-
logging.info(f"[GRAPH_GENERATION] 📊 Limitado a 15 categorias para {graph_type}")
|
| 304 |
-
|
| 305 |
-
logging.info(f"[GRAPH_GENERATION] 📊 Barras simples: {x_col} + {y_col}")
|
| 306 |
-
return result_df
|
| 307 |
-
else:
|
| 308 |
-
logging.warning("[GRAPH_GENERATION] Dados insuficientes para gráfico de barras")
|
| 309 |
-
return df
|
| 310 |
-
|
| 311 |
-
def _prepare_for_grouped_graphs(df: pd.DataFrame, structure: dict, graph_type: str) -> pd.DataFrame:
|
| 312 |
-
"""
|
| 313 |
-
FUNÇÃO CRÍTICA: Prepara dados para gráficos agrupados com lógica inteligente
|
| 314 |
-
"""
|
| 315 |
-
numeric_cols = structure['numeric_cols']
|
| 316 |
-
categorical_cols = structure['categorical_cols']
|
| 317 |
-
has_multiple_numerics = structure['has_multiple_numerics']
|
| 318 |
-
has_multiple_categoricals = structure['has_multiple_categoricals']
|
| 319 |
-
|
| 320 |
-
logging.info(f"[GRAPH_GENERATION] 🎯 Preparando agrupado: {len(numeric_cols)} num, {len(categorical_cols)} cat")
|
| 321 |
-
|
| 322 |
-
if has_multiple_numerics:
|
| 323 |
-
# CENÁRIO 1: Múltiplas numéricas - usar primeira categórica + todas numéricas
|
| 324 |
-
cols_to_keep = [categorical_cols[0]] + numeric_cols
|
| 325 |
-
result_df = df[cols_to_keep]
|
| 326 |
-
logging.info(f"[GRAPH_GENERATION] ✅ Múltiplas numéricas: {cols_to_keep}")
|
| 327 |
-
return result_df
|
| 328 |
-
|
| 329 |
-
elif len(numeric_cols) == 1 and has_multiple_categoricals:
|
| 330 |
-
# CENÁRIO 2: 1 numérica + múltiplas categóricas - AGRUPAMENTO POR COR
|
| 331 |
-
# Usar TODAS as categóricas + a numérica
|
| 332 |
-
cols_to_keep = categorical_cols + numeric_cols
|
| 333 |
-
result_df = df[cols_to_keep]
|
| 334 |
-
logging.info(f"[GRAPH_GENERATION] ✅ Agrupamento por cor: {cols_to_keep}")
|
| 335 |
-
return result_df
|
| 336 |
-
|
| 337 |
-
elif len(numeric_cols) == 1 and len(categorical_cols) == 1:
|
| 338 |
-
# CENÁRIO 3: 1 numérica + 1 categórica - gráfico simples
|
| 339 |
-
cols_to_keep = categorical_cols + numeric_cols
|
| 340 |
-
result_df = df[cols_to_keep]
|
| 341 |
-
logging.info(f"[GRAPH_GENERATION] ⚠️ Dados simples para agrupado: {cols_to_keep}")
|
| 342 |
-
return result_df
|
| 343 |
-
|
| 344 |
-
else:
|
| 345 |
-
# CENÁRIO 4: Dados inadequados
|
| 346 |
-
logging.warning("[GRAPH_GENERATION] ❌ Dados inadequados para gráfico agrupado")
|
| 347 |
-
return df
|
| 348 |
-
|
| 349 |
-
def _prepare_for_pie_graphs(df: pd.DataFrame, categorical_cols: list, numeric_cols: list, graph_type: str) -> pd.DataFrame:
|
| 350 |
-
"""Prepara dados para gráficos de pizza"""
|
| 351 |
-
if categorical_cols and numeric_cols:
|
| 352 |
-
cat_col, val_col = categorical_cols[0], numeric_cols[0]
|
| 353 |
-
|
| 354 |
-
if graph_type == 'pie_multiple' and len(categorical_cols) >= 2:
|
| 355 |
-
# Para pizzas múltiplas, manter 2 categóricas + 1 numérica
|
| 356 |
-
result_df = df[[categorical_cols[0], categorical_cols[1], val_col]]
|
| 357 |
-
logging.info(f"[GRAPH_GENERATION] 🥧 Pizzas múltiplas: {result_df.columns.tolist()}")
|
| 358 |
-
else:
|
| 359 |
-
# Agrupar e somar valores para pizza simples/donut
|
| 360 |
-
result_df = df.groupby(cat_col)[val_col].sum().reset_index()
|
| 361 |
-
result_df = result_df.sort_values(by=val_col, ascending=False)
|
| 362 |
-
|
| 363 |
-
# Limitar a 10 categorias
|
| 364 |
-
if len(result_df) > 10:
|
| 365 |
-
top_9 = result_df.head(9)
|
| 366 |
-
others_sum = result_df.iloc[9:][val_col].sum()
|
| 367 |
-
if others_sum > 0:
|
| 368 |
-
others_row = pd.DataFrame({cat_col: ['Outros'], val_col: [others_sum]})
|
| 369 |
-
result_df = pd.concat([top_9, others_row], ignore_index=True)
|
| 370 |
-
else:
|
| 371 |
-
result_df = top_9
|
| 372 |
-
|
| 373 |
-
logging.info(f"[GRAPH_GENERATION] 🥧 Pizza: {cat_col} + {val_col} ({len(result_df)} categorias)")
|
| 374 |
-
|
| 375 |
-
return result_df
|
| 376 |
-
else:
|
| 377 |
-
logging.warning("[GRAPH_GENERATION] Dados insuficientes para gráfico de pizza")
|
| 378 |
-
return df
|
| 379 |
-
|
| 380 |
-
def _prepare_for_multiline_graphs(df: pd.DataFrame, structure: dict) -> pd.DataFrame:
|
| 381 |
-
"""Prepara dados para gráficos de múltiplas linhas"""
|
| 382 |
-
date_cols = structure['date_cols']
|
| 383 |
-
numeric_cols = structure['numeric_cols']
|
| 384 |
-
categorical_cols = structure['categorical_cols']
|
| 385 |
-
|
| 386 |
-
if date_cols and len(numeric_cols) >= 2:
|
| 387 |
-
# Data + múltiplas numéricas
|
| 388 |
-
cols_to_keep = [date_cols[0]] + numeric_cols
|
| 389 |
-
result_df = df[cols_to_keep].sort_values(by=date_cols[0])
|
| 390 |
-
logging.info(f"[GRAPH_GENERATION] 📈 Multilinhas temporais: {cols_to_keep}")
|
| 391 |
-
return result_df
|
| 392 |
-
elif categorical_cols and len(numeric_cols) >= 2:
|
| 393 |
-
# Categórica + múltiplas numéricas
|
| 394 |
-
cols_to_keep = [categorical_cols[0]] + numeric_cols
|
| 395 |
-
result_df = df[cols_to_keep]
|
| 396 |
-
logging.info(f"[GRAPH_GENERATION] 📈 Multilinhas categóricas: {cols_to_keep}")
|
| 397 |
-
return result_df
|
| 398 |
-
else:
|
| 399 |
-
logging.warning("[GRAPH_GENERATION] Dados insuficientes para multilinhas")
|
| 400 |
-
return df
|
| 401 |
-
|
| 402 |
-
def _prepare_basic_fallback(df: pd.DataFrame, categorical_cols: list, numeric_cols: list) -> pd.DataFrame:
|
| 403 |
-
"""Preparação básica de fallback"""
|
| 404 |
-
if categorical_cols and numeric_cols:
|
| 405 |
-
result_df = df[[categorical_cols[0], numeric_cols[0]]]
|
| 406 |
-
logging.info(f"[GRAPH_GENERATION] 🔄 Fallback básico: {result_df.columns.tolist()}")
|
| 407 |
-
return result_df
|
| 408 |
-
else:
|
| 409 |
-
logging.warning("[GRAPH_GENERATION] Dados inadequados para qualquer gráfico")
|
| 410 |
-
return df
|
| 411 |
-
|
| 412 |
-
def save_plot_to_image() -> Image.Image:
|
| 413 |
-
"""
|
| 414 |
-
Salva o plot atual do matplotlib como imagem PIL
|
| 415 |
-
|
| 416 |
-
Returns:
|
| 417 |
-
Imagem PIL
|
| 418 |
-
"""
|
| 419 |
-
buf = io.BytesIO()
|
| 420 |
-
plt.savefig(buf, format='png', dpi=100, bbox_inches='tight', facecolor='white')
|
| 421 |
-
buf.seek(0)
|
| 422 |
-
img = Image.open(buf)
|
| 423 |
-
plt.close() # Importante: fechar o plot para liberar memória
|
| 424 |
-
return img
|
| 425 |
-
|
| 426 |
-
# ==================== FUNÇÕES DE GERAÇÃO ESPECÍFICAS ====================
|
| 427 |
-
|
| 428 |
-
async def generate_line_simple(df: pd.DataFrame, title: str, colors) -> Optional[Image.Image]:
|
| 429 |
-
"""Gera gráfico de linha simples"""
|
| 430 |
-
if len(df.columns) < 2:
|
| 431 |
-
return None
|
| 432 |
-
|
| 433 |
-
x_col, y_col = df.columns[0], df.columns[1]
|
| 434 |
-
is_date = pd.api.types.is_datetime64_any_dtype(df[x_col])
|
| 435 |
-
|
| 436 |
-
plt.figure(figsize=(12, 6))
|
| 437 |
-
|
| 438 |
-
if is_date:
|
| 439 |
-
plt.plot(df[x_col], df[y_col], marker='o', linewidth=2, color=colors[0])
|
| 440 |
-
plt.gcf().autofmt_xdate()
|
| 441 |
-
plt.gca().xaxis.set_major_formatter(mdates.DateFormatter('%d/%m/%Y'))
|
| 442 |
-
else:
|
| 443 |
-
plt.plot(range(len(df)), df[y_col], marker='o', linewidth=2, color=colors[0])
|
| 444 |
-
plt.xticks(range(len(df)), df[x_col], rotation=45, ha='right')
|
| 445 |
-
|
| 446 |
-
plt.xlabel(x_col)
|
| 447 |
-
plt.ylabel(y_col)
|
| 448 |
-
plt.title(title or f"{y_col} por {x_col}")
|
| 449 |
-
plt.grid(True, linestyle='--', alpha=0.7)
|
| 450 |
-
plt.tight_layout()
|
| 451 |
-
|
| 452 |
-
return save_plot_to_image()
|
| 453 |
-
|
| 454 |
-
async def generate_multiline(df: pd.DataFrame, title: str, colors) -> Optional[Image.Image]:
|
| 455 |
-
"""Gera gráfico de múltiplas linhas"""
|
| 456 |
-
if len(df.columns) < 2:
|
| 457 |
-
return None
|
| 458 |
-
|
| 459 |
-
x_col = df.columns[0]
|
| 460 |
-
y_cols = [col for col in df.columns[1:] if pd.api.types.is_numeric_dtype(df[col])]
|
| 461 |
-
|
| 462 |
-
if not y_cols:
|
| 463 |
-
return await generate_line_simple(df, title, colors)
|
| 464 |
-
|
| 465 |
-
is_date = pd.api.types.is_datetime64_any_dtype(df[x_col])
|
| 466 |
-
|
| 467 |
-
plt.figure(figsize=(12, 6))
|
| 468 |
-
|
| 469 |
-
for i, y_col in enumerate(y_cols):
|
| 470 |
-
if is_date:
|
| 471 |
-
plt.plot(df[x_col], df[y_col], marker='o', linewidth=2,
|
| 472 |
-
label=y_col, color=colors[i % len(colors)])
|
| 473 |
-
else:
|
| 474 |
-
plt.plot(range(len(df)), df[y_col], marker='o', linewidth=2,
|
| 475 |
-
label=y_col, color=colors[i % len(colors)])
|
| 476 |
-
|
| 477 |
-
if is_date:
|
| 478 |
-
plt.gcf().autofmt_xdate()
|
| 479 |
-
plt.gca().xaxis.set_major_formatter(mdates.DateFormatter('%d/%m/%Y'))
|
| 480 |
-
else:
|
| 481 |
-
plt.xticks(range(len(df)), df[x_col], rotation=45, ha='right')
|
| 482 |
-
|
| 483 |
-
plt.xlabel(x_col)
|
| 484 |
-
plt.ylabel("Valores")
|
| 485 |
-
plt.title(title or f"Comparação por {x_col}")
|
| 486 |
-
plt.legend(title="Séries", loc='best')
|
| 487 |
-
plt.grid(True, linestyle='--', alpha=0.7)
|
| 488 |
-
plt.tight_layout()
|
| 489 |
-
|
| 490 |
-
return save_plot_to_image()
|
| 491 |
-
|
| 492 |
-
async def generate_area(df: pd.DataFrame, title: str, colors) -> Optional[Image.Image]:
|
| 493 |
-
"""Gera gráfico de área"""
|
| 494 |
-
if len(df.columns) < 2:
|
| 495 |
-
return None
|
| 496 |
-
|
| 497 |
-
x_col, y_col = df.columns[0], df.columns[1]
|
| 498 |
-
is_date = pd.api.types.is_datetime64_any_dtype(df[x_col])
|
| 499 |
-
|
| 500 |
-
plt.figure(figsize=(12, 6))
|
| 501 |
-
|
| 502 |
-
if is_date:
|
| 503 |
-
plt.fill_between(df[x_col], df[y_col], alpha=0.5, color=colors[0])
|
| 504 |
-
plt.plot(df[x_col], df[y_col], color=colors[0], linewidth=2)
|
| 505 |
-
plt.gcf().autofmt_xdate()
|
| 506 |
-
plt.gca().xaxis.set_major_formatter(mdates.DateFormatter('%d/%m/%Y'))
|
| 507 |
-
else:
|
| 508 |
-
plt.fill_between(range(len(df)), df[y_col], alpha=0.5, color=colors[0])
|
| 509 |
-
plt.plot(range(len(df)), df[y_col], color=colors[0], linewidth=2)
|
| 510 |
-
plt.xticks(range(len(df)), df[x_col], rotation=45, ha='right')
|
| 511 |
-
|
| 512 |
-
plt.xlabel(x_col)
|
| 513 |
-
plt.ylabel(y_col)
|
| 514 |
-
plt.title(title or f"{y_col} por {x_col}")
|
| 515 |
-
plt.grid(True, linestyle='--', alpha=0.7)
|
| 516 |
-
plt.tight_layout()
|
| 517 |
-
|
| 518 |
-
return save_plot_to_image()
|
| 519 |
-
|
| 520 |
-
async def generate_bar_vertical(df: pd.DataFrame, title: str, colors) -> Optional[Image.Image]:
|
| 521 |
-
"""Gera gráfico de barras verticais"""
|
| 522 |
-
if len(df.columns) < 2:
|
| 523 |
-
return None
|
| 524 |
-
|
| 525 |
-
x_col, y_col = df.columns[0], df.columns[1]
|
| 526 |
-
|
| 527 |
-
# Preparar dados numéricos - converter strings com vírgula para float
|
| 528 |
-
df_plot = df.copy()
|
| 529 |
-
try:
|
| 530 |
-
if df_plot[y_col].dtype == 'object':
|
| 531 |
-
# Converte strings para números, tratando vírgulas como separador decimal
|
| 532 |
-
df_plot[y_col] = pd.to_numeric(df_plot[y_col].astype(str).str.replace(',', '.'), errors='coerce')
|
| 533 |
-
|
| 534 |
-
# Remove linhas com valores não numéricos
|
| 535 |
-
df_plot = df_plot.dropna(subset=[y_col])
|
| 536 |
-
|
| 537 |
-
if df_plot.empty:
|
| 538 |
-
logging.error(f"[GRAPH_GENERATION] Nenhum valor numérico válido encontrado na coluna {y_col}")
|
| 539 |
-
return None
|
| 540 |
-
|
| 541 |
-
except Exception as e:
|
| 542 |
-
logging.error(f"[GRAPH_GENERATION] Erro ao converter dados para numérico: {e}")
|
| 543 |
-
return None
|
| 544 |
-
|
| 545 |
-
plt.figure(figsize=(12, 8))
|
| 546 |
-
bars = plt.bar(range(len(df_plot)), df_plot[y_col], color=colors[0])
|
| 547 |
-
|
| 548 |
-
# Adicionar valores nas barras
|
| 549 |
-
try:
|
| 550 |
-
max_value = df_plot[y_col].max()
|
| 551 |
-
for i, bar in enumerate(bars):
|
| 552 |
-
height = bar.get_height()
|
| 553 |
-
if isinstance(height, (int, float)) and not pd.isna(height):
|
| 554 |
-
plt.text(bar.get_x() + bar.get_width()/2., height + 0.02 * max_value,
|
| 555 |
-
f'{height:,.0f}', ha='center', fontsize=9)
|
| 556 |
-
except Exception as e:
|
| 557 |
-
logging.warning(f"[GRAPH_GENERATION] Erro ao adicionar valores nas barras: {e}")
|
| 558 |
-
|
| 559 |
-
plt.xlabel(x_col)
|
| 560 |
-
plt.ylabel(y_col)
|
| 561 |
-
plt.title(title or f"{y_col} por {x_col}")
|
| 562 |
-
plt.xticks(range(len(df_plot)), df_plot[x_col], rotation=45, ha='right')
|
| 563 |
-
plt.grid(True, linestyle='--', alpha=0.7, axis='y')
|
| 564 |
-
plt.tight_layout()
|
| 565 |
-
|
| 566 |
-
return save_plot_to_image()
|
| 567 |
-
|
| 568 |
-
async def generate_bar_horizontal(df: pd.DataFrame, title: str, colors) -> Optional[Image.Image]:
|
| 569 |
-
"""Gera gráfico de barras horizontais"""
|
| 570 |
-
if len(df.columns) < 2:
|
| 571 |
-
return None
|
| 572 |
-
|
| 573 |
-
x_col, y_col = df.columns[0], df.columns[1]
|
| 574 |
-
|
| 575 |
-
# Preparar dados numéricos - converter strings com vírgula para float
|
| 576 |
-
df_plot = df.copy()
|
| 577 |
-
try:
|
| 578 |
-
if df_plot[y_col].dtype == 'object':
|
| 579 |
-
# Converte strings para números, tratando vírgulas como separador decimal
|
| 580 |
-
df_plot[y_col] = pd.to_numeric(df_plot[y_col].astype(str).str.replace(',', '.'), errors='coerce')
|
| 581 |
-
|
| 582 |
-
# Remove linhas com valores não numéricos
|
| 583 |
-
df_plot = df_plot.dropna(subset=[y_col])
|
| 584 |
-
|
| 585 |
-
if df_plot.empty:
|
| 586 |
-
logging.error(f"[GRAPH_GENERATION] Nenhum valor numérico válido encontrado na coluna {y_col}")
|
| 587 |
-
return None
|
| 588 |
-
|
| 589 |
-
except Exception as e:
|
| 590 |
-
logging.error(f"[GRAPH_GENERATION] Erro ao converter dados para numérico: {e}")
|
| 591 |
-
return None
|
| 592 |
-
|
| 593 |
-
plt.figure(figsize=(12, max(6, len(df_plot) * 0.4)))
|
| 594 |
-
bars = plt.barh(range(len(df_plot)), df_plot[y_col], color=colors[0])
|
| 595 |
-
|
| 596 |
-
# Adicionar valores nas barras
|
| 597 |
-
try:
|
| 598 |
-
max_value = df_plot[y_col].max()
|
| 599 |
-
for i, bar in enumerate(bars):
|
| 600 |
-
width = bar.get_width()
|
| 601 |
-
if isinstance(width, (int, float)) and not pd.isna(width):
|
| 602 |
-
plt.text(width + 0.02 * max_value, bar.get_y() + bar.get_height()/2.,
|
| 603 |
-
f'{width:,.0f}', va='center', fontsize=9)
|
| 604 |
-
except Exception as e:
|
| 605 |
-
logging.warning(f"[GRAPH_GENERATION] Erro ao adicionar valores nas barras: {e}")
|
| 606 |
-
|
| 607 |
-
plt.xlabel(y_col)
|
| 608 |
-
plt.ylabel(x_col)
|
| 609 |
-
plt.title(title or f"{y_col} por {x_col}")
|
| 610 |
-
plt.yticks(range(len(df_plot)), df_plot[x_col])
|
| 611 |
-
plt.grid(True, linestyle='--', alpha=0.7, axis='x')
|
| 612 |
-
plt.tight_layout()
|
| 613 |
-
|
| 614 |
-
return save_plot_to_image()
|
| 615 |
-
|
| 616 |
-
async def generate_bar_grouped(df: pd.DataFrame, title: str, colors) -> Optional[Image.Image]:
|
| 617 |
-
"""
|
| 618 |
-
FUNÇÃO REFATORADA: Gera gráfico de barras agrupadas com fallbacks inteligentes
|
| 619 |
-
"""
|
| 620 |
-
logging.info(f"[GRAPH_GENERATION] 🎯 Gerando barras agrupadas REFATORADO. Colunas: {df.columns.tolist()}")
|
| 621 |
-
|
| 622 |
-
if len(df.columns) < 2:
|
| 623 |
-
logging.warning("[GRAPH_GENERATION] ❌ Dados insuficientes para gráfico agrupado")
|
| 624 |
-
return None
|
| 625 |
-
|
| 626 |
-
# Analisar estrutura dos dados
|
| 627 |
-
structure = analyze_dataframe_structure(df)
|
| 628 |
-
numeric_cols = structure['numeric_cols']
|
| 629 |
-
categorical_cols = structure['categorical_cols']
|
| 630 |
-
|
| 631 |
-
logging.info(f"[GRAPH_GENERATION] 📊 Estrutura: {len(numeric_cols)} numéricas, {len(categorical_cols)} categóricas")
|
| 632 |
-
|
| 633 |
-
if not numeric_cols:
|
| 634 |
-
logging.warning("[GRAPH_GENERATION] ❌ Nenhuma coluna numérica encontrada")
|
| 635 |
-
return await generate_bar_vertical(df, title, colors)
|
| 636 |
-
|
| 637 |
-
# DECISÃO INTELIGENTE baseada na estrutura dos dados
|
| 638 |
-
if len(numeric_cols) >= 2:
|
| 639 |
-
# CENÁRIO 1: Múltiplas numéricas - gráfico agrupado tradicional
|
| 640 |
-
return await _generate_multi_numeric_grouped(df, title, colors, categorical_cols[0], numeric_cols)
|
| 641 |
-
|
| 642 |
-
elif len(numeric_cols) == 1 and len(categorical_cols) >= 2:
|
| 643 |
-
# CENÁRIO 2: 1 numérica + múltiplas categóricas - agrupamento por cor
|
| 644 |
-
return await _generate_color_grouped_bars(df, title, colors, categorical_cols, numeric_cols[0])
|
| 645 |
-
|
| 646 |
-
elif len(numeric_cols) == 1 and len(categorical_cols) == 1:
|
| 647 |
-
# CENÁRIO 3: Dados simples - fallback inteligente para barras verticais
|
| 648 |
-
logging.info("[GRAPH_GENERATION] ⚠️ Dados simples, usando barras verticais")
|
| 649 |
-
return await generate_bar_vertical(df, title, colors)
|
| 650 |
-
|
| 651 |
-
else:
|
| 652 |
-
# CENÁRIO 4: Estrutura inadequada
|
| 653 |
-
logging.warning("[GRAPH_GENERATION] ❌ Estrutura de dados inadequada para agrupamento")
|
| 654 |
-
return await generate_bar_vertical(df, title, colors)
|
| 655 |
-
|
| 656 |
-
async def _generate_multi_numeric_grouped(df: pd.DataFrame, title: str, colors, x_col: str, y_cols: list) -> Optional[Image.Image]:
|
| 657 |
-
"""
|
| 658 |
-
Gera gráfico agrupado com múltiplas colunas numéricas (cenário tradicional)
|
| 659 |
-
"""
|
| 660 |
-
logging.info(f"[GRAPH_GENERATION] 📊 Múltiplas numéricas: {x_col} + {y_cols}")
|
| 661 |
-
|
| 662 |
-
# Converter colunas numéricas se necessário
|
| 663 |
-
for col in y_cols:
|
| 664 |
-
if df[col].dtype == 'object':
|
| 665 |
-
df[col] = pd.to_numeric(df[col].astype(str).str.replace(',', '.'), errors='coerce')
|
| 666 |
-
|
| 667 |
-
# Remover linhas com valores NaN
|
| 668 |
-
df_clean = df.dropna(subset=y_cols)
|
| 669 |
-
|
| 670 |
-
if df_clean.empty:
|
| 671 |
-
logging.error("[GRAPH_GENERATION] ❌ Todos os valores são NaN após conversão")
|
| 672 |
-
return None
|
| 673 |
-
|
| 674 |
-
# Verificar diferença de escala entre colunas
|
| 675 |
-
col_ranges = {col: df_clean[col].max() - df_clean[col].min() for col in y_cols}
|
| 676 |
-
max_range = max(col_ranges.values())
|
| 677 |
-
min_range = min(col_ranges.values())
|
| 678 |
-
|
| 679 |
-
if max_range > 0 and min_range > 0 and (max_range / min_range) > 100:
|
| 680 |
-
# Escalas muito diferentes - usar eixos duplos
|
| 681 |
-
logging.info("[GRAPH_GENERATION] 📊 Escalas diferentes, usando eixos duplos")
|
| 682 |
-
return await _generate_dual_axis_chart(df_clean, title, colors, x_col, y_cols[0], y_cols[1])
|
| 683 |
-
|
| 684 |
-
# Gráfico agrupado normal
|
| 685 |
-
x_pos = np.arange(len(df_clean))
|
| 686 |
-
width = 0.8 / len(y_cols)
|
| 687 |
-
|
| 688 |
-
fig, ax = plt.subplots(figsize=(14, 8))
|
| 689 |
-
|
| 690 |
-
for i, col in enumerate(y_cols):
|
| 691 |
-
offset = width * i - width * (len(y_cols) - 1) / 2
|
| 692 |
-
bars = ax.bar(x_pos + offset, df_clean[col], width, label=col,
|
| 693 |
-
color=colors[i % len(colors)], alpha=0.8)
|
| 694 |
-
|
| 695 |
-
# Adicionar valores nas barras
|
| 696 |
-
for bar in bars:
|
| 697 |
-
height = bar.get_height()
|
| 698 |
-
if height > 0:
|
| 699 |
-
ax.text(bar.get_x() + bar.get_width()/2., height + height * 0.02,
|
| 700 |
-
f'{height:.0f}', ha='center', fontsize=8)
|
| 701 |
-
|
| 702 |
-
ax.set_xlabel(x_col)
|
| 703 |
-
ax.set_ylabel('Valores')
|
| 704 |
-
ax.set_title(title or f"Comparação de {', '.join(y_cols)} por {x_col}")
|
| 705 |
-
ax.set_xticks(x_pos)
|
| 706 |
-
ax.set_xticklabels(df_clean[x_col], rotation=45, ha='right')
|
| 707 |
-
ax.legend()
|
| 708 |
-
ax.grid(True, linestyle='--', alpha=0.7, axis='y')
|
| 709 |
-
plt.tight_layout()
|
| 710 |
-
|
| 711 |
-
logging.info(f"[GRAPH_GENERATION] ✅ Gráfico agrupado tradicional criado: {len(y_cols)} métricas")
|
| 712 |
-
return save_plot_to_image()
|
| 713 |
-
|
| 714 |
-
async def _generate_color_grouped_bars(df: pd.DataFrame, title: str, colors, categorical_cols: list, y_col: str) -> Optional[Image.Image]:
|
| 715 |
-
"""
|
| 716 |
-
Gera gráfico agrupado por cor usando múltiplas categóricas (CENÁRIO CRÍTICO)
|
| 717 |
-
"""
|
| 718 |
-
x_col = categorical_cols[0]
|
| 719 |
-
group_col = categorical_cols[1] if len(categorical_cols) > 1 else None
|
| 720 |
-
|
| 721 |
-
logging.info(f"[GRAPH_GENERATION] 🎨 Agrupamento por cor: {x_col} (X) + {y_col} (Y) + {group_col} (cor)")
|
| 722 |
-
|
| 723 |
-
if not group_col:
|
| 724 |
-
logging.warning("[GRAPH_GENERATION] ⚠️ Sem coluna para agrupamento, usando gráfico simples")
|
| 725 |
-
return await generate_bar_vertical(df[[x_col, y_col]], title, colors)
|
| 726 |
-
|
| 727 |
-
# Converter coluna numérica se necessário
|
| 728 |
-
if df[y_col].dtype == 'object':
|
| 729 |
-
df[y_col] = pd.to_numeric(df[y_col].astype(str).str.replace(',', '.'), errors='coerce')
|
| 730 |
-
|
| 731 |
-
# Remover linhas com valores NaN
|
| 732 |
-
df_clean = df.dropna(subset=[y_col])
|
| 733 |
-
|
| 734 |
-
if df_clean.empty:
|
| 735 |
-
logging.error("[GRAPH_GENERATION] ❌ Todos os valores são NaN após conversão")
|
| 736 |
-
return None
|
| 737 |
-
|
| 738 |
-
# Obter categorias únicas
|
| 739 |
-
unique_groups = df_clean[group_col].unique()
|
| 740 |
-
unique_x = df_clean[x_col].unique()
|
| 741 |
-
|
| 742 |
-
logging.info(f"[GRAPH_GENERATION] 🎯 Grupos: {unique_groups} | X: {len(unique_x)} categorias")
|
| 743 |
-
|
| 744 |
-
# Configurar gráfico
|
| 745 |
-
x_pos = np.arange(len(unique_x))
|
| 746 |
-
width = 0.8 / len(unique_groups)
|
| 747 |
-
|
| 748 |
-
fig, ax = plt.subplots(figsize=(14, 8))
|
| 749 |
-
|
| 750 |
-
# Criar barras para cada grupo
|
| 751 |
-
for i, group in enumerate(unique_groups):
|
| 752 |
-
group_data = df_clean[df_clean[group_col] == group]
|
| 753 |
-
|
| 754 |
-
# Criar array de valores para cada posição X
|
| 755 |
-
values = []
|
| 756 |
-
for x_val in unique_x:
|
| 757 |
-
matching_rows = group_data[group_data[x_col] == x_val]
|
| 758 |
-
if not matching_rows.empty:
|
| 759 |
-
values.append(matching_rows[y_col].iloc[0])
|
| 760 |
-
else:
|
| 761 |
-
values.append(0)
|
| 762 |
-
|
| 763 |
-
# Calcular posição das barras
|
| 764 |
-
offset = width * i - width * (len(unique_groups) - 1) / 2
|
| 765 |
-
bars = ax.bar(x_pos + offset, values, width, label=f"{group_col}: {group}",
|
| 766 |
-
color=colors[i % len(colors)], alpha=0.8)
|
| 767 |
-
|
| 768 |
-
# Adicionar valores nas barras
|
| 769 |
-
for bar, value in zip(bars, values):
|
| 770 |
-
if value > 0:
|
| 771 |
-
ax.text(bar.get_x() + bar.get_width()/2., value + value * 0.02,
|
| 772 |
-
f'{value:.0f}', ha='center', fontsize=8)
|
| 773 |
-
|
| 774 |
-
# Configurações do gráfico
|
| 775 |
-
ax.set_xlabel(x_col)
|
| 776 |
-
ax.set_ylabel(y_col)
|
| 777 |
-
ax.set_title(title or f"{y_col} por {x_col} (agrupado por {group_col})")
|
| 778 |
-
ax.set_xticks(x_pos)
|
| 779 |
-
ax.set_xticklabels(unique_x, rotation=45, ha='right')
|
| 780 |
-
ax.legend(bbox_to_anchor=(1.05, 1), loc='upper left')
|
| 781 |
-
ax.grid(True, linestyle='--', alpha=0.7, axis='y')
|
| 782 |
-
plt.tight_layout()
|
| 783 |
-
|
| 784 |
-
logging.info(f"[GRAPH_GENERATION] ✅ Gráfico agrupado por cor criado: {len(unique_groups)} grupos")
|
| 785 |
-
return save_plot_to_image()
|
| 786 |
-
|
| 787 |
-
async def _generate_dual_axis_chart(df: pd.DataFrame, title: str, colors, x_col: str, y1_col: str, y2_col: str) -> Optional[Image.Image]:
|
| 788 |
-
"""
|
| 789 |
-
Gera gráfico com eixos duplos para métricas com escalas diferentes
|
| 790 |
-
"""
|
| 791 |
-
logging.info(f"[GRAPH_GENERATION] 📊 Eixos duplos: {y1_col} (esq) + {y2_col} (dir)")
|
| 792 |
-
|
| 793 |
-
fig, ax1 = plt.subplots(figsize=(14, 8))
|
| 794 |
-
|
| 795 |
-
# Primeiro eixo Y (esquerda)
|
| 796 |
-
x_pos = np.arange(len(df))
|
| 797 |
-
width = 0.35
|
| 798 |
-
|
| 799 |
-
bars1 = ax1.bar(x_pos - width/2, df[y1_col], width, label=y1_col,
|
| 800 |
-
color=colors[0], alpha=0.8)
|
| 801 |
-
ax1.set_xlabel(x_col)
|
| 802 |
-
ax1.set_ylabel(y1_col, color=colors[0])
|
| 803 |
-
ax1.tick_params(axis='y', labelcolor=colors[0])
|
| 804 |
-
|
| 805 |
-
# Segundo eixo Y (direita)
|
| 806 |
-
ax2 = ax1.twinx()
|
| 807 |
-
bars2 = ax2.bar(x_pos + width/2, df[y2_col], width, label=y2_col,
|
| 808 |
-
color=colors[1], alpha=0.8)
|
| 809 |
-
ax2.set_ylabel(y2_col, color=colors[1])
|
| 810 |
-
ax2.tick_params(axis='y', labelcolor=colors[1])
|
| 811 |
-
|
| 812 |
-
# Configurações comuns
|
| 813 |
-
ax1.set_xticks(x_pos)
|
| 814 |
-
ax1.set_xticklabels(df[x_col], rotation=45, ha='right')
|
| 815 |
-
ax1.grid(True, linestyle='--', alpha=0.7, axis='y')
|
| 816 |
-
|
| 817 |
-
# Adicionar valores nas barras
|
| 818 |
-
for bar in bars1:
|
| 819 |
-
height = bar.get_height()
|
| 820 |
-
if height > 0:
|
| 821 |
-
ax1.text(bar.get_x() + bar.get_width()/2., height + height * 0.02,
|
| 822 |
-
f'{height:.0f}', ha='center', fontsize=8)
|
| 823 |
-
|
| 824 |
-
for bar in bars2:
|
| 825 |
-
height = bar.get_height()
|
| 826 |
-
if height > 0:
|
| 827 |
-
ax2.text(bar.get_x() + bar.get_width()/2., height + height * 0.02,
|
| 828 |
-
f'{height:.0f}', ha='center', fontsize=8)
|
| 829 |
-
|
| 830 |
-
plt.title(title or f"{y1_col} e {y2_col} por {x_col}")
|
| 831 |
-
plt.tight_layout()
|
| 832 |
-
|
| 833 |
-
logging.info(f"[GRAPH_GENERATION] ✅ Gráfico com eixos duplos criado: {y1_col} + {y2_col}")
|
| 834 |
-
return save_plot_to_image()
|
| 835 |
-
|
| 836 |
-
# Função removida - substituída pela nova lógica unificada
|
| 837 |
-
|
| 838 |
-
# Função removida - substituída pela nova lógica unificada em _generate_color_grouped_bars()
|
| 839 |
-
|
| 840 |
-
async def generate_bar_stacked(df: pd.DataFrame, title: str, colors) -> Optional[Image.Image]:
|
| 841 |
-
"""Gera gráfico de barras empilhadas"""
|
| 842 |
-
if len(df.columns) < 3:
|
| 843 |
-
return await generate_bar_vertical(df, title, colors)
|
| 844 |
-
|
| 845 |
-
x_col = df.columns[0]
|
| 846 |
-
y_cols = [col for col in df.columns[1:] if pd.api.types.is_numeric_dtype(df[col])]
|
| 847 |
-
|
| 848 |
-
if not y_cols:
|
| 849 |
-
return await generate_bar_vertical(df, title, colors)
|
| 850 |
-
|
| 851 |
-
fig, ax = plt.subplots(figsize=(12, 8))
|
| 852 |
-
bottom = np.zeros(len(df))
|
| 853 |
-
|
| 854 |
-
for i, col in enumerate(y_cols):
|
| 855 |
-
bars = ax.bar(range(len(df)), df[col], bottom=bottom, label=col, color=colors[i % len(colors)])
|
| 856 |
-
|
| 857 |
-
# Adicionar valores nas barras
|
| 858 |
-
for j, bar in enumerate(bars):
|
| 859 |
-
height = bar.get_height()
|
| 860 |
-
if isinstance(height, (int, float)) and height > 0:
|
| 861 |
-
ax.text(bar.get_x() + bar.get_width()/2., bottom[j] + height/2,
|
| 862 |
-
f'{height:.2f}', ha='center', va='center', fontsize=8, color='white')
|
| 863 |
-
|
| 864 |
-
bottom += df[col].fillna(0)
|
| 865 |
-
|
| 866 |
-
ax.set_xlabel(x_col)
|
| 867 |
-
ax.set_ylabel('Valores')
|
| 868 |
-
ax.set_title(title or f"Distribuição por {x_col}")
|
| 869 |
-
ax.set_xticks(range(len(df)))
|
| 870 |
-
ax.set_xticklabels(df[x_col], rotation=45, ha='right')
|
| 871 |
-
ax.legend()
|
| 872 |
-
plt.tight_layout()
|
| 873 |
-
|
| 874 |
-
return save_plot_to_image()
|
| 875 |
-
|
| 876 |
-
async def generate_pie(df: pd.DataFrame, title: str, colors) -> Optional[Image.Image]:
|
| 877 |
-
"""Gera gráfico de pizza"""
|
| 878 |
-
if len(df.columns) < 2:
|
| 879 |
-
return None
|
| 880 |
-
|
| 881 |
-
label_col, value_col = df.columns[0], df.columns[1]
|
| 882 |
-
|
| 883 |
-
# Preparar dados numéricos - converter strings com vírgula para float
|
| 884 |
-
df_plot = df.copy()
|
| 885 |
-
try:
|
| 886 |
-
if df_plot[value_col].dtype == 'object':
|
| 887 |
-
# Converte strings para números, tratando vírgulas como separador decimal
|
| 888 |
-
df_plot[value_col] = pd.to_numeric(df_plot[value_col].astype(str).str.replace(',', '.'), errors='coerce')
|
| 889 |
-
|
| 890 |
-
# Remove linhas com valores não numéricos, negativos ou zero
|
| 891 |
-
df_plot = df_plot.dropna(subset=[value_col])
|
| 892 |
-
df_plot = df_plot[df_plot[value_col] > 0]
|
| 893 |
-
|
| 894 |
-
if df_plot.empty:
|
| 895 |
-
logging.error(f"[GRAPH_GENERATION] Nenhum valor numérico positivo encontrado na coluna {value_col}")
|
| 896 |
-
return await generate_bar_vertical(df, title, colors)
|
| 897 |
-
|
| 898 |
-
except Exception as e:
|
| 899 |
-
logging.error(f"[GRAPH_GENERATION] Erro ao converter dados para numérico: {e}")
|
| 900 |
-
return await generate_bar_vertical(df, title, colors)
|
| 901 |
-
|
| 902 |
-
plt.figure(figsize=(10, 10))
|
| 903 |
-
|
| 904 |
-
# Calcular percentuais para os rótulos
|
| 905 |
-
total = df_plot[value_col].sum()
|
| 906 |
-
labels = [f'{label} ({val:,.0f}, {val/total:.1%})' for label, val in zip(df_plot[label_col], df_plot[value_col])]
|
| 907 |
-
|
| 908 |
-
plt.pie(df_plot[value_col], labels=labels, autopct='%1.1f%%',
|
| 909 |
-
startangle=90, shadow=False, colors=colors[:len(df_plot)])
|
| 910 |
-
|
| 911 |
-
plt.axis('equal')
|
| 912 |
-
plt.title(title or f"Distribuição de {value_col} por {label_col}")
|
| 913 |
-
plt.tight_layout()
|
| 914 |
-
|
| 915 |
-
return save_plot_to_image()
|
| 916 |
-
|
| 917 |
-
async def generate_donut(df: pd.DataFrame, title: str, colors) -> Optional[Image.Image]:
|
| 918 |
-
"""Gera gráfico de donut"""
|
| 919 |
-
if len(df.columns) < 2:
|
| 920 |
-
return None
|
| 921 |
-
|
| 922 |
-
label_col, value_col = df.columns[0], df.columns[1]
|
| 923 |
-
|
| 924 |
-
# Preparar dados numéricos - converter strings com vírgula para float
|
| 925 |
-
df_plot = df.copy()
|
| 926 |
-
try:
|
| 927 |
-
if df_plot[value_col].dtype == 'object':
|
| 928 |
-
# Converte strings para números, tratando vírgulas como separador decimal
|
| 929 |
-
df_plot[value_col] = pd.to_numeric(df_plot[value_col].astype(str).str.replace(',', '.'), errors='coerce')
|
| 930 |
-
|
| 931 |
-
# Remove linhas com valores não numéricos, negativos ou zero
|
| 932 |
-
df_plot = df_plot.dropna(subset=[value_col])
|
| 933 |
-
df_plot = df_plot[df_plot[value_col] > 0]
|
| 934 |
-
|
| 935 |
-
if df_plot.empty:
|
| 936 |
-
logging.error(f"[GRAPH_GENERATION] Nenhum valor numérico positivo encontrado na coluna {value_col}")
|
| 937 |
-
return await generate_bar_vertical(df, title, colors)
|
| 938 |
-
|
| 939 |
-
except Exception as e:
|
| 940 |
-
logging.error(f"[GRAPH_GENERATION] Erro ao converter dados para numérico: {e}")
|
| 941 |
-
return await generate_bar_vertical(df, title, colors)
|
| 942 |
-
|
| 943 |
-
plt.figure(figsize=(10, 10))
|
| 944 |
-
|
| 945 |
-
# Calcular percentuais para os rótulos
|
| 946 |
-
total = df_plot[value_col].sum()
|
| 947 |
-
labels = [f'{label} ({val:,.0f}, {val/total:.1%})' for label, val in zip(df_plot[label_col], df_plot[value_col])]
|
| 948 |
-
|
| 949 |
-
# Criar gráfico de donut (pizza com círculo central)
|
| 950 |
-
plt.pie(df_plot[value_col], labels=labels, autopct='%1.1f%%',
|
| 951 |
-
startangle=90, shadow=False, colors=colors[:len(df_plot)],
|
| 952 |
-
wedgeprops=dict(width=0.5)) # Largura do anel
|
| 953 |
-
|
| 954 |
-
plt.axis('equal')
|
| 955 |
-
plt.title(title or f"Distribuição de {value_col} por {label_col}")
|
| 956 |
-
plt.tight_layout()
|
| 957 |
-
|
| 958 |
-
return save_plot_to_image()
|
| 959 |
-
|
| 960 |
-
async def generate_pie_multiple(df: pd.DataFrame, title: str, colors) -> Optional[Image.Image]:
|
| 961 |
-
"""Gera múltiplos gráficos de pizza"""
|
| 962 |
-
if len(df.columns) < 3:
|
| 963 |
-
return await generate_pie(df, title, colors)
|
| 964 |
-
|
| 965 |
-
cat1, cat2, val_col = df.columns[0], df.columns[1], df.columns[2]
|
| 966 |
-
|
| 967 |
-
# Verificar se o valor é numérico
|
| 968 |
-
if not pd.api.types.is_numeric_dtype(df[val_col]):
|
| 969 |
-
return await generate_bar_grouped(df, title, colors)
|
| 970 |
-
|
| 971 |
-
# Agrupar dados
|
| 972 |
-
grouped = df.groupby([cat1, cat2])[val_col].sum().unstack().fillna(0)
|
| 973 |
-
|
| 974 |
-
# Determinar layout da grade
|
| 975 |
-
n_groups = len(grouped)
|
| 976 |
-
if n_groups == 0:
|
| 977 |
-
return None
|
| 978 |
-
|
| 979 |
-
cols = min(3, n_groups) # Máximo 3 colunas
|
| 980 |
-
rows = (n_groups + cols - 1) // cols # Arredondar para cima
|
| 981 |
-
|
| 982 |
-
# Criar subplots
|
| 983 |
-
fig, axes = plt.subplots(rows, cols, figsize=(15, 5 * rows))
|
| 984 |
-
if rows == 1 and cols == 1:
|
| 985 |
-
axes = np.array([axes]) # Garantir que axes seja um array
|
| 986 |
-
axes = axes.flatten()
|
| 987 |
-
|
| 988 |
-
# Plotar cada pizza
|
| 989 |
-
for i, (group_name, group_data) in enumerate(grouped.iterrows()):
|
| 990 |
-
if i < len(axes):
|
| 991 |
-
# Remover valores zero
|
| 992 |
-
data = group_data[group_data > 0]
|
| 993 |
-
|
| 994 |
-
if not data.empty:
|
| 995 |
-
# Calcular percentuais
|
| 996 |
-
total = data.sum()
|
| 997 |
-
|
| 998 |
-
# Criar rótulos com valores e percentuais
|
| 999 |
-
labels = [f'{idx} ({val:.2f}, {val/total:.1%})' for idx, val in data.items()]
|
| 1000 |
-
|
| 1001 |
-
# Plotar pizza
|
| 1002 |
-
axes[i].pie(data, labels=labels, autopct='%1.1f%%',
|
| 1003 |
-
startangle=90, colors=colors[:len(data)])
|
| 1004 |
-
axes[i].set_title(f"{group_name}")
|
| 1005 |
-
axes[i].axis('equal')
|
| 1006 |
-
|
| 1007 |
-
# Esconder eixos não utilizados
|
| 1008 |
-
for j in range(i + 1, len(axes)):
|
| 1009 |
-
axes[j].axis('off')
|
| 1010 |
-
|
| 1011 |
-
plt.suptitle(title or f"Distribuição de {val_col} por {cat2} para cada {cat1}", fontsize=16)
|
| 1012 |
-
plt.tight_layout()
|
| 1013 |
-
plt.subplots_adjust(top=0.9)
|
| 1014 |
-
|
| 1015 |
-
return save_plot_to_image()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
nodes/graph_selection_node.py
DELETED
|
@@ -1,147 +0,0 @@
|
|
| 1 |
-
"""
|
| 2 |
-
Nó para seleção do tipo de gráfico usando LLM - REFATORADO COMPLETO
|
| 3 |
-
"""
|
| 4 |
-
import logging
|
| 5 |
-
import re
|
| 6 |
-
import pandas as pd
|
| 7 |
-
from typing import Dict, Any, Optional
|
| 8 |
-
|
| 9 |
-
from agents.tools import (
|
| 10 |
-
generate_graph_type_context,
|
| 11 |
-
extract_sql_query_from_response
|
| 12 |
-
)
|
| 13 |
-
from utils.config import OPENAI_API_KEY
|
| 14 |
-
from langchain_openai import ChatOpenAI
|
| 15 |
-
from utils.object_manager import get_object_manager
|
| 16 |
-
|
| 17 |
-
# Mapeamento DIRETO no arquivo para evitar problemas externos
|
| 18 |
-
GRAPH_TYPE_MAPPING = {
|
| 19 |
-
"1": "line_simple",
|
| 20 |
-
"2": "multiline",
|
| 21 |
-
"3": "area",
|
| 22 |
-
"4": "bar_vertical",
|
| 23 |
-
"5": "bar_horizontal",
|
| 24 |
-
"6": "bar_grouped",
|
| 25 |
-
"7": "bar_stacked",
|
| 26 |
-
"8": "pie",
|
| 27 |
-
"9": "donut",
|
| 28 |
-
"10": "pie_multiple"
|
| 29 |
-
}
|
| 30 |
-
|
| 31 |
-
async def graph_selection_node(state: Dict[str, Any]) -> Dict[str, Any]:
|
| 32 |
-
"""
|
| 33 |
-
Nó REFATORADO para seleção do tipo de gráfico usando LLM
|
| 34 |
-
"""
|
| 35 |
-
logging.info("[GRAPH_SELECTION_NEW] 🚀 Iniciando seleção REFATORADA")
|
| 36 |
-
|
| 37 |
-
try:
|
| 38 |
-
# 1. Verificações básicas
|
| 39 |
-
if state.get("query_type") != "sql_query_graphic":
|
| 40 |
-
logging.info("[GRAPH_SELECTION_NEW] Query não requer gráfico")
|
| 41 |
-
return state
|
| 42 |
-
|
| 43 |
-
# 2. Obter SQL query
|
| 44 |
-
sql_query = state.get("sql_query_extracted")
|
| 45 |
-
if not sql_query:
|
| 46 |
-
sql_query = extract_sql_query_from_response(state.get("response", ""))
|
| 47 |
-
|
| 48 |
-
if not sql_query:
|
| 49 |
-
logging.error("[GRAPH_SELECTION_NEW] ❌ SQL query não encontrada")
|
| 50 |
-
state.update({"graph_error": "SQL query não encontrada", "graph_generated": False})
|
| 51 |
-
return state
|
| 52 |
-
|
| 53 |
-
# 3. Obter dados
|
| 54 |
-
obj_manager = get_object_manager()
|
| 55 |
-
engine = obj_manager.get_engine(state.get("engine_id"))
|
| 56 |
-
if not engine:
|
| 57 |
-
logging.error("[GRAPH_SELECTION_NEW] ❌ Engine não encontrada")
|
| 58 |
-
state.update({"graph_error": "Engine não encontrada", "graph_generated": False})
|
| 59 |
-
return state
|
| 60 |
-
|
| 61 |
-
# 4. Executar query
|
| 62 |
-
try:
|
| 63 |
-
df_result = pd.read_sql_query(sql_query, engine)
|
| 64 |
-
if df_result.empty:
|
| 65 |
-
logging.error("[GRAPH_SELECTION_NEW] ❌ Dados vazios")
|
| 66 |
-
state.update({"graph_error": "Dados vazios", "graph_generated": False})
|
| 67 |
-
return state
|
| 68 |
-
except Exception as e:
|
| 69 |
-
logging.error(f"[GRAPH_SELECTION_NEW] ❌ Erro na query: {e}")
|
| 70 |
-
state.update({"graph_error": f"Erro na query: {e}", "graph_generated": False})
|
| 71 |
-
return state
|
| 72 |
-
|
| 73 |
-
# 5. Preparar contexto
|
| 74 |
-
user_query = state.get("user_input", "")
|
| 75 |
-
df_sample = df_result.head(3)
|
| 76 |
-
graph_context = generate_graph_type_context(user_query, sql_query, df_result.columns.tolist(), df_sample)
|
| 77 |
-
|
| 78 |
-
# 6. Chamar LLM de forma LIMPA
|
| 79 |
-
graph_type = await call_llm_for_graph_selection(graph_context, user_query)
|
| 80 |
-
|
| 81 |
-
logging.error(f"🎯 [RESULTADO_FINAL] Tipo selecionado: '{graph_type}'")
|
| 82 |
-
|
| 83 |
-
# 7. Armazenar resultado
|
| 84 |
-
graph_data_id = obj_manager.store_object(df_result, "graph_data")
|
| 85 |
-
state.update({
|
| 86 |
-
"graph_type": graph_type,
|
| 87 |
-
"graph_data": {
|
| 88 |
-
"data_id": graph_data_id,
|
| 89 |
-
"columns": df_result.columns.tolist(),
|
| 90 |
-
"rows": len(df_result),
|
| 91 |
-
"sample": df_sample.to_dict()
|
| 92 |
-
},
|
| 93 |
-
"graph_error": None
|
| 94 |
-
})
|
| 95 |
-
|
| 96 |
-
return state
|
| 97 |
-
|
| 98 |
-
except Exception as e:
|
| 99 |
-
logging.error(f"[GRAPH_SELECTION_NEW] ❌ Erro geral: {e}")
|
| 100 |
-
state.update({"graph_error": f"Erro geral: {e}", "graph_generated": False})
|
| 101 |
-
return state
|
| 102 |
-
|
| 103 |
-
async def call_llm_for_graph_selection(graph_context: str, user_query: str) -> str:
|
| 104 |
-
"""
|
| 105 |
-
Função NOVA e LIMPA para chamar LLM sem interferências
|
| 106 |
-
"""
|
| 107 |
-
logging.error("🔥 [LLM_CALL] Iniciando chamada LIMPA da LLM")
|
| 108 |
-
|
| 109 |
-
# Verificação básica
|
| 110 |
-
if not OPENAI_API_KEY:
|
| 111 |
-
logging.error("🔥 [LLM_CALL] OpenAI não configurada")
|
| 112 |
-
return "line_simple"
|
| 113 |
-
|
| 114 |
-
try:
|
| 115 |
-
# Criar LLM com configuração limpa
|
| 116 |
-
llm = ChatOpenAI(
|
| 117 |
-
model="gpt-4o",
|
| 118 |
-
temperature=0,
|
| 119 |
-
max_tokens=5,
|
| 120 |
-
timeout=30
|
| 121 |
-
)
|
| 122 |
-
|
| 123 |
-
# Log do contexto
|
| 124 |
-
logging.error("🔥 [LLM_CALL] Contexto enviado:")
|
| 125 |
-
logging.error(f"'{graph_context}...'")
|
| 126 |
-
|
| 127 |
-
# Agora a pergunta real
|
| 128 |
-
real_response = llm.invoke(graph_context)
|
| 129 |
-
real_content = real_response.content.strip()
|
| 130 |
-
|
| 131 |
-
logging.error(f"🔥 [LLM_CALL] Resposta REAL: '{real_content}'")
|
| 132 |
-
|
| 133 |
-
# Extrair número da resposta
|
| 134 |
-
number_match = re.search(r'\b([1-9]|10)\b', real_content)
|
| 135 |
-
if number_match:
|
| 136 |
-
number = number_match.group(0)
|
| 137 |
-
graph_type = GRAPH_TYPE_MAPPING.get(number, "line_simple")
|
| 138 |
-
logging.error(f"🔥 [LLM_CALL] Número: {number} → Tipo: {graph_type}")
|
| 139 |
-
return graph_type
|
| 140 |
-
else:
|
| 141 |
-
logging.error(f"🔥 [LLM_CALL] Número não encontrado em: '{real_content}'")
|
| 142 |
-
return "line_simple"
|
| 143 |
-
|
| 144 |
-
except Exception as e:
|
| 145 |
-
logging.error(f"🔥 [LLM_CALL] ERRO: {e}")
|
| 146 |
-
return "line_simple"
|
| 147 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
nodes/query_node.py
DELETED
|
@@ -1,232 +0,0 @@
|
|
| 1 |
-
"""
|
| 2 |
-
Nó para processamento de consultas SQL
|
| 3 |
-
"""
|
| 4 |
-
import time
|
| 5 |
-
import logging
|
| 6 |
-
import pandas as pd
|
| 7 |
-
from typing import Dict, Any, TypedDict
|
| 8 |
-
|
| 9 |
-
from agents.tools import is_greeting, detect_query_type, prepare_sql_context
|
| 10 |
-
from agents.sql_agent import SQLAgentManager
|
| 11 |
-
from utils.object_manager import get_object_manager
|
| 12 |
-
|
| 13 |
-
class QueryState(TypedDict):
|
| 14 |
-
"""Estado para processamento de consultas"""
|
| 15 |
-
user_input: str
|
| 16 |
-
selected_model: str
|
| 17 |
-
response: str
|
| 18 |
-
execution_time: float
|
| 19 |
-
error: str
|
| 20 |
-
intermediate_steps: list
|
| 21 |
-
llama_instruction: str
|
| 22 |
-
sql_result: dict
|
| 23 |
-
|
| 24 |
-
async def process_user_query_node(state: Dict[str, Any]) -> Dict[str, Any]:
|
| 25 |
-
"""
|
| 26 |
-
Nó principal para processar consulta do usuário
|
| 27 |
-
|
| 28 |
-
Args:
|
| 29 |
-
state: Estado atual com entrada do usuário
|
| 30 |
-
|
| 31 |
-
Returns:
|
| 32 |
-
Estado atualizado com resposta processada
|
| 33 |
-
"""
|
| 34 |
-
start_time = time.time()
|
| 35 |
-
user_input = state["user_input"]
|
| 36 |
-
selected_model = state["selected_model"]
|
| 37 |
-
|
| 38 |
-
logging.info(f"[QUERY] Processando: {user_input[:50]}...")
|
| 39 |
-
|
| 40 |
-
try:
|
| 41 |
-
# Verifica se é saudação
|
| 42 |
-
if is_greeting(user_input):
|
| 43 |
-
greeting_response = "Olá! Estou aqui para ajudar com suas consultas. Pergunte algo relacionado aos dados carregados no agente!"
|
| 44 |
-
state.update({
|
| 45 |
-
"response": greeting_response,
|
| 46 |
-
"execution_time": time.time() - start_time,
|
| 47 |
-
"error": None
|
| 48 |
-
})
|
| 49 |
-
return state
|
| 50 |
-
|
| 51 |
-
# Recupera objetos necessários
|
| 52 |
-
obj_manager = get_object_manager()
|
| 53 |
-
|
| 54 |
-
# Recupera cache manager
|
| 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")
|
| 63 |
-
state.update({
|
| 64 |
-
"response": cached_response,
|
| 65 |
-
"execution_time": time.time() - start_time,
|
| 66 |
-
"error": None
|
| 67 |
-
})
|
| 68 |
-
return state
|
| 69 |
-
|
| 70 |
-
# Converte amostra do banco para DataFrame
|
| 71 |
-
db_sample_dict = state.get("db_sample_dict", {})
|
| 72 |
-
if not db_sample_dict:
|
| 73 |
-
raise ValueError("Amostra do banco não disponível")
|
| 74 |
-
|
| 75 |
-
# Reconstrói DataFrame da amostra
|
| 76 |
-
db_sample = pd.DataFrame(db_sample_dict.get("data", []))
|
| 77 |
-
if db_sample.empty:
|
| 78 |
-
raise ValueError("Dados de amostra vazios")
|
| 79 |
-
|
| 80 |
-
# Detecta tipo de query e prepara contexto
|
| 81 |
-
query_type = detect_query_type(user_input)
|
| 82 |
-
state["query_type"] = query_type
|
| 83 |
-
|
| 84 |
-
if query_type in ['sql_query', 'sql_query_graphic']:
|
| 85 |
-
# Prepara contexto para envio direto ao agentSQL
|
| 86 |
-
sql_context = prepare_sql_context(user_input, db_sample)
|
| 87 |
-
state["sql_context"] = sql_context
|
| 88 |
-
|
| 89 |
-
logging.info(f"[DEBUG] Tipo de query detectado: {query_type}")
|
| 90 |
-
logging.info(f"[DEBUG] Contexto preparado para agentSQL:\n{sql_context}\n")
|
| 91 |
-
else:
|
| 92 |
-
# Para tipos futuros (prediction)
|
| 93 |
-
error_msg = f"Tipo de query '{query_type}' ainda não implementado."
|
| 94 |
-
state.update({
|
| 95 |
-
"error": error_msg,
|
| 96 |
-
"response": error_msg,
|
| 97 |
-
"execution_time": time.time() - start_time
|
| 98 |
-
})
|
| 99 |
-
return state
|
| 100 |
-
|
| 101 |
-
# Recupera agente SQL
|
| 102 |
-
agent_id = state.get("agent_id")
|
| 103 |
-
if not agent_id:
|
| 104 |
-
raise ValueError("ID do agente SQL não encontrado")
|
| 105 |
-
|
| 106 |
-
sql_agent = obj_manager.get_sql_agent(agent_id)
|
| 107 |
-
if not sql_agent:
|
| 108 |
-
raise ValueError("Agente SQL não encontrado")
|
| 109 |
-
|
| 110 |
-
# Executa query no agente SQL com contexto direto
|
| 111 |
-
sql_result = await sql_agent.execute_query(state["sql_context"])
|
| 112 |
-
|
| 113 |
-
if not sql_result["success"]:
|
| 114 |
-
state.update({
|
| 115 |
-
"error": sql_result["output"],
|
| 116 |
-
"response": sql_result["output"],
|
| 117 |
-
"sql_result": sql_result
|
| 118 |
-
})
|
| 119 |
-
else:
|
| 120 |
-
# Captura query SQL do resultado do agente
|
| 121 |
-
sql_query_captured = sql_result.get("sql_query")
|
| 122 |
-
|
| 123 |
-
state.update({
|
| 124 |
-
"response": sql_result["output"],
|
| 125 |
-
"intermediate_steps": sql_result["intermediate_steps"],
|
| 126 |
-
"sql_result": sql_result,
|
| 127 |
-
"sql_query_extracted": sql_query_captured, # ← Query SQL capturada
|
| 128 |
-
"error": None
|
| 129 |
-
})
|
| 130 |
-
|
| 131 |
-
# Log apenas se não foi capturada (caso de erro)
|
| 132 |
-
if not sql_query_captured:
|
| 133 |
-
logging.warning("[QUERY] ⚠️ Nenhuma query SQL foi capturada pelo handler")
|
| 134 |
-
|
| 135 |
-
# Armazena no cache se disponível
|
| 136 |
-
if cache_manager and sql_result["success"]:
|
| 137 |
-
cache_manager.cache_response(user_input, state["response"])
|
| 138 |
-
|
| 139 |
-
state["execution_time"] = time.time() - start_time
|
| 140 |
-
logging.info(f"[QUERY] Concluído em {state['execution_time']:.2f}s")
|
| 141 |
-
|
| 142 |
-
except Exception as e:
|
| 143 |
-
error_msg = f"Erro ao processar query: {e}"
|
| 144 |
-
logging.error(f"[QUERY] {error_msg}")
|
| 145 |
-
state.update({
|
| 146 |
-
"error": error_msg,
|
| 147 |
-
"response": error_msg,
|
| 148 |
-
"execution_time": time.time() - start_time
|
| 149 |
-
})
|
| 150 |
-
|
| 151 |
-
return state
|
| 152 |
-
|
| 153 |
-
async def validate_query_input_node(state: Dict[str, Any]) -> Dict[str, Any]:
|
| 154 |
-
"""
|
| 155 |
-
Nó para validar entrada da consulta
|
| 156 |
-
|
| 157 |
-
Args:
|
| 158 |
-
state: Estado com entrada do usuário
|
| 159 |
-
|
| 160 |
-
Returns:
|
| 161 |
-
Estado atualizado com validação
|
| 162 |
-
"""
|
| 163 |
-
user_input = state.get("user_input", "").strip()
|
| 164 |
-
|
| 165 |
-
if not user_input:
|
| 166 |
-
state.update({
|
| 167 |
-
"error": "Entrada vazia",
|
| 168 |
-
"response": "Por favor, digite uma pergunta.",
|
| 169 |
-
"execution_time": 0.0
|
| 170 |
-
})
|
| 171 |
-
return state
|
| 172 |
-
|
| 173 |
-
if len(user_input) > 1000:
|
| 174 |
-
state.update({
|
| 175 |
-
"error": "Entrada muito longa",
|
| 176 |
-
"response": "Pergunta muito longa. Por favor, seja mais conciso.",
|
| 177 |
-
"execution_time": 0.0
|
| 178 |
-
})
|
| 179 |
-
return state
|
| 180 |
-
|
| 181 |
-
# Validação passou
|
| 182 |
-
state["error"] = None
|
| 183 |
-
logging.info(f"[VALIDATION] Entrada validada: {len(user_input)} caracteres")
|
| 184 |
-
|
| 185 |
-
return state
|
| 186 |
-
|
| 187 |
-
async def prepare_query_context_node(state: Dict[str, Any]) -> Dict[str, Any]:
|
| 188 |
-
"""
|
| 189 |
-
Nó para preparar contexto da consulta
|
| 190 |
-
|
| 191 |
-
Args:
|
| 192 |
-
state: Estado atual
|
| 193 |
-
|
| 194 |
-
Returns:
|
| 195 |
-
Estado com contexto preparado
|
| 196 |
-
"""
|
| 197 |
-
try:
|
| 198 |
-
# Verifica se todos os componentes necessários estão disponíveis
|
| 199 |
-
required_ids = ["agent_id", "engine_id", "cache_id"]
|
| 200 |
-
missing_ids = [id_name for id_name in required_ids if not state.get(id_name)]
|
| 201 |
-
|
| 202 |
-
if missing_ids:
|
| 203 |
-
raise ValueError(f"IDs necessários não encontrados: {missing_ids}")
|
| 204 |
-
|
| 205 |
-
obj_manager = get_object_manager()
|
| 206 |
-
|
| 207 |
-
# Verifica se objetos existem
|
| 208 |
-
for id_name in required_ids:
|
| 209 |
-
obj_id = state[id_name]
|
| 210 |
-
if id_name == "agent_id":
|
| 211 |
-
obj = obj_manager.get_sql_agent(obj_id)
|
| 212 |
-
elif id_name == "engine_id":
|
| 213 |
-
obj = obj_manager.get_engine(obj_id)
|
| 214 |
-
elif id_name == "cache_id":
|
| 215 |
-
obj = obj_manager.get_cache_manager(obj_id)
|
| 216 |
-
|
| 217 |
-
if obj is None:
|
| 218 |
-
raise ValueError(f"Objeto não encontrado para {id_name}: {obj_id}")
|
| 219 |
-
|
| 220 |
-
# Contexto preparado com sucesso
|
| 221 |
-
state["context_ready"] = True
|
| 222 |
-
logging.info("[CONTEXT] Contexto da consulta preparado")
|
| 223 |
-
|
| 224 |
-
except Exception as e:
|
| 225 |
-
error_msg = f"Erro ao preparar contexto: {e}"
|
| 226 |
-
logging.error(f"[CONTEXT] {error_msg}")
|
| 227 |
-
state.update({
|
| 228 |
-
"error": error_msg,
|
| 229 |
-
"context_ready": False
|
| 230 |
-
})
|
| 231 |
-
|
| 232 |
-
return state
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
nodes/refinement_node.py
DELETED
|
@@ -1,141 +0,0 @@
|
|
| 1 |
-
"""
|
| 2 |
-
Nó para refinamento de respostas
|
| 3 |
-
"""
|
| 4 |
-
import logging
|
| 5 |
-
from typing import Dict, Any
|
| 6 |
-
|
| 7 |
-
from agents.tools import refine_response_with_llm
|
| 8 |
-
|
| 9 |
-
async def refine_response_node(state: Dict[str, Any]) -> Dict[str, Any]:
|
| 10 |
-
"""
|
| 11 |
-
Nó para refinar a resposta usando LLM adicional
|
| 12 |
-
|
| 13 |
-
Args:
|
| 14 |
-
state: Estado atual do agente
|
| 15 |
-
|
| 16 |
-
Returns:
|
| 17 |
-
Estado atualizado com resposta refinada
|
| 18 |
-
"""
|
| 19 |
-
if not state.get("advanced_mode", False) or state.get("error"):
|
| 20 |
-
# Pula refinamento se modo avançado desabilitado ou há erro
|
| 21 |
-
logging.info("[REFINE] Pulando refinamento - modo avançado desabilitado ou erro presente")
|
| 22 |
-
return state
|
| 23 |
-
|
| 24 |
-
logging.info("[REFINE] Iniciando refinamento da resposta")
|
| 25 |
-
|
| 26 |
-
try:
|
| 27 |
-
original_response = state.get("response", "")
|
| 28 |
-
user_input = state.get("user_input", "")
|
| 29 |
-
|
| 30 |
-
if not original_response or not user_input:
|
| 31 |
-
logging.warning("[REFINE] Resposta ou entrada do usuário não disponível")
|
| 32 |
-
return state
|
| 33 |
-
|
| 34 |
-
# Refina resposta com LLM adicional
|
| 35 |
-
refined_response = await refine_response_with_llm(
|
| 36 |
-
user_input,
|
| 37 |
-
original_response
|
| 38 |
-
)
|
| 39 |
-
|
| 40 |
-
# Atualiza estado com resposta refinada
|
| 41 |
-
state["response"] = refined_response
|
| 42 |
-
state["refined"] = True
|
| 43 |
-
|
| 44 |
-
logging.info("[REFINE] Resposta refinada com sucesso")
|
| 45 |
-
|
| 46 |
-
except Exception as e:
|
| 47 |
-
error_msg = f"Erro ao refinar resposta: {e}"
|
| 48 |
-
logging.error(f"[REFINE] {error_msg}")
|
| 49 |
-
# Mantém resposta original em caso de erro
|
| 50 |
-
state["refinement_error"] = error_msg
|
| 51 |
-
|
| 52 |
-
return state
|
| 53 |
-
|
| 54 |
-
async def check_refinement_quality_node(state: Dict[str, Any]) -> Dict[str, Any]:
|
| 55 |
-
"""
|
| 56 |
-
Nó para verificar qualidade do refinamento
|
| 57 |
-
|
| 58 |
-
Args:
|
| 59 |
-
state: Estado com resposta refinada
|
| 60 |
-
|
| 61 |
-
Returns:
|
| 62 |
-
Estado com avaliação da qualidade
|
| 63 |
-
"""
|
| 64 |
-
try:
|
| 65 |
-
original_response = state.get("sql_result", {}).get("output", "")
|
| 66 |
-
refined_response = state.get("response", "")
|
| 67 |
-
|
| 68 |
-
if not state.get("refined", False):
|
| 69 |
-
state["refinement_quality"] = "not_refined"
|
| 70 |
-
return state
|
| 71 |
-
|
| 72 |
-
# Métricas simples de qualidade
|
| 73 |
-
quality_metrics = {
|
| 74 |
-
"length_increase": len(refined_response) - len(original_response),
|
| 75 |
-
"has_insights": any(word in refined_response.lower() for word in [
|
| 76 |
-
"insight", "análise", "interpretação", "conclusão", "tendência"
|
| 77 |
-
]),
|
| 78 |
-
"has_statistics": any(word in refined_response.lower() for word in [
|
| 79 |
-
"média", "total", "percentual", "proporção", "estatística"
|
| 80 |
-
]),
|
| 81 |
-
"improved": len(refined_response) > len(original_response) * 1.1
|
| 82 |
-
}
|
| 83 |
-
|
| 84 |
-
# Determina qualidade geral
|
| 85 |
-
if quality_metrics["improved"] and (quality_metrics["has_insights"] or quality_metrics["has_statistics"]):
|
| 86 |
-
quality_score = "high"
|
| 87 |
-
elif quality_metrics["length_increase"] > 0:
|
| 88 |
-
quality_score = "medium"
|
| 89 |
-
else:
|
| 90 |
-
quality_score = "low"
|
| 91 |
-
|
| 92 |
-
state["refinement_quality"] = quality_score
|
| 93 |
-
state["quality_metrics"] = quality_metrics
|
| 94 |
-
|
| 95 |
-
logging.info(f"[REFINE] Qualidade avaliada: {quality_score}")
|
| 96 |
-
|
| 97 |
-
except Exception as e:
|
| 98 |
-
logging.error(f"[REFINE] Erro ao avaliar qualidade: {e}")
|
| 99 |
-
state["refinement_quality"] = "error"
|
| 100 |
-
|
| 101 |
-
return state
|
| 102 |
-
|
| 103 |
-
async def format_final_response_node(state: Dict[str, Any]) -> Dict[str, Any]:
|
| 104 |
-
"""
|
| 105 |
-
Nó para formatação final da resposta
|
| 106 |
-
|
| 107 |
-
Args:
|
| 108 |
-
state: Estado com resposta processada
|
| 109 |
-
|
| 110 |
-
Returns:
|
| 111 |
-
Estado com resposta formatada
|
| 112 |
-
"""
|
| 113 |
-
try:
|
| 114 |
-
response = state.get("response", "")
|
| 115 |
-
execution_time = state.get("execution_time", 0.0)
|
| 116 |
-
advanced_mode = state.get("advanced_mode", False)
|
| 117 |
-
refined = state.get("refined", False)
|
| 118 |
-
|
| 119 |
-
# Adiciona informações de contexto se necessário
|
| 120 |
-
if advanced_mode and refined:
|
| 121 |
-
quality = state.get("refinement_quality", "unknown")
|
| 122 |
-
if quality == "high":
|
| 123 |
-
response += "\n\n💡 *Resposta aprimorada com análise avançada*"
|
| 124 |
-
elif quality == "medium":
|
| 125 |
-
response += "\n\n🔍 *Resposta complementada*"
|
| 126 |
-
|
| 127 |
-
# Adiciona tempo de execução se significativo
|
| 128 |
-
if execution_time > 2.0:
|
| 129 |
-
response += f"\n\n⏱️ *Processado em {execution_time:.1f}s*"
|
| 130 |
-
|
| 131 |
-
# Formatação final
|
| 132 |
-
state["response"] = response.strip()
|
| 133 |
-
state["formatted"] = True
|
| 134 |
-
|
| 135 |
-
logging.info(f"[FORMAT] Resposta formatada - {len(response)} caracteres")
|
| 136 |
-
|
| 137 |
-
except Exception as e:
|
| 138 |
-
logging.error(f"[FORMAT] Erro na formatação: {e}")
|
| 139 |
-
# Mantém resposta original se houver erro na formatação
|
| 140 |
-
|
| 141 |
-
return state
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|