rwayz commited on
Commit
6d012ce
·
verified ·
1 Parent(s): 2ab9338

Delete nodes

Browse files
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