rwayz commited on
Commit
6d597f0
·
1 Parent(s): 68bf46e
.gitignore ADDED
@@ -0,0 +1,170 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Banco de dados
2
+ data.db
3
+ *.db
4
+ *.sqlite
5
+ *.sqlite3
6
+
7
+ # Arquivos CSV de upload
8
+ uploaded_data.csv
9
+ test_upload.csv
10
+ tabela.csv
11
+
12
+ # Logs
13
+ *.log
14
+ logs/
15
+ __pycache__/
16
+ *.py[cod]
17
+ *$py.class
18
+
19
+ # Distribuição / empacotamento
20
+ *.so
21
+ .Python
22
+ build/
23
+ develop-eggs/
24
+ dist/
25
+ downloads/
26
+ eggs/
27
+ .eggs/
28
+ lib/
29
+ lib64/
30
+ parts/
31
+ sdist/
32
+ var/
33
+ wheels/
34
+ pip-wheel-metadata/
35
+ share/python-wheels/
36
+ *.egg-info/
37
+ .installed.cfg
38
+ *.egg
39
+ MANIFEST
40
+
41
+ # PyInstaller
42
+ *.manifest
43
+ *.spec
44
+
45
+ # Installer logs
46
+ pip-log.txt
47
+ pip-delete-this-directory.txt
48
+
49
+ # Unit test / coverage reports
50
+ htmlcov/
51
+ .tox/
52
+ .nox/
53
+ .coverage
54
+ .coverage.*
55
+ .cache
56
+ nosetests.xml
57
+ coverage.xml
58
+ *.cover
59
+ *.py,cover
60
+ .hypothesis/
61
+ .pytest_cache/
62
+
63
+ # Translations
64
+ *.mo
65
+ *.pot
66
+
67
+ # Django stuff:
68
+ *.log
69
+ local_settings.py
70
+ db.sqlite3
71
+ db.sqlite3-journal
72
+
73
+ # Flask stuff:
74
+ instance/
75
+ .webassets-cache
76
+
77
+ # Scrapy stuff:
78
+ .scrapy
79
+
80
+ # Sphinx documentation
81
+ docs/_build/
82
+
83
+ # PyBuilder
84
+ target/
85
+
86
+ # Jupyter Notebook
87
+ .ipynb_checkpoints
88
+
89
+ # IPython
90
+ profile_default/
91
+ ipython_config.py
92
+
93
+ # pyenv
94
+ .python-version
95
+
96
+ # pipenv
97
+ Pipfile.lock
98
+
99
+ # PEP 582
100
+ __pypackages__/
101
+
102
+ # Celery stuff
103
+ celerybeat-schedule
104
+ celerybeat.pid
105
+
106
+ # SageMath parsed files
107
+ *.sage.py
108
+
109
+ # Environments
110
+ .env
111
+ .venv
112
+ env/
113
+ venv/
114
+ ENV/
115
+ env.bak/
116
+ venv.bak/
117
+
118
+ # Spyder project settings
119
+ .spyderproject
120
+ .spyproject
121
+
122
+ # Rope project settings
123
+ .ropeproject
124
+
125
+ # mkdocs documentation
126
+ /site
127
+
128
+ # mypy
129
+ .mypy_cache/
130
+ .dmypy.json
131
+ dmypy.json
132
+
133
+ # Pyre type checker
134
+ .pyre/
135
+
136
+ # IDEs
137
+ .vscode/
138
+ .idea/
139
+ *.swp
140
+ *.swo
141
+ *~
142
+
143
+ # OS
144
+ .DS_Store
145
+ .DS_Store?
146
+ ._*
147
+ .Spotlight-V100
148
+ .Trashes
149
+ ehthumbs.db
150
+ Thumbs.db
151
+
152
+ # Temporary files
153
+ *.tmp
154
+ *.temp
155
+ temp/
156
+ tmp/
157
+
158
+ # API Keys e configurações sensíveis
159
+ .env.local
160
+ .env.production
161
+ config.json
162
+ secrets.json
163
+
164
+ # Gradio
165
+ gradio_cached_examples/
166
+ flagged/
167
+
168
+ # Cache do sistema
169
+ cache/
170
+ *.cache
ARCHITECTURE.md ADDED
@@ -0,0 +1,418 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # 🏗️ AgentGraph - Arquitetura Técnica Detalhada
2
+
3
+ ## 🎯 Visão Geral
4
+
5
+ O AgentGraph é uma **plataforma multi-agente** construída com LangGraph, implementando uma arquitetura modular e extensível baseada em nós especializados. O sistema suporta múltiplos provedores LLM (OpenAI, Anthropic, HuggingFace) com processamento assíncrono, gerenciamento inteligente de objetos não-serializáveis e sistema robusto de retry para rate limiting.
6
+
7
+ ### **Principais Inovações Arquiteturais**
8
+ - 🔄 **Fluxo Otimizado**: Detecção → AgentSQL → Refinamento (sem LLM intermediária)
9
+ - 🧠 **Multi-Provedor**: Suporte nativo a OpenAI, Anthropic e HuggingFace
10
+ - 🛠️ **Tool-Calling**: Ferramentas SQL nativas with verbose ativo
11
+ - 🎛️ **Object Manager**: Solução elegante para objetos não-serializáveis
12
+ - ⚡ **Async/Await**: Processamento não-bloqueante em toda a stack
13
+ - 🔍 **LangSmith Integration**: Observabilidade completa com rastreamento automático
14
+
15
+ ## 📁 Estrutura do Projeto
16
+
17
+ ```
18
+ agentgraph/
19
+ ├── app.py # Entry point: Gradio + LangGraph
20
+ ├── graphs/
21
+ │ └── main_graph.py # StateGraph principal
22
+ ├── nodes/ # Nós especializados
23
+ │ ├── csv_processing_node.py # Processamento genérico de CSV
24
+ │ ├── database_node.py # Operações de banco de dados
25
+ │ ├── query_node.py # Processamento de consultas
26
+ │ ├── refinement_node.py # Refinamento de respostas
27
+ │ ├── cache_node.py # Gerenciamento de cache
28
+ │ ├── agent_node.py # Coordenação geral
29
+ │ └── custom_nodes.py # Nós especializados
30
+ ├── agents/
31
+ │ ├── sql_agent.py # Criação do agente SQL
32
+ │ └── tools.py # Ferramentas do agente
33
+ ├── utils/
34
+ │ ├── database.py # Funções de banco de dados
35
+ │ ├── config.py # Configurações
36
+ │ └── object_manager.py # Gerenciador de objetos não-serializáveis
37
+ ├── uploaded_data/ # Arquivos CSV enviados
38
+ ├── requirements.txt
39
+ ├── README.md
40
+ ├── ARCHITECTURE.md
41
+ └── .env
42
+ ```
43
+
44
+ ## 🔄 Fluxo do LangGraph
45
+
46
+ ### Fluxo Principal de Consulta
47
+
48
+ ```mermaid
49
+ graph TD
50
+ A[validate_input] --> B[check_cache]
51
+ B --> C{Cache Hit?}
52
+ C -->|Sim| H[update_history]
53
+ C -->|Não| D[prepare_context]
54
+ D --> E[get_db_sample]
55
+ E --> F[process_query]
56
+ F --> G{Modo Avançado?}
57
+ G -->|Sim| I[refine_response]
58
+ G -->|Não| J[cache_response]
59
+ I --> K[format_response]
60
+ K --> J
61
+ J --> H
62
+ H --> L[END]
63
+ ```
64
+
65
+ ### Nós Especializados
66
+
67
+ #### 1. **csv_processing_node.py**
68
+ - **Função**: Processamento genérico de CSV
69
+ - **Características**:
70
+ - Detecção automática de separadores (`;`, `,`, `\t`, `|`)
71
+ - Identificação inteligente de tipos de dados
72
+ - Conversão robusta para SQL types
73
+ - Estatísticas de processamento
74
+
75
+ #### 2. **database_node.py**
76
+ - **Função**: Operações de banco de dados
77
+ - **Características**:
78
+ - Criação de banco a partir de DataFrame processado
79
+ - Carregamento de banco existente
80
+ - Obtenção de amostras de dados
81
+ - Validação de integridade
82
+
83
+ #### 3. **query_node.py**
84
+ - **Função**: Processamento de consultas SQL
85
+ - **Características**:
86
+ - Validação de entrada
87
+ - Preparação de contexto
88
+ - Execução via agente SQL
89
+ - Tratamento de erros
90
+
91
+ #### 4. **refinement_node.py**
92
+ - **Função**: Refinamento de respostas
93
+ - **Características**:
94
+ - Modo avançado com LLM adicional
95
+ - Avaliação de qualidade
96
+ - Formatação final
97
+ - Adição de insights
98
+
99
+ #### 5. **cache_node.py**
100
+ - **Função**: Gerenciamento de cache e histórico
101
+ - **Características**:
102
+ - Verificação de cache
103
+ - Armazenamento de respostas
104
+ - Atualização de histórico
105
+ - Estatísticas de uso
106
+
107
+ ## 🔍 Integração LangSmith
108
+
109
+ ### **Observabilidade Automática**
110
+ O AgentGraph inclui integração completa com LangSmith para rastreamento e monitoramento:
111
+
112
+ ```python
113
+ # Configuração automática via variáveis de ambiente
114
+ LANGSMITH_TRACING=true
115
+ LANGSMITH_API_KEY=lsv2_pt_...
116
+ LANGSMITH_PROJECT=agentgraph-project
117
+
118
+ # Rastreamento automático de todo o fluxo LangGraph
119
+ workflow.invoke(state) # ← Automaticamente rastreado
120
+ ```
121
+
122
+ ### **Componentes Rastreados**
123
+ - ✅ **Todos os nós LangGraph**: validate_input → process_query → cache_response
124
+ - ✅ **Agentes SQL**: Chamadas LLM com inputs/outputs completos
125
+ - ✅ **Modelos Multi-Provedor**: OpenAI, Anthropic, HuggingFace
126
+ - ✅ **Operações de Dados**: CSV processing, database operations
127
+ - ✅ **Geração de Gráficos**: Seleção e criação de visualizações
128
+
129
+ ### **Benefícios da Integração**
130
+ - 🔍 **Debug Avançado**: Visualize fluxo completo de execução
131
+ - 📊 **Métricas de Performance**: Latência por nó e operação
132
+ - 💰 **Análise de Custos**: Uso de tokens por modelo
133
+ - 🐛 **Troubleshooting**: Identifique gargalos e erros
134
+ - 📈 **Dashboards**: Monitoramento em tempo real
135
+
136
+ ## 🧠 Gerenciador de Objetos
137
+
138
+ ### Problema Resolvido
139
+ O LangGraph requer que o estado seja serializável, mas objetos como SQLAgentManager, Engine e CacheManager não são serializáveis.
140
+
141
+ ### Solução: ObjectManager
142
+ ```python
143
+ # Armazena objetos não-serializáveis
144
+ agent_id = object_manager.store_sql_agent(sql_agent)
145
+ engine_id = object_manager.store_engine(engine)
146
+ cache_id = object_manager.store_cache_manager(cache_manager)
147
+
148
+ # Estado serializável
149
+ state = {
150
+ "user_input": "query",
151
+ "agent_id": agent_id,
152
+ "engine_id": engine_id,
153
+ "cache_id": cache_id
154
+ }
155
+
156
+ # Recupera objetos quando necessário
157
+ sql_agent = object_manager.get_sql_agent(agent_id)
158
+ ```
159
+
160
+ ## 📊 Processamento CSV Genérico
161
+
162
+ ### Detecção Automática de Tipos
163
+
164
+ ```python
165
+ # Detecta automaticamente:
166
+ - Datas: Tenta conversão com pd.to_datetime()
167
+ - Números inteiros: Verifica padrões numéricos
168
+ - Números decimais: Detecta pontos/vírgulas
169
+ - Texto: Mantém como string
170
+
171
+ # Regras de processamento:
172
+ - parse_dates: Para colunas de data
173
+ - convert_to_int: Para números inteiros
174
+ - convert_to_float: Para números decimais
175
+ - convert_text_to_int/float: Para texto numérico
176
+ - keep_as_text: Para texto puro
177
+ ```
178
+
179
+ ### Separadores Suportados
180
+ - `;` (ponto e vírgula)
181
+ - `,` (vírgula)
182
+ - `\t` (tab)
183
+ - `|` (pipe)
184
+
185
+ ## 🔧 Configurações
186
+
187
+ ### Arquivo .env
188
+ ```env
189
+ # API Keys
190
+ HUGGINGFACE_API_KEY=your_key_here
191
+ OPENAI_API_KEY=your_key_here
192
+ ANTHROPIC_API_KEY=your_key_here
193
+
194
+ # LangSmith - Observabilidade (OPCIONAL)
195
+ LANGSMITH_API_KEY=lsv2_pt_your_key_here
196
+ LANGSMITH_TRACING=true
197
+ LANGSMITH_ENDPOINT=https://api.smith.langchain.com
198
+ LANGSMITH_PROJECT=agentgraph-project
199
+
200
+ # Database Configuration
201
+ SQL_DB_PATH=data.db
202
+ DEFAULT_CSV_PATH=tabela.csv
203
+ UPLOAD_DIR=uploaded_data
204
+
205
+ # Model Configuration
206
+ DEFAULT_MODEL=GPT-4o-mini
207
+ MAX_ITERATIONS=40
208
+ TEMPERATURE=0
209
+
210
+ # Gradio Configuration
211
+ GRADIO_SHARE=False
212
+ GRADIO_PORT=7860
213
+ ```
214
+
215
+ ## 🚀 Funcionalidades
216
+
217
+ ### ✅ Mantidas do Código Original
218
+ - Múltiplos modelos LLM (LLaMA 70B, 8B, Qwen 32B)
219
+ - Upload de CSV personalizado
220
+ - Sistema de cache inteligente
221
+ - Modo avançado com refinamento
222
+ - Histórico de conversas
223
+ - Interface Gradio moderna
224
+ - Reset do sistema
225
+
226
+ ### ✅ Novas Funcionalidades
227
+ - Processamento genérico de CSV
228
+ - Arquitetura modular de nós
229
+ - Gerenciamento de objetos não-serializáveis
230
+ - Fluxo condicional otimizado
231
+ - Validação automática de sistema
232
+ - Detecção automática de portas
233
+ - Logs estruturados
234
+ - **Integração LangSmith**: Observabilidade completa e automática
235
+
236
+ ## 🧪 Testes
237
+
238
+ ### Arquivo de Teste
239
+ ```bash
240
+ python test_new_architecture.py
241
+ ```
242
+
243
+ Testa individualmente:
244
+ - Processamento CSV
245
+ - Criação de banco
246
+ - Agente SQL
247
+ - Gerenciador de objetos
248
+ - Amostra de dados
249
+
250
+ ## 🔄 Deploy
251
+
252
+ ### Local
253
+ ```bash
254
+ python app.py
255
+ ```
256
+
257
+ ### HuggingFace Spaces
258
+ 1. Configure as variáveis de ambiente
259
+ 2. Faça upload dos arquivos
260
+ 3. O sistema detectará automaticamente a porta disponível
261
+
262
+ ## 📈 Benefícios da Nova Arquitetura
263
+
264
+ 1. **Escalabilidade**: Fácil adição de novos nós
265
+ 2. **Manutenibilidade**: Código organizado e modular
266
+ 3. **Robustez**: Sem problemas de serialização
267
+ 4. **Flexibilidade**: Processamento genérico de dados
268
+ 5. **Performance**: Fluxo otimizado com cache
269
+ 6. **Debugging**: Logs detalhados por nó
270
+ 7. **Testabilidade**: Nós independentes testáveis
271
+
272
+ ## 🔍 Monitoramento
273
+
274
+ ### Logs Estruturados
275
+ ```
276
+ [VALIDATION] - Validação de entrada
277
+ [CACHE] - Operações de cache
278
+ [CONTEXT] - Preparação de contexto
279
+ [DATABASE] - Operações de banco
280
+ [QUERY] - Processamento de consultas
281
+ [REFINE] - Refinamento de respostas
282
+ [HISTORY] - Atualização de histórico
283
+ ```
284
+
285
+ ### Estatísticas
286
+ - Tempo de execução por nó
287
+ - Taxa de acerto do cache
288
+ - Estatísticas de processamento CSV
289
+ - Validação de componentes
290
+
291
+ ## 🚀 Roadmap de Expansão
292
+
293
+ ### **🎯 Arquitetura Preparada para Múltiplos Agentes**
294
+
295
+ A arquitetura atual está **perfeitamente preparada** para expansão com novos agentes especializados:
296
+
297
+ #### **📄 Agente PDF (Curto Prazo)**
298
+ ```python
299
+ # Implementação planejada:
300
+ nodes/pdf_processing_node.py
301
+ agents/pdf_agent.py
302
+
303
+ # Funcionalidades:
304
+ - Extração de texto (PyPDF2, pdfplumber)
305
+ - OCR para documentos escaneados (Tesseract)
306
+ - Análise de estrutura de documentos
307
+ - Busca semântica em conteúdo
308
+ - Integração com LangGraph existente
309
+ ```
310
+
311
+ #### **🗄️ Agente MySQL (Médio Prazo)**
312
+ ```python
313
+ # Implementação planejada:
314
+ nodes/mysql_node.py
315
+ agents/mysql_agent.py
316
+
317
+ # Funcionalidades:
318
+ - Conexões externas MySQL/PostgreSQL
319
+ - Pool de conexões otimizado
320
+ - Queries complexas com JOINs
321
+ - Transações e rollbacks
322
+ - Múltiplas bases de dados
323
+ ```
324
+
325
+ #### **📊 Agente de Gráficos (Médio Prazo)**
326
+ ```python
327
+ # Implementação planejada:
328
+ nodes/chart_generation_node.py
329
+ agents/chart_agent.py
330
+
331
+ # Funcionalidades:
332
+ - Matplotlib, Plotly, Seaborn
333
+ - Gráficos baseados em consultas SQL
334
+ - Análise automática de dados
335
+ - Exporta��ão em múltiplos formatos
336
+ - Dashboards interativos
337
+ ```
338
+
339
+ #### **🤖 Agente de ML/Previsões (Longo Prazo)**
340
+ ```python
341
+ # Implementação planejada:
342
+ nodes/prediction_node.py
343
+ agents/ml_agent.py
344
+
345
+ # Funcionalidades:
346
+ - Modelos de Machine Learning
347
+ - Análise de séries temporais
348
+ - Previsões automáticas
349
+ - Integração com scikit-learn
350
+ - AutoML capabilities
351
+ ```
352
+
353
+ ### **🔄 Sistema de Detecção Expandido**
354
+
355
+ ```python
356
+ def detect_query_type(user_query: str) -> str:
357
+ """Função já preparada para expansão"""
358
+
359
+ query_lower = user_query.lower().strip()
360
+
361
+ # Detecção atual
362
+ if 'sql' in query_lower or 'tabela' in query_lower:
363
+ return 'sql_query'
364
+
365
+ # Expansões futuras (já estruturadas)
366
+ elif 'pdf' in query_lower or 'documento' in query_lower:
367
+ return 'pdf_processing'
368
+ elif 'mysql' in query_lower or 'banco mysql' in query_lower:
369
+ return 'mysql_query'
370
+ elif 'gráfico' in query_lower or 'chart' in query_lower:
371
+ return 'chart_generation'
372
+ elif 'prever' in query_lower or 'previsão' in query_lower:
373
+ return 'prediction'
374
+
375
+ return 'sql_query' # Default
376
+ ```
377
+
378
+ ### **🎛️ Roteamento Condicional Preparado**
379
+
380
+ ```python
381
+ # No main_graph.py - Estrutura já preparada
382
+ def route_by_type(state: Dict[str, Any]) -> str:
383
+ query_type = state.get("query_type", "sql_query")
384
+
385
+ routing_map = {
386
+ "sql_query": "sql_processing",
387
+ "pdf_processing": "pdf_processing", # FUTURO
388
+ "mysql_query": "mysql_processing", # FUTURO
389
+ "chart_generation": "chart_generation", # FUTURO
390
+ "prediction": "prediction_processing" # FUTURO
391
+ }
392
+
393
+ return routing_map.get(query_type, "sql_processing")
394
+ ```
395
+
396
+ ### **📈 Facilidade de Implementação**
397
+
398
+ **Por que é fácil expandir:**
399
+ - ✅ **Estrutura modular** - Cada agente = novo nó
400
+ - ✅ **ObjectManager flexível** - Gerencia qualquer objeto
401
+ - ✅ **Sistema de detecção** - Já preparado para novos tipos
402
+ - ✅ **Configurações centralizadas** - Fácil adicionar APIs
403
+ - ✅ **Interface dinâmica** - Dropdown automático
404
+ - ✅ **Async/await** - Performance mantida
405
+ - ✅ **Logs estruturados** - Debugging facilitado
406
+
407
+ ### **🎯 Próximos Passos Recomendados**
408
+
409
+ 1. **Agente PDF** - Implementação mais simples e útil
410
+ 2. **Sistema de Templates** - Prompts especializados por agente
411
+ 3. **Métricas avançadas** - Performance por tipo de agente
412
+ 4. **API REST** - Exposição de funcionalidades
413
+ 5. **Agente MySQL** - Conexões externas
414
+ 6. **Sistema de Pipelines** - Combinação de agentes
415
+
416
+ ---
417
+
418
+ **🏆 Conclusão**: A arquitetura atual é **excepcional** e está perfeitamente preparada para se tornar uma **plataforma completa de agentes especializados**. A expansão será natural e incremental, mantendo a robustez e performance existentes.
README copy.md ADDED
@@ -0,0 +1,366 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # 🤖 AgentGraph - Plataforma Multi-Agente LangGraph
2
+
3
+ Uma plataforma inteligente de agentes especializados que utiliza LangGraph para processar consultas em linguagem natural, com suporte a múltiplos provedores de LLM e arquitetura modular extensível.
4
+
5
+ ## ✨ Funcionalidades Principais
6
+
7
+ ### 🎯 **Sistema Multi-Agente**
8
+ - **Agente SQL**: Consultas inteligentes em dados CSV/SQLite
9
+ - **Detecção Automática**: Identifica tipo de processamento necessário
10
+ - **Arquitetura Extensível**: Preparado para PDF, MySQL, Gráficos e ML
11
+
12
+ ### 🧠 **Múltiplos Provedores LLM**
13
+ - **OpenAI**: GPT-4o, GPT-4o-mini, o3-mini
14
+ - **Anthropic**: Claude-3.5-Sonnet com tool-calling
15
+ - **HuggingFace**: LLaMA 70B, LLaMA 8B, DeepSeek-R1 (refinamento)
16
+
17
+ ### 🔄 **LangGraph Avançado**
18
+ - Arquitetura baseada em nós especializados
19
+ - Processamento assíncrono e paralelo
20
+ - Gerenciamento inteligente de objetos não-serializáveis
21
+ - Sistema de retry com backoff exponencial
22
+
23
+ ### 🔍 **Observabilidade com LangSmith**
24
+ - Rastreamento completo de execuções LangGraph
25
+ - Monitoramento de performance em tempo real
26
+ - Debug avançado de agentes e fluxos
27
+ - Análise de custos e uso de tokens
28
+ - Dashboards de observabilidade integrados
29
+
30
+ ### 🌐 **Interface Moderna**
31
+ - Interface Gradio responsiva e centralizada
32
+ - Configurações separadas do chat principal
33
+ - Upload de CSV com processamento automático
34
+ - Histórico detalhado e logs estruturados
35
+
36
+ ### 💾 **Sistema Inteligente**
37
+ - Cache otimizado com verificação de hits
38
+ - Processamento genérico de CSV com detecção automática
39
+ - Modo avançado com refinamento de respostas
40
+ - Verbose ativo para debugging
41
+
42
+ ## 📁 Estrutura do Projeto
43
+
44
+ ```
45
+ agentgraph/
46
+ ├── app.py # 🚀 Entry point: Gradio + LangGraph
47
+ ├── graphs/
48
+ │ └── main_graph.py # 🔄 StateGraph principal com roteamento
49
+ ├── nodes/ # 🎯 Nós especializados
50
+ │ ├── csv_processing_node.py # 📊 Processamento genérico de CSV
51
+ │ ├── database_node.py # 🗄️ Operações de banco de dados
52
+ │ ├── query_node.py # 🔍 Processamento de consultas
53
+ │ ├── refinement_node.py # ✨ Refinamento de respostas
54
+ │ ├── cache_node.py # 💾 Gerenciamento de cache
55
+ │ └── agent_node.py # 🤖 Coordenação geral
56
+ ├── agents/ # 🧠 Agentes especializados
57
+ │ ├── sql_agent.py # 📝 Agente SQL multi-provedor
58
+ │ └── tools.py # 🛠️ Ferramentas e detecção
59
+ ├── utils/ # ⚙️ Utilitários
60
+ │ ├── database.py # 🗃️ Funções de banco de dados
61
+ │ ├── config.py # 📋 Configurações centralizadas
62
+ │ └── object_manager.py # 🎛️ Gerenciador de objetos
63
+ ├── uploaded_data/ # 📂 Arquivos CSV enviados
64
+ ├── requirements.txt # 📦 Dependências
65
+ ├── README.md # 📖 Documentação
66
+ ├── architecture.md # 🏗️ Arquitetura detalhada
67
+ └── .env # 🔐 Variáveis de ambiente
68
+ ```
69
+
70
+ ## 🚀 Instalação Rápida
71
+
72
+ ### 1. **Clone o Repositório**
73
+ ```bash
74
+ git clone https://github.com/seu-usuario/agentgraph.git
75
+ cd agentgraph
76
+ ```
77
+
78
+ ### 2. **Instale as Dependências**
79
+ ```bash
80
+ pip install -r requirements.txt
81
+ ```
82
+
83
+ ### 3. **Configure as Variáveis de Ambiente**
84
+ Crie/edite o arquivo `.env`:
85
+
86
+ ```env
87
+ # 🔑 API Keys (pelo menos uma é obrigatória)
88
+ HUGGINGFACE_API_KEY=hf_your_key_here
89
+ OPENAI_API_KEY=sk-your_key_here
90
+ ANTHROPIC_API_KEY=sk-ant-your_key_here
91
+
92
+ # 🔍 LangSmith - Observabilidade (OPCIONAL)
93
+ LANGSMITH_API_KEY=lsv2_pt_your_key_here
94
+ LANGSMITH_TRACING=true
95
+ LANGSMITH_ENDPOINT=https://api.smith.langchain.com
96
+ LANGSMITH_PROJECT=agentgraph-project
97
+
98
+ # 🗄️ Configurações de Banco
99
+ SQL_DB_PATH=data.db
100
+ DEFAULT_CSV_PATH=tabela.csv
101
+ UPLOAD_DIR=uploaded_data
102
+
103
+ # 🤖 Configurações de Modelo
104
+ DEFAULT_MODEL=GPT-4o-mini
105
+ MAX_ITERATIONS=40
106
+ TEMPERATURE=0
107
+
108
+ # 🌐 Configurações do Gradio
109
+ GRADIO_SHARE=False
110
+ GRADIO_PORT=7860
111
+ ```
112
+
113
+ ### 4. **Execute a Aplicação**
114
+ ```bash
115
+ python app.py
116
+ ```
117
+
118
+ 🎉 **Pronto!** Acesse `http://localhost:7860` no seu navegador.
119
+
120
+ ### 5. **Configure LangSmith (Opcional)**
121
+ Para habilitar observabilidade avançada:
122
+
123
+ 1. **Crie conta** em [LangSmith](https://smith.langchain.com/)
124
+ 2. **Obtenha API Key** no dashboard
125
+ 3. **Configure no .env**:
126
+ ```env
127
+ LANGSMITH_API_KEY=lsv2_pt_your_key_here
128
+ LANGSMITH_TRACING=true
129
+ LANGSMITH_PROJECT=agentgraph-project
130
+ ```
131
+ 4. **Reinicie** a aplicação
132
+
133
+ ✨ **Com LangSmith você terá**:
134
+ - 🔍 Rastreamento completo de execuções
135
+ - 📊 Dashboards de performance
136
+ - 🐛 Debug avançado de agentes
137
+ - 💰 Análise de custos de tokens
138
+ - 📈 Métricas de uso em tempo real
139
+
140
+ ## 💡 Como Usar
141
+
142
+ ### 🎯 **Fluxo Básico**
143
+ 1. **Acesse** `http://localhost:7860`
144
+ 2. **Selecione** o modelo LLM desejado (OpenAI, Anthropic ou HuggingFace)
145
+ 3. **Upload** de CSV (opcional) ou use dados padrão
146
+ 4. **Digite** perguntas em linguagem natural
147
+ 5. **Receba** respostas detalhadas com SQL e análises
148
+
149
+ ### 🤖 **Modelos Disponíveis**
150
+
151
+ | Provedor | Modelo | Uso | Características |
152
+ |----------|--------|-----|-----------------|
153
+ | **OpenAI** | GPT-4o | AgentSQL | Tools + Verbose ativo |
154
+ | **OpenAI** | GPT-4o-mini | AgentSQL | Modelo padrão, rápido |
155
+ | **OpenAI** | o3-mini | AgentSQL | Sem temperature |
156
+ | **Anthropic** | Claude-3.5-Sonnet | AgentSQL | Tool-calling + Retry |
157
+ | **HuggingFace** | LLaMA 70B | Refinamento | Opcional |
158
+ | **HuggingFace** | LLaMA 8B | Refinamento | Opcional |
159
+ | **HuggingFace** | DeepSeek-R1 | Refinamento | Opcional |
160
+
161
+ ### 📊 **Exemplos de Perguntas**
162
+ ```
163
+ "Quais são os produtos com maior preço?"
164
+ "Mostre as vendas por categoria"
165
+ "Qual a média de idade dos clientes?"
166
+ "Produtos com estoque baixo"
167
+ "Análise de tendências mensais"
168
+ ```
169
+
170
+ ## 🧪 Verificação e Testes
171
+
172
+ ### **Verificação Rápida**
173
+ ```bash
174
+ # Verifica configuração LangSmith
175
+ python check_langsmith_setup.py
176
+
177
+ # Teste completo de integração
178
+ python test_langsmith_integration.py
179
+ ```
180
+
181
+ ### **Arquivos de Teste Disponíveis**
182
+ - `check_langsmith_setup.py` - Verificação rápida de configuração
183
+ - `test_langsmith_integration.py` - Teste completo de integração
184
+ - `test_new_architecture.py` - Teste da arquitetura LangGraph
185
+ - `test_graph_functionality.py` - Teste de funcionalidades de gráficos
186
+
187
+ ## 🛠️ Tecnologias
188
+
189
+ ### **Core Framework**
190
+ - **LangGraph**: Orquestração de agentes com nós especializados
191
+ - **LangChain**: Framework de LLM com tool-calling
192
+ - **LangSmith**: Observabilidade e rastreamento avançado
193
+ - **Gradio**: Interface web moderna e responsiva
194
+
195
+ ### **Processamento de Dados**
196
+ - **SQLAlchemy**: ORM para banco de dados SQLite
197
+ - **Pandas**: Processamento e análise de dados CSV
198
+ - **SQLite**: Banco de dados embarcado
199
+
200
+ ### **Provedores LLM**
201
+ - **OpenAI**: GPT-4o, GPT-4o-mini, o3-mini
202
+ - **Anthropic**: Claude-3.5-Sonnet
203
+ - **HuggingFace**: LLaMA, DeepSeek via Together AI
204
+
205
+ ### **Utilitários**
206
+ - **AsyncIO**: Processamento assíncrono
207
+ - **Logging**: Sistema de logs estruturados
208
+ - **Retry**: Backoff exponencial para rate limiting
209
+
210
+ ## 🏗️ Arquitetura Atual
211
+
212
+ ### **Fluxo Principal**
213
+ ```
214
+ Pergunta → Detecção de Tipo → AgentSQL → Refinamento (Opcional) → Resposta
215
+ ```
216
+
217
+ ### **Nós Especializados**
218
+ - **🔍 Query Node**: Detecção e processamento de consultas
219
+ - **🗄️ Database Node**: Operações de banco e CSV
220
+ - **💾 Cache Node**: Gerenciamento de cache e histórico
221
+ - **✨ Refinement Node**: Melhoria de respostas (modo avançado)
222
+ - **🤖 Agent Node**: Coordenação geral do fluxo
223
+
224
+ ### **Características Técnicas**
225
+ - ✅ **Async/Await**: Processamento não-bloqueante
226
+ - ✅ **Multi-Provedor**: OpenAI, Anthropic, HuggingFace
227
+ - ✅ **Tool-Calling**: Ferramentas SQL nativas
228
+ - ✅ **Verbose Ativo**: Debugging detalhado
229
+ - ✅ **Rate Limiting**: Retry automático para APIs
230
+ - ✅ **Object Manager**: Gerenciamento de objetos não-serializáveis
231
+
232
+ ## 🚀 Roadmap - Implementações Futuras
233
+
234
+ ### **🎯 Curto Prazo (1-2 meses)**
235
+
236
+ #### **📄 Agente PDF**
237
+ ```python
238
+ # Funcionalidades planejadas:
239
+ - Extração de texto de PDFs
240
+ - OCR para documentos escaneados
241
+ - Análise de estrutura de documentos
242
+ - Busca semântica em conteúdo
243
+ - Integração com LangGraph
244
+ ```
245
+
246
+ #### **📊 Agente de Gráficos**
247
+ ```python
248
+ # Funcionalidades planejadas:
249
+ - Geração automática de visualizações
250
+ - Matplotlib, Plotly, Seaborn
251
+ - Gráficos baseados em consultas SQL
252
+ - Exportação em múltiplos formatos
253
+ ```
254
+
255
+ ### **🎯 Médio Prazo (3-6 meses)**
256
+
257
+ #### **🗄️ Agente MySQL**
258
+ ```python
259
+ # Funcionalidades planejadas:
260
+ - Conexões externas MySQL/PostgreSQL
261
+ - Queries complexas com JOINs
262
+ - Gerenciamento de múltiplas bases
263
+ - Pool de conexões
264
+ ```
265
+
266
+ #### **🤖 Agente de ML/Previsões**
267
+ ```python
268
+ # Funcionalidades planejadas:
269
+ - Modelos de Machine Learning
270
+ - Análise de séries temporais
271
+ - Previsões automáticas
272
+ - Integração com scikit-learn
273
+ ```
274
+
275
+ ### **🎯 Longo Prazo (6+ meses)**
276
+
277
+ #### **🔄 Sistema de Pipelines**
278
+ ```python
279
+ # Funcionalidades planejadas:
280
+ - Combinação de múltiplos agentes
281
+ - Workflows customizáveis
282
+ - Processamento em lote
283
+ - Agendamento de tarefas
284
+ ```
285
+
286
+ #### **🌐 API REST**
287
+ ```python
288
+ # Funcionalidades planejadas:
289
+ - Endpoints para cada agente
290
+ - Autenticação e autorização
291
+ - Rate limiting por usuário
292
+ - Documentação OpenAPI
293
+ ```
294
+
295
+ #### **☁️ Integração Cloud**
296
+ ```python
297
+ # Funcionalidades planejadas:
298
+ - Deploy em AWS/Azure/GCP
299
+ - Armazenamento em nuvem
300
+ - Escalabilidade automática
301
+ - Monitoramento avançado
302
+ ```
303
+
304
+ ## 📈 Exemplos de Uso
305
+
306
+ ### **Análise de Vendas**
307
+ ```
308
+ Usuário: "Quais produtos tiveram maior crescimento no último trimestre?"
309
+
310
+ Sistema:
311
+ 1. 🔍 Detecta: consulta SQL
312
+ 2. 🧠 Claude analisa estrutura da tabela
313
+ 3. 📝 Gera SQL otimizado
314
+ 4. 📊 Executa e analisa resultados
315
+ 5. ��� Resposta detalhada em português
316
+ ```
317
+
318
+ ### **Relatório Financeiro**
319
+ ```
320
+ Usuário: "Mostre um resumo das receitas por categoria"
321
+
322
+ Sistema:
323
+ 1. 🔍 Identifica: agregação de dados
324
+ 2. 🧠 GPT-4o cria query com GROUP BY
325
+ 3. 📊 Executa com LIMIT 20
326
+ 4. ✨ Refinamento opcional com LLaMA
327
+ 5. 📈 Resposta com insights
328
+ ```
329
+
330
+ ## 🤝 Contribuição
331
+
332
+ ### **Como Contribuir**
333
+ 1. **Fork** o repositório
334
+ 2. **Crie** uma branch para sua feature (`git checkout -b feature/nova-funcionalidade`)
335
+ 3. **Commit** suas mudanças (`git commit -am 'Adiciona nova funcionalidade'`)
336
+ 4. **Push** para a branch (`git push origin feature/nova-funcionalidade`)
337
+ 5. **Abra** um Pull Request
338
+
339
+ ### **Áreas de Contribuição**
340
+ - 🐛 **Bug fixes** e melhorias
341
+ - 📄 **Documentação** e exemplos
342
+ - 🧪 **Testes** automatizados
343
+ - 🎯 **Novos agentes** (PDF, MySQL, etc.)
344
+ - 🎨 **Interface** e UX
345
+ - ⚡ **Performance** e otimizações
346
+
347
+ ## 📄 Licença
348
+
349
+ Este projeto está licenciado sob a [MIT License](LICENSE).
350
+
351
+ ## 🙏 Agradecimentos
352
+
353
+ - **LangChain** e **LangGraph** pela framework excepcional
354
+ - **Anthropic**, **OpenAI** e **HuggingFace** pelos modelos LLM
355
+ - **Gradio** pela interface web intuitiva
356
+ - Comunidade open source pelas contribuições
357
+
358
+ ---
359
+
360
+ **⭐ Se este projeto foi útil, considere dar uma estrela no GitHub!**
361
+
362
+ **🔗 Links Úteis:**
363
+ - [Documentação Detalhada](architecture.md)
364
+ - [Exemplos de Uso](examples/)
365
+ - [Issues e Sugestões](https://github.com/seu-usuario/agentgraph/issues)
366
+ - [Discussões](https://github.com/seu-usuario/agentgraph/discussions)
agentchain.md ADDED
@@ -0,0 +1,361 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import time
3
+ import shutil
4
+ import pandas as pd
5
+ from sqlalchemy import create_engine
6
+ from langchain_openai import ChatOpenAI
7
+ from langchain_community.agent_toolkits import create_sql_agent
8
+ from langchain_community.utilities import SQLDatabase
9
+ from huggingface_hub import InferenceClient
10
+ import gradio as gr
11
+ from dotenv import load_dotenv
12
+ import logging
13
+ from sqlalchemy.types import DateTime, Integer, Float
14
+
15
+ load_dotenv()
16
+
17
+ UPLOAD_DIR = "uploaded_data"
18
+ os.makedirs(UPLOAD_DIR, exist_ok=True)
19
+
20
+ DEFAULT_CSV_PATH = "tabela.csv"
21
+ UPLOADED_CSV_PATH = os.path.join(UPLOAD_DIR, "tabela.csv")
22
+ SQL_DB_PATH = "data.db"
23
+
24
+ HUGGINGFACE_API_KEY = os.getenv("HUGGINGFACE_API_KEY")
25
+ OPENAI_API_KEY = os.getenv("OPENAI_API_KEY")
26
+
27
+ LLAMA_MODELS = {
28
+ "LLaMA 70B": "meta-llama/Llama-3.3-70B-Instruct",
29
+ "LlaMA 8B": "meta-llama/Llama-3.1-8B-Instruct",
30
+ "Qwen 32B": "Qwen/QwQ-32B"
31
+ }
32
+
33
+ MAX_TOKENS_MAP = {
34
+ "meta-llama/Llama-3.3-70B-Instruct": 900,
35
+ "meta-llama/Llama-3.1-8B-Instruct": 700,
36
+ "Qwen/QwQ-32B": 8192
37
+ }
38
+
39
+ hf_client = InferenceClient(
40
+ provider="together", api_key=HUGGINGFACE_API_KEY
41
+ )
42
+
43
+ os.environ["OPENAI_API_KEY"] = OPENAI_API_KEY
44
+
45
+ query_cache = {}
46
+ history_log = []
47
+ recent_history = []
48
+ show_history_flag = False
49
+ engine = None
50
+
51
+ logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
52
+
53
+ def get_active_csv_path():
54
+ """Retorna o CSV ativo: o carregado ou o padrão.."""
55
+ if os.path.exists(UPLOADED_CSV_PATH):
56
+ logging.info(f"[CSV] Usando arquivo CSV carregado: {UPLOADED_CSV_PATH}")
57
+ return UPLOADED_CSV_PATH
58
+ else:
59
+ logging.info(f"[CSV] Usando arquivo CSV padrão: {DEFAULT_CSV_PATH}")
60
+ return DEFAULT_CSV_PATH
61
+
62
+ def create_engine_and_load_db(csv_path, sql_db_path):
63
+ if os.path.exists(sql_db_path):
64
+ print("Banco de dados SQL já existe. Carregando...")
65
+ return create_engine(f"sqlite:///{sql_db_path}")
66
+ else:
67
+ print("Banco de dados SQL não encontrado. Criando...")
68
+ engine = create_engine(f"sqlite:///{sql_db_path}")
69
+
70
+ df = pd.read_csv(
71
+ csv_path,
72
+ sep=";",
73
+ encoding='utf-8',
74
+ parse_dates=["DATA_INICIAL", "DATA_FINAL"],
75
+ dayfirst=True,
76
+ on_bad_lines="skip"
77
+ )
78
+
79
+ colunas_para_float = [
80
+ "PRECO_VISTA", "PRECO_CHEIO"
81
+ ]
82
+
83
+ colunas_para_int = [
84
+ "QUANTIDADE", "TOTAL_PAGINAS_CAPA", "VALOR_MEDIDA", "DIAS_VALIDADE"
85
+ ]
86
+
87
+ for col in colunas_para_float:
88
+ if col in df.columns:
89
+ df[col] = pd.to_numeric(df[col].replace("-", None), errors="coerce")
90
+
91
+ for col in colunas_para_int:
92
+ if col in df.columns:
93
+ df[col] = pd.to_numeric(df[col].replace("-", None), errors="coerce")
94
+ df[col] = df[col].where(df[col].dropna() == df[col].dropna().astype(int))
95
+ df[col] = df[col].astype("Int64")
96
+
97
+ sql_dtype = {
98
+ "DATA_INICIAL": DateTime(),
99
+ "DATA_FINAL": DateTime(),
100
+ "QUANTIDADE": Integer(),
101
+ "PRECO_VISTA": Float(),
102
+ "PRECO_CHEIO": Float(),
103
+ "TOTAL_PAGINAS_CAPA": Integer(),
104
+ "VALOR_MEDIDA": Integer(),
105
+ "DIAS_VALIDADE": Integer()
106
+
107
+ }
108
+
109
+ print("[DEBUG] Tipos das colunas:")
110
+ print(df.dtypes)
111
+
112
+ df.to_sql("tabela", engine, index=False, if_exists="replace", dtype=sql_dtype)
113
+ print("Banco de dados SQL criado com sucesso!")
114
+ return engine
115
+
116
+ def handle_csv_upload(file):
117
+ global engine, db, sql_agent
118
+
119
+ try:
120
+ file_path = file.name
121
+ shutil.copy(file_path, UPLOADED_CSV_PATH)
122
+ logging.info(f"[UPLOAD] CSV salvo como: {UPLOADED_CSV_PATH}")
123
+
124
+ engine = create_engine_and_load_db(UPLOADED_CSV_PATH, SQL_DB_PATH)
125
+ db = SQLDatabase(engine=engine)
126
+ logging.info("[UPLOAD] Novo banco carregado e DB atualizado.")
127
+
128
+ sql_agent = create_sql_agent(
129
+ ChatOpenAI(model="gpt-4o-mini", temperature=0),
130
+ db=db,
131
+ agent_type="openai-tools",
132
+ verbose=True,
133
+ max_iterations=40,
134
+ return_intermediate_steps=True
135
+ )
136
+
137
+ logging.info("[UPLOAD] Novo banco carregado e agente recriado. Cache limpo.")
138
+ query_cache.clear()
139
+ history_log.clear()
140
+ recent_history.clear()
141
+
142
+ return "✅ CSV carregado com sucesso!"
143
+
144
+ except Exception as e:
145
+ logging.error(f"[ERRO] Falha ao processar novo CSV: {e}")
146
+ return f"❌ Erro ao processar CSV: {e}"
147
+
148
+ def reset_app():
149
+ global engine, db, sql_agent, query_cache, history_log, recent_history
150
+
151
+ try:
152
+ if os.path.exists(UPLOADED_CSV_PATH):
153
+ os.remove(UPLOADED_CSV_PATH)
154
+ logging.info("[RESET] CSV personalizado removido.")
155
+
156
+ engine = create_engine_and_load_db(DEFAULT_CSV_PATH, SQL_DB_PATH)
157
+ db = SQLDatabase(engine=engine)
158
+ sql_agent = create_sql_agent(ChatOpenAI(model="gpt-4o-mini", temperature=0), db=db, agent_type="openai-tools", verbose=True, max_iterations=40, return_intermediate_steps=True)
159
+ query_cache.clear()
160
+ history_log.clear()
161
+ recent_history.clear()
162
+
163
+ return "🔄 Sistema resetado para o estado inicial."
164
+
165
+ except Exception as e:
166
+ return f"❌ Erro ao resetar: {e}"
167
+
168
+ engine = create_engine_and_load_db(get_active_csv_path(), SQL_DB_PATH)
169
+ db = SQLDatabase(engine=engine)
170
+ llm = ChatOpenAI(model="gpt-4o-mini", temperature=0)
171
+ sql_agent = create_sql_agent(llm, db=db, agent_type="openai-tools", verbose=True, max_iterations=40, return_intermediate_steps=True)
172
+
173
+ def generate_initial_context(db_sample):
174
+ return (
175
+ f"Você é um assistente que gera queries SQL objetivas e eficientes. Sempre inclua LIMIT 20 nas queries. Aqui está o banco de dados:\n\n"
176
+ f"Exemplos do banco de dados:\n{db_sample.head().to_string(index=False)}\n\n"
177
+ "\n***IMPORTANTE***: Detecte automaticamente o idioma da pergunta do usuário e responda sempre no mesmo idioma."
178
+ "\nEsta base contém os SKUs (produtos) que foram promocionados por meio de TABLOIDE OU PROMOCAO OU ANUNCIO.\n"
179
+ "Cada linha representa um SKU OU PRODUTO único PRESENTE NO TABLOIDE OU PROMOCAO OU ANUNCIO, incluindo sua descrição completa, os veículos OU MIDIAS de promoção utilizados e o respectivo período em que a promoção ocorreu.\n"
180
+
181
+ "\nInformações imporatantes:\n"
182
+ "- Use `LIKE '%<palavras-chave>%'` para buscas em colunas de texto.\n"
183
+ "- Quando o usuário mencionar uma categoria, procure nas colunas: `CATEGORIA_PRODUTO_SKU`.\n"
184
+ "- Se o usuário se referir a Nestle, o jeito correto de se escrever é Nestle sem acento e não Nestlé.\n"
185
+ "- Você está usando um banco de dados SQLite.\n"
186
+
187
+ "\nRetorne apenas a pergunta e a query SQL mais eficiente para entregar ao agent SQL do LangChain para gerar uma resposta para a pergunta. O formato deve ser:\n"
188
+ "\nPergunta: <pergunta do usuário>\n"
189
+ "\nOpção de Query SQL:\n<query SQL>"
190
+ "\nIdioma: <idioma>"
191
+ )
192
+
193
+ def is_greeting(user_query):
194
+ greetings = ["olá", "oi", "bom dia", "boa tarde", "boa noite", "oi, tudo bem?"]
195
+ return user_query.lower().strip() in greetings
196
+
197
+ def query_with_llama(user_query, db_sample, selected_model_name):
198
+ model_id = LLAMA_MODELS[selected_model_name]
199
+ max_tokens = MAX_TOKENS_MAP.get(model_id, 512)
200
+
201
+ initial_context = generate_initial_context(db_sample)
202
+ formatted_history = "\n".join(
203
+ [f"{msg['role'].capitalize()}: {msg['content']}" for msg in recent_history[-2:]]
204
+ )
205
+
206
+ full_prompt = f"{initial_context}\n\nHistórico recente:\n{formatted_history}\n\nPergunta do usuário:\n{user_query}"
207
+
208
+ logging.info(f"[DEBUG] Contexto enviado ao ({selected_model_name}):\n{full_prompt}\n")
209
+
210
+ start_time = time.time()
211
+
212
+ try:
213
+ response = hf_client.chat.completions.create(
214
+ model=model_id,
215
+ messages=[{"role": "system", "content": full_prompt}],
216
+ max_tokens=max_tokens,
217
+ stream=False
218
+ )
219
+
220
+ llama_response = response["choices"][0]["message"]["content"]
221
+ end_time = time.time()
222
+ logging.info(f"[DEBUG] Resposta do {selected_model_name} para o Agent SQL:\n{llama_response.strip()}\n[Tempo de execução: {end_time - start_time:.2f}s]\n")
223
+ return llama_response.strip(), model_id
224
+
225
+ except Exception as e:
226
+ logging.error(f"[ERRO] Falha ao interagir com o modelo {selected_model_name}: {e}")
227
+ return None, model_id
228
+
229
+ def query_sql_agent(user_query, selected_model_name):
230
+ try:
231
+ if user_query in query_cache:
232
+ print(f"[CACHE] Retornando resposta do cache para a consulta: {user_query}")
233
+ return query_cache[user_query]
234
+
235
+ if is_greeting(user_query):
236
+ greeting_response = "Olá! Estou aqui para ajudar com suas consultas. Pergunte algo relacionado aos dados carregados no agente!"
237
+ query_cache[user_query] = greeting_response
238
+ return greeting_response
239
+
240
+ column_data = pd.read_sql_query("SELECT * FROM tabela LIMIT 10", engine)
241
+ llama_instruction = query_with_llama(user_query, column_data, selected_model_name)
242
+
243
+ if not llama_instruction:
244
+ return "Erro: O modelo Llama não conseguiu gerar uma instrução válida."
245
+
246
+ print("------- Agent SQL: Executando query -------")
247
+ response = sql_agent.invoke({"input": llama_instruction})
248
+ sql_response = response.get("output", "Erro ao obter a resposta do agente.")
249
+
250
+ query_cache[user_query] = sql_response
251
+ return sql_response
252
+
253
+ except Exception as e:
254
+ return f"Erro ao consultar o agente SQL: {e}"
255
+
256
+ advanced_mode_enabled = False # Novo estado global
257
+
258
+ def toggle_advanced_mode(state):
259
+ global advanced_mode_enabled
260
+ advanced_mode_enabled = state
261
+ logging.info(f"[MODO AVANÇADO] {'Ativado' if state else 'Desativado'}")
262
+ return "Modo avançado ativado." if state else "Modo avançado desativado."
263
+
264
+ def refine_response_with_llm(user_question, sql_response, chart_md=""):
265
+ prompt = (
266
+ f"Pergunta do usuário:\n{user_question}\n\n"
267
+ f"Resposta gerada pelo agente SQL:\n{sql_response}\n\n"
268
+ "Sua tarefa é refinar, complementar e melhorar a resposta.\n"
269
+ "Adicione interpretações estatísticas ou insights relevantes."
270
+ )
271
+
272
+ logging.info(f"[DEBUG] Prompt enviado ao modelo de refinamento:\n{prompt}\n")
273
+
274
+ try:
275
+ response = hf_client.chat.completions.create(
276
+ model=LLAMA_MODELS["LLaMA 70B"],
277
+ messages=[{"role": "system", "content": prompt}],
278
+ max_tokens=1200,
279
+ stream=False
280
+ )
281
+ improved_response = response["choices"][0]["message"]["content"]
282
+ logging.info(f"[DEBUG] Resposta do modelo de refinamento:\n{improved_response}\n")
283
+ return improved_response + ("\n\n" + chart_md if chart_md else "")
284
+
285
+ except Exception as e:
286
+ logging.error(f"[ERRO] Falha ao refinar resposta com LLM: {e}")
287
+ return sql_response + ("\n\n" + chart_md if chart_md else "")
288
+
289
+ def chatbot_response(user_input, selected_model_name):
290
+ start_time = time.time()
291
+ response = query_sql_agent(user_input, selected_model_name)
292
+ end_time = time.time()
293
+
294
+ model_id = LLAMA_MODELS[selected_model_name]
295
+
296
+ if advanced_mode_enabled:
297
+ response = refine_response_with_llm(user_input, response)
298
+
299
+ history_log.append({
300
+ "Modelo LLM": model_id,
301
+ "Pergunta": user_input,
302
+ "Resposta": response,
303
+ "Tempo de Resposta (s)": round(end_time - start_time, 2)
304
+ })
305
+
306
+ recent_history.append({"role": "user", "content": user_input})
307
+ recent_history.append({"role": "assistant", "content": response})
308
+
309
+ if len(recent_history) > 4:
310
+ recent_history.pop(0)
311
+ recent_history.pop(0)
312
+
313
+ return response
314
+
315
+ def toggle_history():
316
+ global show_history_flag
317
+ show_history_flag = not show_history_flag
318
+ return history_log if show_history_flag else {}
319
+
320
+
321
+ with gr.Blocks(theme=gr.themes.Soft()) as demo:
322
+ with gr.Row():
323
+ with gr.Column(scale=1):
324
+ gr.Markdown("## Configurações")
325
+ model_selector = gr.Dropdown(list(LLAMA_MODELS.keys()), value="LLaMA 70B", label="")
326
+ csv_file = gr.File(file_types=[".csv"], label="")
327
+ upload_feedback = gr.Markdown()
328
+ advanced_checkbox = gr.Checkbox(label="Refinar Resposta")
329
+ reset_btn = gr.Button("Resetar")
330
+
331
+ with gr.Column(scale=4):
332
+ gr.Markdown("## Reasoning Agent")
333
+ chatbot = gr.Chatbot(height=500)
334
+ msg = gr.Textbox(placeholder="Digite sua pergunta aqui...", lines=1, label="")
335
+ btn = gr.Button("Enviar", variant="primary")
336
+ history_btn = gr.Button("Histórico", variant="secondary")
337
+ history_output = gr.JSON()
338
+ download_file = gr.File(visible=False)
339
+
340
+ def respond(message, chat_history, selected_model):
341
+ response = chatbot_response(message, selected_model)
342
+ chat_history.append((message, response))
343
+ return "", chat_history
344
+
345
+ def handle_csv_and_clear_chat(file):
346
+ feedback = handle_csv_upload(file)
347
+ return feedback, []
348
+
349
+ def reset_all():
350
+ feedback = reset_app()
351
+ return feedback, [], None
352
+
353
+ msg.submit(respond, [msg, chatbot, model_selector], [msg, chatbot])
354
+ btn.click(respond, [msg, chatbot, model_selector], [msg, chatbot])
355
+ history_btn.click(toggle_history, outputs=history_output)
356
+ csv_file.change(handle_csv_and_clear_chat, inputs=csv_file, outputs=[upload_feedback, chatbot])
357
+ reset_btn.click(reset_all, outputs=[upload_feedback, chatbot, csv_file])
358
+ advanced_checkbox.change(toggle_advanced_mode, inputs=advanced_checkbox, outputs=[])
359
+
360
+ if __name__ == "__main__":
361
+ demo.launch(share=False)
agents/sql_agent.py ADDED
@@ -0,0 +1,392 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Criação e configuração do agente SQL
3
+ """
4
+ import logging
5
+ import time
6
+ import asyncio
7
+ from typing import Optional, Dict, Any, List
8
+ from langchain_openai import ChatOpenAI
9
+ from langchain_anthropic import ChatAnthropic
10
+ from langchain_community.agent_toolkits import create_sql_agent
11
+ from langchain_community.utilities import SQLDatabase
12
+ from langchain.callbacks.base import BaseCallbackHandler
13
+ from langchain.schema import AgentAction, AgentFinish
14
+
15
+
16
+ from utils.config import (
17
+ MAX_ITERATIONS,
18
+ TEMPERATURE,
19
+ AVAILABLE_MODELS,
20
+ OPENAI_MODELS,
21
+ ANTHROPIC_MODELS
22
+ )
23
+
24
+ class SQLQueryCaptureHandler(BaseCallbackHandler):
25
+ """
26
+ Handler para capturar queries SQL executadas pelo agente
27
+ """
28
+
29
+ def __init__(self):
30
+ super().__init__()
31
+ self.sql_queries: List[str] = []
32
+ self.agent_actions: List[Dict[str, Any]] = []
33
+ self.step_count = 0
34
+
35
+ def on_agent_action(self, action: AgentAction, **kwargs) -> None:
36
+ """
37
+ Captura ações do agente, especialmente queries SQL
38
+
39
+ Args:
40
+ action: Ação do agente
41
+ """
42
+ try:
43
+ self.step_count += 1
44
+ tool_name = action.tool
45
+ tool_input = action.tool_input
46
+
47
+ # Capturar SQL especificamente (sem log de cada passo)
48
+ if tool_name == 'sql_db_query' and isinstance(tool_input, dict):
49
+ sql_query = tool_input.get('query', '')
50
+ if sql_query and sql_query.strip():
51
+ clean_query = sql_query.strip()
52
+ self.sql_queries.append(clean_query)
53
+
54
+ # Log apenas uma vez com query completa
55
+ logging.info(f"[SQL_HANDLER] 🔍 Query SQL capturada:\n{clean_query}")
56
+
57
+ # Armazenar todas as ações para debug
58
+ self.agent_actions.append({
59
+ "step": self.step_count,
60
+ "tool": tool_name,
61
+ "input": tool_input,
62
+ "timestamp": time.time()
63
+ })
64
+
65
+ except Exception as e:
66
+ logging.error(f"[SQL_HANDLER] Erro ao capturar ação: {e}")
67
+
68
+ def get_last_sql_query(self) -> Optional[str]:
69
+ """
70
+ Retorna a última query SQL capturada
71
+
72
+ Returns:
73
+ Última query SQL ou None se não houver
74
+ """
75
+ return self.sql_queries[-1] if self.sql_queries else None
76
+
77
+ def get_all_sql_queries(self) -> List[str]:
78
+ """
79
+ Retorna todas as queries SQL capturadas
80
+
81
+ Returns:
82
+ Lista de queries SQL
83
+ """
84
+ return self.sql_queries.copy()
85
+
86
+ def reset(self):
87
+ """Reseta o handler para nova execução"""
88
+ self.sql_queries.clear()
89
+ self.agent_actions.clear()
90
+ self.step_count = 0
91
+
92
+ async def retry_with_backoff(func, max_retries=3, base_delay=1.0):
93
+ """
94
+ Executa função com retry e backoff exponencial para lidar com rate limiting
95
+
96
+ Args:
97
+ func: Função a ser executada
98
+ max_retries: Número máximo de tentativas
99
+ base_delay: Delay base em segundos
100
+
101
+ Returns:
102
+ Resultado da função ou levanta exceção após esgotar tentativas
103
+ """
104
+ for attempt in range(max_retries + 1):
105
+ try:
106
+ return func()
107
+ except Exception as e:
108
+ error_str = str(e)
109
+
110
+ # Verifica se é erro de rate limiting ou overload
111
+ if any(keyword in error_str.lower() for keyword in ['overloaded', 'rate_limit', 'too_many_requests', 'quota']):
112
+ if attempt < max_retries:
113
+ delay = base_delay * (2 ** attempt) # Backoff exponencial
114
+ logging.warning(f"API sobrecarregada (tentativa {attempt + 1}/{max_retries + 1}). Aguardando {delay}s...")
115
+ await asyncio.sleep(delay)
116
+ continue
117
+ else:
118
+ logging.error(f"API continua sobrecarregada após {max_retries + 1} tentativas")
119
+ raise Exception(f"API da Anthropic sobrecarregada. Tente novamente em alguns minutos. Erro original: {e}")
120
+ else:
121
+ # Se não é erro de rate limiting, levanta imediatamente
122
+ raise e
123
+
124
+ # Não deveria chegar aqui, mas por segurança
125
+ raise Exception("Número máximo de tentativas excedido")
126
+
127
+
128
+
129
+ def create_sql_agent_executor(db: SQLDatabase, model_name: str = "gpt-4o-mini"):
130
+ """
131
+ Cria um agente SQL usando LangChain com suporte a diferentes provedores
132
+
133
+ Args:
134
+ db: Objeto SQLDatabase do LangChain
135
+ model_name: Nome do modelo a usar (OpenAI, Anthropic)
136
+
137
+ Returns:
138
+ Agente SQL configurado
139
+ """
140
+ try:
141
+ # Obtém o ID real do modelo
142
+ model_id = AVAILABLE_MODELS.get(model_name, model_name)
143
+
144
+ # Cria o modelo LLM baseado no provedor
145
+ if model_id in OPENAI_MODELS:
146
+ # Configurações específicas para modelos OpenAI
147
+ if model_id == "o3-mini":
148
+ # o3-mini não suporta temperature
149
+ llm = ChatOpenAI(model=model_id)
150
+ else:
151
+ # GPT-4o e GPT-4o-mini suportam temperature
152
+ llm = ChatOpenAI(model=model_id, temperature=TEMPERATURE)
153
+
154
+ agent_type = "openai-tools"
155
+
156
+ elif model_id in ANTHROPIC_MODELS:
157
+ # Claude com tool-calling e configurações para rate limiting
158
+ llm = ChatAnthropic(
159
+ model=model_id,
160
+ temperature=TEMPERATURE,
161
+ max_tokens=4096,
162
+ max_retries=2, # Retry interno do cliente
163
+ timeout=60.0 # Timeout mais longo
164
+ )
165
+ agent_type = "tool-calling" # Claude usa tool-calling
166
+
167
+ else:
168
+ # Fallback para OpenAI
169
+ llm = ChatOpenAI(
170
+ model="gpt-4o-mini",
171
+ temperature=TEMPERATURE
172
+ )
173
+ agent_type = "openai-tools"
174
+ logging.warning(f"Modelo {model_name} não reconhecido, usando gpt-4o-mini como fallback")
175
+
176
+ # Cria o agente SQL
177
+ sql_agent = create_sql_agent(
178
+ llm=llm,
179
+ db=db,
180
+ agent_type=agent_type,
181
+ verbose=True,
182
+ max_iterations=MAX_ITERATIONS,
183
+ return_intermediate_steps=True,
184
+ top_k=10
185
+ )
186
+
187
+ logging.info(f"Agente SQL criado com sucesso usando modelo {model_name} ({model_id}) com agent_type={agent_type}")
188
+ return sql_agent
189
+
190
+ except Exception as e:
191
+ logging.error(f"Erro ao criar agente SQL: {e}")
192
+ raise
193
+
194
+ class SQLAgentManager:
195
+ """
196
+ Gerenciador do agente SQL com funcionalidades avançadas
197
+ """
198
+
199
+ def __init__(self, db: SQLDatabase, model_name: str = "gpt-4o-mini"):
200
+ self.db = db
201
+ self.model_name = model_name
202
+ self.agent = None
203
+ self._initialize_agent()
204
+
205
+ def _initialize_agent(self):
206
+ """Inicializa o agente SQL"""
207
+ self.agent = create_sql_agent_executor(self.db, self.model_name)
208
+
209
+ def recreate_agent(self, new_db: SQLDatabase = None, new_model: str = None):
210
+ """
211
+ Recria o agente com novos parâmetros
212
+
213
+ Args:
214
+ new_db: Novo banco de dados (opcional)
215
+ new_model: Novo modelo (opcional)
216
+ """
217
+ if new_db:
218
+ self.db = new_db
219
+ if new_model:
220
+ self.model_name = new_model
221
+
222
+ self._initialize_agent()
223
+ logging.info("Agente SQL recriado com sucesso")
224
+
225
+ def _extract_text_from_claude_response(self, output) -> str:
226
+ """
227
+ Extrai texto limpo da resposta do Claude que pode vir em formato complexo
228
+
229
+ Args:
230
+ output: Resposta do agente (pode ser string, lista ou dict)
231
+
232
+ Returns:
233
+ String limpa com o texto da resposta
234
+ """
235
+ try:
236
+ # Se já é string, retorna diretamente
237
+ if isinstance(output, str):
238
+ return output
239
+
240
+ # Se é lista, procura por dicionários com 'text'
241
+ if isinstance(output, list):
242
+ text_parts = []
243
+ for item in output:
244
+ if isinstance(item, dict) and 'text' in item:
245
+ text_parts.append(item['text'])
246
+ elif isinstance(item, str):
247
+ text_parts.append(item)
248
+
249
+ if text_parts:
250
+ return '\n'.join(text_parts)
251
+
252
+ # Se é dict, procura por 'text' ou converte para string
253
+ if isinstance(output, dict):
254
+ if 'text' in output:
255
+ return output['text']
256
+ elif 'content' in output:
257
+ return str(output['content'])
258
+
259
+ # Fallback: converte para string
260
+ return str(output)
261
+
262
+ except Exception as e:
263
+ logging.warning(f"Erro ao extrair texto da resposta: {e}")
264
+ return str(output)
265
+
266
+ async def execute_query(self, instruction: str) -> dict:
267
+ """
268
+ Executa uma query através do agente SQL com retry para rate limiting
269
+
270
+ Args:
271
+ instruction: Instrução para o agente
272
+
273
+ Returns:
274
+ Resultado da execução
275
+ """
276
+ try:
277
+ logging.info("------- Agent SQL: Executando query -------")
278
+
279
+ # Criar handler para capturar SQL
280
+ sql_handler = SQLQueryCaptureHandler()
281
+
282
+ # Verifica se é agente Claude para aplicar retry
283
+ model_id = getattr(self, 'model_name', '')
284
+ is_claude = any(claude_model in model_id for claude_model in ANTHROPIC_MODELS)
285
+
286
+ if is_claude:
287
+ # Usa retry com backoff para Claude
288
+ response = await retry_with_backoff(
289
+ lambda: self.agent.invoke(
290
+ {"input": instruction},
291
+ {"callbacks": [sql_handler]}
292
+ ),
293
+ max_retries=3,
294
+ base_delay=2.0
295
+ )
296
+ else:
297
+ # Execução normal para outros modelos
298
+ response = self.agent.invoke(
299
+ {"input": instruction},
300
+ {"callbacks": [sql_handler]}
301
+ )
302
+
303
+ # Extrai e limpa a resposta
304
+ raw_output = response.get("output", "Erro ao obter a resposta do agente.")
305
+ clean_output = self._extract_text_from_claude_response(raw_output)
306
+
307
+ # Captura a última query SQL executada
308
+ sql_query = sql_handler.get_last_sql_query()
309
+
310
+ result = {
311
+ "output": clean_output,
312
+ "intermediate_steps": response.get("intermediate_steps", []),
313
+ "success": True,
314
+ "sql_query": sql_query, # ← Query SQL capturada
315
+ "all_sql_queries": sql_handler.get_all_sql_queries()
316
+ }
317
+
318
+ logging.info(f"Query executada com sucesso: {result['output'][:100]}...")
319
+ return result
320
+
321
+ except Exception as e:
322
+ error_str = str(e)
323
+
324
+ # Mensagem mais amigável para problemas de rate limiting
325
+ if any(keyword in error_str.lower() for keyword in ['overloaded', 'rate_limit', 'too_many_requests', 'quota']):
326
+ error_msg = (
327
+ "🚫 **API da Anthropic temporariamente sobrecarregada**\n\n"
328
+ "A API do Claude está com muitas solicitações no momento. "
329
+ "Por favor, aguarde alguns minutos e tente novamente.\n\n"
330
+ "**Sugestões:**\n"
331
+ "- Aguarde 2-3 minutos antes de tentar novamente\n"
332
+ "- Considere usar um modelo OpenAI temporariamente\n"
333
+ "- Tente novamente em horários de menor movimento\n\n"
334
+ f"*Erro técnico: {e}*"
335
+ )
336
+ else:
337
+ error_msg = f"Erro ao consultar o agente SQL: {e}"
338
+
339
+ logging.error(error_msg)
340
+ return {
341
+ "output": error_msg,
342
+ "intermediate_steps": [],
343
+ "success": False
344
+ }
345
+
346
+ def get_agent_info(self) -> dict:
347
+ """
348
+ Retorna informações sobre o agente atual
349
+
350
+ Returns:
351
+ Dicionário com informações do agente
352
+ """
353
+ return {
354
+ "model_name": self.model_name,
355
+ "max_iterations": MAX_ITERATIONS,
356
+ "temperature": TEMPERATURE,
357
+ "database_tables": self.db.get_usable_table_names() if self.db else [],
358
+ "agent_type": "openai-tools"
359
+ }
360
+
361
+ def validate_agent(self) -> bool:
362
+ """
363
+ Valida se o agente está funcionando corretamente
364
+
365
+ Returns:
366
+ True se válido, False caso contrário
367
+ """
368
+ try:
369
+ # Testa com uma query simples
370
+ test_result = self.agent.invoke({
371
+ "input": "Quantas linhas existem na tabela?"
372
+ })
373
+
374
+ success = "output" in test_result and test_result["output"]
375
+ logging.info(f"Validação do agente: {'Sucesso' if success else 'Falha'}")
376
+ return success
377
+
378
+ except Exception as e:
379
+ logging.error(f"Erro na validação do agente: {e}")
380
+ return False
381
+
382
+ def get_default_sql_agent(db: SQLDatabase) -> SQLAgentManager:
383
+ """
384
+ Cria um agente SQL com configurações padrão
385
+
386
+ Args:
387
+ db: Objeto SQLDatabase
388
+
389
+ Returns:
390
+ SQLAgentManager configurado
391
+ """
392
+ return SQLAgentManager(db)
agents/tools.py ADDED
@@ -0,0 +1,439 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Ferramentas para o agente SQL
3
+ """
4
+ import time
5
+ import logging
6
+ import re
7
+ from typing import Dict, Any, Optional, List
8
+ from huggingface_hub import InferenceClient
9
+ from langchain_community.utilities import SQLDatabase
10
+ from langchain_openai import ChatOpenAI
11
+ from langchain_anthropic import ChatAnthropic
12
+ import pandas as pd
13
+
14
+ from utils.config import (
15
+ HUGGINGFACE_API_KEY,
16
+ OPENAI_API_KEY,
17
+ ANTHROPIC_API_KEY,
18
+ AVAILABLE_MODELS,
19
+ REFINEMENT_MODELS,
20
+ LLAMA_MODELS,
21
+ MAX_TOKENS_MAP,
22
+ OPENAI_MODELS,
23
+ ANTHROPIC_MODELS,
24
+ HUGGINGFACE_MODELS
25
+ )
26
+
27
+ # Cliente HuggingFace
28
+ hf_client = InferenceClient(
29
+ provider="together",
30
+ api_key=HUGGINGFACE_API_KEY
31
+ )
32
+
33
+ # Cliente OpenAI
34
+ openai_client = None
35
+ if OPENAI_API_KEY:
36
+ openai_client = ChatOpenAI(
37
+ api_key=OPENAI_API_KEY,
38
+ temperature=0
39
+ )
40
+
41
+ # Cliente Anthropic
42
+ anthropic_client = None
43
+ if ANTHROPIC_API_KEY:
44
+ anthropic_client = ChatAnthropic(
45
+ model="claude-3-5-sonnet-20241022",
46
+ api_key=ANTHROPIC_API_KEY,
47
+ temperature=0
48
+ )
49
+
50
+ def generate_initial_context(db_sample: pd.DataFrame) -> str:
51
+ """
52
+ Gera contexto inicial para o modelo LLM
53
+
54
+ Args:
55
+ db_sample: Amostra dos dados do banco
56
+
57
+ Returns:
58
+ String com o contexto formatado
59
+ """
60
+ return (
61
+ f"Você é um assistente especializado em gerar queries SQL precisas e otimizadas. Analise cuidadosamente a estrutura da tabela e a pergunta do usuário.\n\n"
62
+
63
+ "**REGRAS ESSENCIAIS**:\n"
64
+ "2. Para buscar texto parcial use LIKE '%termo%'.\n"
65
+ "3. Para NULL use IS NULL ou IS NOT NULL (nunca = NULL).\n"
66
+ "4. Em agregações (SUM, COUNT, AVG) use GROUP BY nas colunas não agregadas.\n"
67
+ "5. Para datas use formato 'YYYY-MM-DD' ou funções date() do SQLite.\n"
68
+ "6. Nomes de colunas devem ser EXATAMENTE como mostrado.\n"
69
+ "- Detecte o idioma da pergunta e responda no mesmo idioma\n"
70
+ )
71
+
72
+ def is_greeting(user_query: str) -> bool:
73
+ """
74
+ Verifica se a query do usuário é uma saudação
75
+
76
+ Args:
77
+ user_query: Query do usuário
78
+
79
+ Returns:
80
+ True se for saudação, False caso contrário
81
+ """
82
+ greetings = ["olá", "oi", "bom dia", "boa tarde", "boa noite", "oi, tudo bem?"]
83
+ return user_query.lower().strip() in greetings
84
+
85
+ def detect_query_type(user_query: str) -> str:
86
+ """
87
+ Detecta o tipo de processamento necessário para a query do usuário
88
+
89
+ Args:
90
+ user_query: Pergunta do usuário
91
+
92
+ Returns:
93
+ Tipo de processamento: 'sql_query', 'sql_query_graphic', 'prediction', 'chart'
94
+ """
95
+ query_lower = user_query.lower().strip()
96
+
97
+ # Palavras-chave para diferentes tipos
98
+ prediction_keywords = ['prever', 'predizer', 'previsão', 'forecast', 'predict', 'tendência', 'projeção']
99
+
100
+ # Palavras-chave para gráficos - expandida para melhor detecção
101
+ chart_keywords = [
102
+ 'gráfico', 'grafico', 'chart', 'plot', 'visualizar', 'visualização', 'visualizacao',
103
+ 'mostrar gráfico', 'mostrar grafico', 'gerar gráfico', 'gerar grafico',
104
+ 'criar gráfico', 'criar grafico', 'plotar', 'desenhar gráfico', 'desenhar grafico',
105
+ 'exibir gráfico', 'exibir grafico', 'fazer gráfico', 'fazer grafico',
106
+ 'gráfico de', 'grafico de', 'em gráfico', 'em grafico',
107
+ 'barras', 'linha', 'pizza', 'área', 'area', 'histograma',
108
+ 'scatter', 'dispersão', 'dispersao', 'boxplot', 'heatmap'
109
+ ]
110
+
111
+ # Verifica se há solicitação de gráfico
112
+ has_chart_request = any(keyword in query_lower for keyword in chart_keywords)
113
+
114
+ # Verifica se há solicitação de previsão
115
+ has_prediction_request = any(keyword in query_lower for keyword in prediction_keywords)
116
+
117
+ # Lógica de detecção
118
+ if has_prediction_request:
119
+ return 'prediction' # Futuro: agente de ML/previsões
120
+ elif has_chart_request:
121
+ return 'sql_query_graphic' # SQL + Gráfico
122
+ else:
123
+ return 'sql_query' # SQL normal
124
+
125
+ def prepare_sql_context(user_query: str, db_sample: pd.DataFrame) -> str:
126
+ """
127
+ Prepara o contexto inicial para ser enviado diretamente ao agentSQL
128
+
129
+ Args:
130
+ user_query: Pergunta do usuário
131
+ db_sample: Amostra dos dados do banco
132
+
133
+ Returns:
134
+ Contexto formatado para o agentSQL
135
+ """
136
+ # Usa o contexto base do generate_initial_context
137
+ base_context = generate_initial_context(db_sample)
138
+
139
+ context = (
140
+ f"""
141
+ Você é um assistente especializado em consultas SQL e análise de dados.
142
+
143
+ REGRAS OBRIGATORIAS:
144
+ - “Retorne exclusivamente os resultados da consulta em formato legível, sem incluir o texto da query SQL executada ou qualquer explicação sobre ela.”
145
+
146
+ IMPORTANTE:
147
+ - Responda SEMPRE em português brasileiro, independentemente do idioma da pergunta.
148
+ - Mantenha suas respostas consistentes, claras e objetivas.
149
+ - O nome da tabela é "tabela".
150
+ - Os dados são de logística de entregas de produtos.
151
+ - Realize TODOS os cálculos aritméticos diretamente dentro da query SQL.
152
+ - NÃO realize cálculos fora da query.
153
+ - Use funções SQL como AVG, SUM, COUNT, MAX, MIN, CASE WHEN, etc., conforme necessário.
154
+ """
155
+ "\n\n"
156
+ f"**PERGUNTA DO USUÁRIO**:\n{user_query}"
157
+ )
158
+
159
+ return context
160
+
161
+ async def refine_response_with_llm(
162
+ user_question: str,
163
+ sql_response: str,
164
+ chart_md: str = ""
165
+ ) -> str:
166
+ """
167
+ Refina a resposta usando um modelo LLM adicional
168
+
169
+ Args:
170
+ user_question: Pergunta original do usuário
171
+ sql_response: Resposta do agente SQL
172
+ chart_md: Markdown de gráficos (opcional)
173
+
174
+ Returns:
175
+ Resposta refinada
176
+ """
177
+ prompt = (
178
+ f"Pergunta do usuário:\n{user_question}\n\n"
179
+ f"Resposta gerada pelo agente SQL:\n{sql_response}\n\n"
180
+ "Sua tarefa é refinar a resposta para deixá-la mais clara, completa e compreensível em português, "
181
+ "mantendo a resposta original no início do texto e adicionando insights úteis sobre logística de entregas de produtos, "
182
+ "por exemplo: comparar com padrões típicos, identificar possíveis problemas ou sugerir ações para melhorar atrasos, performance ou custos. "
183
+ "Evite repetir informações sem necessidade e não invente dados."
184
+ )
185
+
186
+ logging.info(f"[DEBUG] Prompt enviado ao modelo de refinamento:\n{prompt}\n")
187
+
188
+ try:
189
+ response = hf_client.chat.completions.create(
190
+ model=REFINEMENT_MODELS["LLaMA 70B"],
191
+ messages=[{"role": "system", "content": prompt}],
192
+ max_tokens=1200,
193
+ stream=False
194
+ )
195
+ improved_response = response["choices"][0]["message"]["content"]
196
+ logging.info(f"[DEBUG] Resposta do modelo de refinamento:\n{improved_response}\n")
197
+ return improved_response + ("\n\n" + chart_md if chart_md else "")
198
+
199
+ except Exception as e:
200
+ logging.error(f"[ERRO] Falha ao refinar resposta com LLM: {e}")
201
+ return sql_response + ("\n\n" + chart_md if chart_md else "")
202
+
203
+ class CacheManager:
204
+ """Gerenciador de cache para queries"""
205
+
206
+ def __init__(self):
207
+ self.query_cache: Dict[str, str] = {}
208
+ self.history_log: List[Dict[str, Any]] = []
209
+ self.recent_history: List[Dict[str, str]] = []
210
+
211
+ def get_cached_response(self, query: str) -> Optional[str]:
212
+ """Obtém resposta do cache"""
213
+ return self.query_cache.get(query)
214
+
215
+ def cache_response(self, query: str, response: str):
216
+ """Armazena resposta no cache"""
217
+ self.query_cache[query] = response
218
+
219
+ def add_to_history(self, entry: Dict[str, Any]):
220
+ """Adiciona entrada ao histórico"""
221
+ self.history_log.append(entry)
222
+
223
+ def update_recent_history(self, user_input: str, response: str):
224
+ """Atualiza histórico recente"""
225
+ self.recent_history.append({"role": "user", "content": user_input})
226
+ self.recent_history.append({"role": "assistant", "content": response})
227
+
228
+ # Mantém apenas as últimas 4 entradas (2 pares pergunta-resposta)
229
+ if len(self.recent_history) > 4:
230
+ self.recent_history.pop(0)
231
+ self.recent_history.pop(0)
232
+
233
+ def clear_cache(self):
234
+ """Limpa todo o cache"""
235
+ self.query_cache.clear()
236
+ self.history_log.clear()
237
+ self.recent_history.clear()
238
+
239
+ def get_history(self) -> List[Dict[str, Any]]:
240
+ """Retorna histórico completo"""
241
+ return self.history_log
242
+
243
+ # ==================== FUNÇÕES DE GRÁFICOS ====================
244
+
245
+ def generate_graph_type_context(user_query: str, sql_query: str, df_columns: List[str], df_sample: pd.DataFrame) -> str:
246
+ """
247
+ Gera contexto para LLM escolher o tipo de gráfico mais adequado
248
+
249
+ Args:
250
+ user_query: Pergunta original do usuário
251
+ sql_query: Query SQL gerada pelo agente
252
+ df_columns: Lista de colunas retornadas pela query
253
+ df_sample: Amostra dos dados para análise
254
+
255
+ Returns:
256
+ Contexto formatado para a LLM
257
+ """
258
+ # Criar uma descrição detalhada dos dados para ajudar a LLM a entender melhor a estrutura
259
+ data_description = ""
260
+ if not df_sample.empty:
261
+ # Verificar tipos de dados de forma mais robusta
262
+ numeric_cols = []
263
+ date_cols = []
264
+ categorical_cols = []
265
+
266
+ for col in df_sample.columns:
267
+ col_data = df_sample[col]
268
+
269
+ # Verifica se é numérico (incluindo strings que representam números)
270
+ try:
271
+ # Tenta converter para numérico, tratando vírgulas como separador decimal
272
+ if col_data.dtype == 'object':
273
+ test_numeric = pd.to_numeric(col_data.astype(str).str.replace(',', '.'), errors='coerce')
274
+ if test_numeric.notna().sum() > len(col_data) * 0.8: # 80% são números válidos
275
+ numeric_cols.append(col)
276
+ else:
277
+ categorical_cols.append(col)
278
+ elif pd.api.types.is_numeric_dtype(col_data):
279
+ numeric_cols.append(col)
280
+ elif pd.api.types.is_datetime64_any_dtype(col_data) or 'data' in col.lower():
281
+ date_cols.append(col)
282
+ else:
283
+ categorical_cols.append(col)
284
+ except:
285
+ categorical_cols.append(col)
286
+
287
+ # Adicionar informações sobre os primeiros valores de cada coluna
288
+ data_description = "\nAmostra dos dados (primeiras 3 linhas):\n"
289
+ data_description += df_sample.head(3).to_string(index=False)
290
+
291
+ # Adicionar análise detalhada dos tipos de dados
292
+ data_description += f"\n\nAnálise dos dados ({len(df_sample)} linhas total):"
293
+ data_description += f"\n- Total de colunas: {len(df_sample.columns)}"
294
+
295
+ if numeric_cols:
296
+ data_description += f"\n- Colunas NUMÉRICAS ({len(numeric_cols)}): {', '.join(numeric_cols)}"
297
+ # Adiciona informação sobre valores numéricos
298
+ for col in numeric_cols[:2]: # Máximo 2 colunas para não ficar muito longo
299
+ try:
300
+ if df_sample[col].dtype == 'object':
301
+ # Converte strings para números
302
+ numeric_values = pd.to_numeric(df_sample[col].astype(str).str.replace(',', '.'), errors='coerce')
303
+ min_val, max_val = numeric_values.min(), numeric_values.max()
304
+ else:
305
+ min_val, max_val = df_sample[col].min(), df_sample[col].max()
306
+ data_description += f"\n • {col}: valores de {min_val} a {max_val}"
307
+ except:
308
+ pass
309
+
310
+ if date_cols:
311
+ data_description += f"\n- Colunas de DATA/TEMPO ({len(date_cols)}): {', '.join(date_cols)}"
312
+
313
+ if categorical_cols:
314
+ data_description += f"\n- Colunas CATEGÓRICAS ({len(categorical_cols)}): {', '.join(categorical_cols)}"
315
+ # Adiciona informação sobre categorias únicas
316
+ for col in categorical_cols[:3]: # Máximo 3 colunas
317
+ unique_count = df_sample[col].nunique()
318
+ data_description += f"\n • {col}: {unique_count} valores únicos"
319
+
320
+ # Destaque especial para múltiplas categóricas importantes
321
+ if len(categorical_cols) >= 2 and len(numeric_cols) >= 1:
322
+ data_description += f"\n\n⚠️ ATENÇÃO: {len(categorical_cols)} colunas categóricas + {len(numeric_cols)} numérica(s) → CONSIDERE GRÁFICO AGRUPADO (6) para mostrar múltiplas dimensões!"
323
+
324
+ # Prompt ULTRA SIMPLIFICADO
325
+ return (
326
+ f"Escolha o gráfico mais adequado e de acordo com pergunta do usuário e os dados:\n\n"
327
+ f"COLUNAS RETORNADAS: {', '.join(df_columns)}\n\n"
328
+ f"DADOS: {data_description}\n\n"
329
+ f"PERGUNTA: {user_query}\n\n"
330
+ f"OPÇÕES DE GRÁFICOS::\n"
331
+ f"1. Linha - evolução temporal\n"
332
+ f"2. Multilinhas - múltiplas tendências\n"
333
+ f"3. Área - volume temporal\n"
334
+ f"4. Barras Verticais - comparar categorias (nomes curtos)\n"
335
+ f"5. Barras Horizontais - comparar categorias (nomes longos)\n"
336
+ f"6. Barras Agrupadas - múltiplas métricas\n"
337
+ f"7. Barras Empilhadas - partes de um todo\n"
338
+ f"8. Pizza - proporções (poucas categorias)\n"
339
+ f"9. Dona - proporções (muitas categorias)\n"
340
+ f"10. Pizzas Múltiplas - proporções por grupos\n\n"
341
+ f"Responda apenas o número (1-10)."
342
+ "\n\nINSTRUÇÕES FINAIS:\n"
343
+ "1. PRIMEIRO: Verifique se o usuário especificou um tipo de gráfico na pergunta do usuário\n"
344
+ "2. SE SIM: Use o gráfico solicitado (consulte o mapeamento acima)\n"
345
+ "3. SE NÃO: Escolha o gráfico mais adequado\n\n"
346
+ )
347
+
348
+ def extract_sql_query_from_response(agent_response: str) -> Optional[str]:
349
+ """
350
+ Extrai a query SQL da resposta do agente SQL
351
+
352
+ Args:
353
+ agent_response: Resposta completa do agente SQL
354
+
355
+ Returns:
356
+ Query SQL extraída ou None se não encontrada
357
+ """
358
+ if not agent_response:
359
+ return None
360
+
361
+ # Padrões para encontrar SQL na resposta - ordem de prioridade
362
+ sql_patterns = [
363
+ # Padrão mais comum: ```sql ... ``` (multiline)
364
+ r"```sql\s*(.*?)\s*```",
365
+ # Padrão alternativo: ``` ... ``` com SELECT (multiline)
366
+ r"```\s*(SELECT.*?)\s*```",
367
+ # SELECT com múltiplas linhas até ponto e vírgula
368
+ r"(SELECT\s+.*?;)",
369
+ # SELECT com múltiplas linhas até quebra dupla ou final
370
+ r"(SELECT\s+.*?)(?:\n\s*\n|\n\s*$|\n\s*Agora|\n\s*Em seguida)",
371
+ # Padrões com prefixos específicos
372
+ r"Query:\s*(SELECT.*?)(?:\n|$|;)",
373
+ r"SQL:\s*(SELECT.*?)(?:\n|$|;)",
374
+ r"Consulta:\s*(SELECT.*?)(?:\n|$|;)",
375
+ # SELECT em uma linha
376
+ r"(SELECT\s+[^\n]+)",
377
+ ]
378
+
379
+ for i, pattern in enumerate(sql_patterns):
380
+ matches = re.findall(pattern, agent_response, re.DOTALL | re.IGNORECASE)
381
+ if matches:
382
+ # Pega a primeira query encontrada
383
+ query = matches[0].strip()
384
+
385
+ # Limpa a query
386
+ query = clean_sql_query(query)
387
+
388
+ # Verifica se é uma query válida
389
+ if is_valid_sql_query(query):
390
+ logging.info(f"[GRAPH] Query SQL extraída (padrão {i+1}): {query[:100]}...")
391
+ return query
392
+
393
+ # Log da resposta para debug se não encontrar SQL
394
+ logging.warning(f"[GRAPH] Não foi possível extrair query SQL. Resposta (primeiros 200 chars): {agent_response[:200]}...")
395
+ return None
396
+
397
+ def clean_sql_query(query: str) -> str:
398
+ """
399
+ Limpa e normaliza a query SQL extraída
400
+
401
+ Args:
402
+ query: Query SQL bruta
403
+
404
+ Returns:
405
+ Query SQL limpa
406
+ """
407
+ if not query:
408
+ return ""
409
+
410
+ # Remove espaços extras e quebras de linha desnecessárias
411
+ query = re.sub(r'\s+', ' ', query.strip())
412
+
413
+ # Remove ponto e vírgula no final se existir
414
+ if query.endswith(';'):
415
+ query = query[:-1].strip()
416
+
417
+ # Remove aspas ou caracteres especiais no início/fim
418
+ query = query.strip('`"\'')
419
+
420
+ return query
421
+
422
+ def is_valid_sql_query(query: str) -> bool:
423
+ """
424
+ Verifica se a string é uma query SQL válida
425
+
426
+ Args:
427
+ query: String para verificar
428
+
429
+ Returns:
430
+ True se for uma query SQL válida
431
+ """
432
+ if not query or len(query.strip()) < 6: # Mínimo para "SELECT"
433
+ return False
434
+
435
+ # Verifica se começa com comando SQL válido
436
+ sql_commands = ['SELECT', 'INSERT', 'UPDATE', 'DELETE', 'WITH']
437
+ query_upper = query.strip().upper()
438
+
439
+ return any(query_upper.startswith(cmd) for cmd in sql_commands)
app.py ADDED
@@ -0,0 +1,452 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ AgentGraph - Aplicação principal com interface Gradio e LangGraph
3
+ """
4
+ import asyncio
5
+ import logging
6
+ import gradio as gr
7
+ import tempfile
8
+ import os
9
+ from typing import List, Tuple, Optional, Dict
10
+ from PIL import Image
11
+
12
+ from graphs.main_graph import initialize_graph, get_graph_manager
13
+ from utils.config import (
14
+ AVAILABLE_MODELS,
15
+ DEFAULT_MODEL,
16
+ GRADIO_SHARE,
17
+ GRADIO_PORT,
18
+ validate_config,
19
+ is_langsmith_enabled,
20
+ LANGSMITH_PROJECT
21
+ )
22
+ from utils.object_manager import get_object_manager
23
+
24
+ # Configuração de logging
25
+ logging.basicConfig(
26
+ level=logging.INFO,
27
+ format='%(asctime)s - %(levelname)s - %(message)s'
28
+ )
29
+
30
+ # Variáveis globais
31
+ graph_manager = None
32
+ show_history_flag = False
33
+
34
+ async def initialize_app():
35
+ """Inicializa a aplicação"""
36
+ global graph_manager
37
+
38
+ try:
39
+ # Valida configurações
40
+ validate_config()
41
+
42
+ # Inicializa o grafo
43
+ graph_manager = await initialize_graph()
44
+
45
+ # Informa sobre o status do LangSmith
46
+ if is_langsmith_enabled():
47
+ logging.info(f"✅ LangSmith habilitado - Projeto: '{LANGSMITH_PROJECT}'")
48
+ logging.info("🔍 Traces serão enviados para LangSmith automaticamente")
49
+ else:
50
+ logging.info("ℹ️ LangSmith não configurado - Executando sem observabilidade")
51
+
52
+ logging.info("Aplicação inicializada com sucesso")
53
+ return True
54
+
55
+ except Exception as e:
56
+ logging.error(f"Erro ao inicializar aplicação: {e}")
57
+ return False
58
+
59
+ def run_async(coro):
60
+ """Executa corrotina de forma síncrona"""
61
+ try:
62
+ loop = asyncio.get_event_loop()
63
+ except RuntimeError:
64
+ loop = asyncio.new_event_loop()
65
+ asyncio.set_event_loop(loop)
66
+
67
+ return loop.run_until_complete(coro)
68
+
69
+ def chatbot_response(user_input: str, selected_model: str, advanced_mode: bool = False) -> Tuple[str, Optional[str]]:
70
+ """
71
+ Processa resposta do chatbot usando LangGraph
72
+
73
+ Args:
74
+ user_input: Entrada do usuário
75
+ selected_model: Modelo LLM selecionado
76
+ advanced_mode: Se deve usar refinamento avançado
77
+
78
+ Returns:
79
+ Tupla com (resposta_texto, caminho_imagem_grafico)
80
+ """
81
+ global graph_manager
82
+
83
+ if not graph_manager:
84
+ return "❌ Sistema não inicializado. Tente recarregar a página.", None
85
+
86
+ try:
87
+ # Processa query através do LangGraph
88
+ result = run_async(graph_manager.process_query(
89
+ user_input=user_input,
90
+ selected_model=selected_model,
91
+ advanced_mode=advanced_mode
92
+ ))
93
+
94
+ response_text = result.get("response", "Erro ao processar resposta")
95
+ graph_image_path = None
96
+
97
+ # Verifica se foi gerado um gráfico
98
+ if result.get("graph_generated", False) and result.get("graph_image_id"):
99
+ graph_image_path = save_graph_image_to_temp(result["graph_image_id"])
100
+
101
+ # Adiciona informação sobre o gráfico na resposta
102
+ if graph_image_path:
103
+ graph_type = result.get("graph_type", "gráfico")
104
+ response_text += f"\n\n📊 **Gráfico gerado**: {graph_type.replace('_', ' ').title()}"
105
+
106
+ return response_text, graph_image_path
107
+
108
+ except Exception as e:
109
+ error_msg = f"Erro no chatbot: {e}"
110
+ logging.error(error_msg)
111
+ logging.error(f"Detalhes do erro: {type(e).__name__}: {str(e)}")
112
+ return error_msg, None
113
+
114
+ def save_graph_image_to_temp(graph_image_id: str) -> Optional[str]:
115
+ """
116
+ Salva imagem do gráfico em arquivo temporário para exibição no Gradio
117
+
118
+ Args:
119
+ graph_image_id: ID da imagem no ObjectManager
120
+
121
+ Returns:
122
+ Caminho do arquivo temporário ou None se falhar
123
+ """
124
+ try:
125
+ obj_manager = get_object_manager()
126
+ graph_image = obj_manager.get_object(graph_image_id)
127
+
128
+ if graph_image and isinstance(graph_image, Image.Image):
129
+ # Cria arquivo temporário
130
+ temp_file = tempfile.NamedTemporaryFile(delete=False, suffix='.png')
131
+ graph_image.save(temp_file.name, format='PNG')
132
+ temp_file.close()
133
+
134
+ logging.info(f"[GRADIO] Gráfico salvo em: {temp_file.name}")
135
+ return temp_file.name
136
+
137
+ except Exception as e:
138
+ logging.error(f"[GRADIO] Erro ao salvar gráfico: {e}")
139
+
140
+ return None
141
+
142
+ def handle_csv_upload(file) -> str:
143
+ """
144
+ Processa upload de arquivo CSV
145
+
146
+ Args:
147
+ file: Arquivo enviado pelo Gradio
148
+
149
+ Returns:
150
+ Mensagem de feedback
151
+ """
152
+ global graph_manager
153
+
154
+ if not graph_manager:
155
+ return "❌ Sistema não inicializado."
156
+
157
+ if not file:
158
+ return "❌ Nenhum arquivo selecionado."
159
+
160
+ try:
161
+ # Log detalhado do arquivo recebido
162
+ logging.info(f"[UPLOAD] Arquivo recebido: {file}")
163
+ logging.info(f"[UPLOAD] Nome do arquivo: {file.name}")
164
+ logging.info(f"[UPLOAD] Tipo do arquivo: {type(file)}")
165
+
166
+ # Verifica se o arquivo existe
167
+ import os
168
+ if not os.path.exists(file.name):
169
+ return f"❌ Arquivo não encontrado: {file.name}"
170
+
171
+ # Verifica se é um arquivo CSV
172
+ if not file.name.lower().endswith('.csv'):
173
+ return "❌ Por favor, selecione um arquivo CSV válido."
174
+
175
+ # Verifica o tamanho do arquivo
176
+ file_size = os.path.getsize(file.name)
177
+ file_size_mb = file_size / (1024 * 1024)
178
+ file_size_gb = file_size / (1024 * 1024 * 1024)
179
+
180
+ if file_size_gb >= 1:
181
+ size_str = f"{file_size_gb:.2f} GB"
182
+ else:
183
+ size_str = f"{file_size_mb:.2f} MB"
184
+
185
+ logging.info(f"[UPLOAD] Tamanho do arquivo: {file_size} bytes ({size_str})")
186
+
187
+ if file_size == 0:
188
+ return "❌ O arquivo está vazio."
189
+
190
+ if file_size > 5 * 1024 * 1024 * 1024: # 5GB
191
+ return "❌ Arquivo muito grande. Máximo permitido: 5GB."
192
+
193
+ # Aviso para arquivos grandes
194
+ if file_size_mb > 100:
195
+ logging.info(f"[UPLOAD] Arquivo grande detectado ({size_str}). Processamento pode demorar...")
196
+ return f"⏳ Processando arquivo grande ({size_str}). Aguarde..."
197
+
198
+ # Processa upload através do LangGraph
199
+ logging.info(f"[UPLOAD] Iniciando processamento do arquivo: {file.name}")
200
+ result = run_async(graph_manager.handle_csv_upload(file.name))
201
+
202
+ logging.info(f"[UPLOAD] Resultado do processamento: {result}")
203
+ return result.get("message", "Erro no upload")
204
+
205
+ except Exception as e:
206
+ error_msg = f"❌ Erro ao processar upload: {e}"
207
+ logging.error(error_msg)
208
+ logging.error(f"[UPLOAD] Detalhes do erro: {type(e).__name__}: {str(e)}")
209
+ import traceback
210
+ logging.error(f"[UPLOAD] Traceback: {traceback.format_exc()}")
211
+ return error_msg
212
+
213
+ def reset_system() -> str:
214
+ """
215
+ Reseta o sistema ao estado inicial
216
+
217
+ Returns:
218
+ Mensagem de feedback
219
+ """
220
+ global graph_manager
221
+
222
+ if not graph_manager:
223
+ return "❌ Sistema não inicializado."
224
+
225
+ try:
226
+ # Reseta sistema através do LangGraph
227
+ result = run_async(graph_manager.reset_system())
228
+
229
+ return result.get("message", "Erro no reset")
230
+
231
+ except Exception as e:
232
+ error_msg = f"❌ Erro ao resetar sistema: {e}"
233
+ logging.error(error_msg)
234
+ return error_msg
235
+
236
+ def toggle_advanced_mode(enabled: bool) -> str:
237
+ """
238
+ Alterna modo avançado
239
+
240
+ Args:
241
+ enabled: Se deve habilitar modo avançado
242
+
243
+ Returns:
244
+ Mensagem de status
245
+ """
246
+ global graph_manager
247
+
248
+ if not graph_manager:
249
+ return "❌ Sistema não inicializado."
250
+
251
+ return graph_manager.toggle_advanced_mode(enabled)
252
+
253
+ def toggle_history():
254
+ """Alterna exibição do histórico"""
255
+ global show_history_flag, graph_manager
256
+
257
+ show_history_flag = not show_history_flag
258
+
259
+ if show_history_flag and graph_manager:
260
+ return graph_manager.get_history()
261
+ else:
262
+ return {}
263
+
264
+ def respond(message: str, chat_history: List[Dict[str, str]], selected_model: str, advanced_mode: bool):
265
+ """
266
+ Função de resposta para o chatbot Gradio
267
+
268
+ Args:
269
+ message: Mensagem do usuário
270
+ chat_history: Histórico do chat (formato messages)
271
+ selected_model: Modelo selecionado
272
+ advanced_mode: Modo avançado habilitado
273
+
274
+ Returns:
275
+ Tupla com (mensagem_vazia, histórico_atualizado, imagem_grafico)
276
+ """
277
+ if not message.strip():
278
+ return "", chat_history, None
279
+
280
+ # Processa resposta
281
+ response, graph_image_path = chatbot_response(message, selected_model, advanced_mode)
282
+
283
+ # Atualiza histórico no formato messages
284
+ chat_history.append({"role": "user", "content": message})
285
+ chat_history.append({"role": "assistant", "content": response})
286
+
287
+ return "", chat_history, graph_image_path
288
+
289
+ def handle_csv_and_clear_chat(file):
290
+ """
291
+ Processa CSV e limpa chat
292
+
293
+ Args:
294
+ file: Arquivo CSV
295
+
296
+ Returns:
297
+ Tupla com (feedback, chat_limpo, grafico_limpo)
298
+ """
299
+ feedback = handle_csv_upload(file)
300
+ return feedback, [], gr.update(visible=False)
301
+
302
+ def reset_all():
303
+ """
304
+ Reseta tudo e limpa interface
305
+
306
+ Returns:
307
+ Tupla com (feedback, chat_limpo, arquivo_limpo, grafico_limpo)
308
+ """
309
+ feedback = reset_system()
310
+ return feedback, [], None, gr.update(visible=False)
311
+
312
+ # Interface Gradio
313
+ def create_interface():
314
+ """Cria interface Gradio"""
315
+
316
+ # CSS customizado para pequeno espaçamento lateral
317
+ custom_css = """
318
+ .gradio-container {
319
+ padding: 20px 30px !important;
320
+ }
321
+ """
322
+
323
+ with gr.Blocks(theme=gr.themes.Soft(), css=custom_css) as demo:
324
+
325
+ with gr.Row():
326
+ with gr.Column(scale=1):
327
+ gr.Markdown("## Configurações")
328
+ model_selector = gr.Dropdown(list(AVAILABLE_MODELS.keys()), value=DEFAULT_MODEL, label="")
329
+ csv_file = gr.File(file_types=[".csv"], label="")
330
+ upload_feedback = gr.Markdown()
331
+ advanced_checkbox = gr.Checkbox(label="Refinar Resposta")
332
+
333
+ # Status do LangSmith
334
+ if is_langsmith_enabled():
335
+ gr.Markdown(f"🔍 **LangSmith**: Ativo")
336
+ else:
337
+ gr.Markdown("🔍 **LangSmith**: Desabilitado")
338
+
339
+ reset_btn = gr.Button("Resetar")
340
+
341
+ with gr.Column(scale=4):
342
+ gr.Markdown("## Agent86")
343
+ chatbot = gr.Chatbot(
344
+ height=600,
345
+ show_label=False,
346
+ container=True,
347
+ type="messages"
348
+ )
349
+
350
+ msg = gr.Textbox(placeholder="Digite sua pergunta aqui...", lines=1, label="")
351
+ btn = gr.Button("Enviar", variant="primary")
352
+ history_btn = gr.Button("Histórico", variant="secondary")
353
+ history_output = gr.JSON()
354
+
355
+ # Componente para exibir gráficos - posicionado após histórico
356
+ graph_image = gr.Image(
357
+ label="📊 Visualização de Dados",
358
+ visible=False,
359
+ height=500, # Altura maior para ocupar mais espaço
360
+ show_label=True,
361
+ container=True,
362
+ interactive=False,
363
+ show_download_button=True
364
+ )
365
+
366
+ download_file = gr.File(visible=False)
367
+
368
+ # Event handlers (usando as funções originais do sistema)
369
+ def handle_response_with_graph(message, chat_history, model, advanced):
370
+ """Wrapper para lidar com resposta e gráfico"""
371
+ empty_msg, updated_history, graph_path = respond(message, chat_history, model, advanced)
372
+
373
+ # Controla visibilidade do componente de gráfico
374
+ if graph_path:
375
+ return empty_msg, updated_history, gr.update(value=graph_path, visible=True)
376
+ else:
377
+ return empty_msg, updated_history, gr.update(visible=False)
378
+
379
+ msg.submit(
380
+ handle_response_with_graph,
381
+ inputs=[msg, chatbot, model_selector, advanced_checkbox],
382
+ outputs=[msg, chatbot, graph_image]
383
+ )
384
+
385
+ btn.click(
386
+ handle_response_with_graph,
387
+ inputs=[msg, chatbot, model_selector, advanced_checkbox],
388
+ outputs=[msg, chatbot, graph_image]
389
+ )
390
+
391
+ csv_file.change(
392
+ handle_csv_and_clear_chat,
393
+ inputs=csv_file,
394
+ outputs=[upload_feedback, chatbot, graph_image]
395
+ )
396
+
397
+ reset_btn.click(
398
+ reset_all,
399
+ outputs=[upload_feedback, chatbot, csv_file, graph_image]
400
+ )
401
+
402
+ advanced_checkbox.change(
403
+ toggle_advanced_mode,
404
+ inputs=advanced_checkbox,
405
+ outputs=[]
406
+ )
407
+
408
+ history_btn.click(
409
+ toggle_history,
410
+ outputs=history_output
411
+ )
412
+
413
+ return demo
414
+
415
+ async def main():
416
+ """Função principal"""
417
+ # Inicializa aplicação
418
+ success = await initialize_app()
419
+
420
+ if not success:
421
+ logging.error("Falha na inicialização. Encerrando aplicação.")
422
+ return
423
+
424
+ # Cria e lança interface
425
+ demo = create_interface()
426
+
427
+ # Tenta diferentes portas se a padrão estiver ocupada
428
+ ports_to_try = [GRADIO_PORT, 7861, 7862, 7863, 7864, 0] # 0 = porta automática
429
+
430
+ for port in ports_to_try:
431
+ try:
432
+ logging.info(f"Tentando iniciar interface Gradio na porta {port}")
433
+ demo.launch(
434
+ share=GRADIO_SHARE,
435
+ server_port=port if port != 0 else None,
436
+ show_error=True,
437
+ quiet=False
438
+ )
439
+ break # Se chegou aqui, deu certo
440
+ except OSError as e:
441
+ if "Cannot find empty port" in str(e) and port != ports_to_try[-1]:
442
+ logging.warning(f"Porta {port} ocupada, tentando próxima...")
443
+ continue
444
+ else:
445
+ logging.error(f"Erro ao iniciar servidor: {e}")
446
+ raise
447
+ except Exception as e:
448
+ logging.error(f"Erro inesperado ao iniciar interface: {e}")
449
+ raise
450
+
451
+ if __name__ == "__main__":
452
+ run_async(main())
env.example.txt ADDED
@@ -0,0 +1,61 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # ========================================
2
+ # AgentGraph - Configurações de Ambiente
3
+ # ========================================
4
+
5
+ # 🔑 API Keys (pelo menos uma é obrigatória)
6
+ HUGGINGFACE_API_KEY=hf_your_key_here
7
+ OPENAI_API_KEY=sk-your_key_here
8
+ ANTHROPIC_API_KEY=sk-ant-your_key_here
9
+
10
+ # 🔍 LangSmith - Observabilidade (OPCIONAL)
11
+ # Obtenha sua API key em: https://smith.langchain.com/
12
+ LANGSMITH_API_KEY=lsv2_pt_your_key_here
13
+ LANGSMITH_TRACING=true
14
+ LANGSMITH_ENDPOINT=https://api.smith.langchain.com
15
+ LANGSMITH_PROJECT=agentgraph-project
16
+
17
+ # 🗄️ Configurações de Banco
18
+ SQL_DB_PATH=data.db
19
+ DEFAULT_CSV_PATH=tabela.csv
20
+ UPLOAD_DIR=uploaded_data
21
+
22
+ # 🤖 Configurações de Modelo
23
+ DEFAULT_MODEL=GPT-4o-mini
24
+ MAX_ITERATIONS=40
25
+ TEMPERATURE=0
26
+
27
+ # 🌐 Configurações do Gradio
28
+ GRADIO_SHARE=false
29
+ GRADIO_PORT=7860
30
+
31
+ # 📊 Configurações de Logging
32
+ LOG_LEVEL=INFO
33
+
34
+ # ========================================
35
+ # Instruções de Configuração
36
+ # ========================================
37
+
38
+ # 1. Copie este arquivo para .env:
39
+ # cp .env.example .env
40
+
41
+ # 2. Configure pelo menos uma API key (OpenAI, Anthropic ou HuggingFace)
42
+
43
+ # 3. Para habilitar LangSmith (observabilidade):
44
+ # - Crie conta em https://smith.langchain.com/
45
+ # - Obtenha sua API key
46
+ # - Configure LANGSMITH_API_KEY e LANGSMITH_TRACING=true
47
+ # - Personalize LANGSMITH_PROJECT se desejar
48
+
49
+ # 4. Execute a aplicação:
50
+ # python app.py
51
+
52
+ # ========================================
53
+ # Benefícios do LangSmith
54
+ # ========================================
55
+
56
+ # ✅ Rastreamento completo de execuções
57
+ # ✅ Monitoramento de performance
58
+ # ✅ Debug de agentes LangGraph
59
+ # ✅ Análise de custos de tokens
60
+ # ✅ Comparação de modelos
61
+ # ✅ Dashboards de observabilidade
graphs/main_graph.py ADDED
@@ -0,0 +1,775 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Grafo principal do LangGraph para o AgentGraph
3
+ """
4
+ import logging
5
+ import pandas as pd
6
+ import re
7
+ from typing import Dict, Any, Optional
8
+ from langgraph.graph import StateGraph, END
9
+ from langgraph.checkpoint.memory import MemorySaver
10
+ from sqlalchemy import Integer, Float, DateTime
11
+
12
+ from nodes.agent_node import AgentState, should_refine_response, should_generate_graph
13
+ from nodes.csv_processing_node import csv_processing_node
14
+ from nodes.database_node import (
15
+ create_database_from_dataframe_node,
16
+ load_existing_database_node,
17
+ get_database_sample_node
18
+ )
19
+ from nodes.query_node import (
20
+ validate_query_input_node,
21
+ prepare_query_context_node,
22
+ process_user_query_node
23
+ )
24
+ from nodes.refinement_node import (
25
+ refine_response_node,
26
+ format_final_response_node
27
+ )
28
+ from nodes.cache_node import (
29
+ check_cache_node,
30
+ cache_response_node,
31
+ update_history_node
32
+ )
33
+ from nodes.graph_selection_node import graph_selection_node
34
+ from nodes.graph_generation_node import graph_generation_node
35
+ from nodes.custom_nodes import CustomNodeManager
36
+ from agents.sql_agent import SQLAgentManager
37
+ from agents.tools import CacheManager
38
+ from utils.database import create_sql_database
39
+ from utils.config import get_active_csv_path, SQL_DB_PATH
40
+ from utils.object_manager import get_object_manager
41
+
42
+ class AgentGraphManager:
43
+ """
44
+ Gerenciador principal do grafo LangGraph
45
+ """
46
+
47
+ def __init__(self):
48
+ self.graph = None
49
+ self.app = None
50
+ self.cache_manager = CacheManager()
51
+ self.custom_node_manager = CustomNodeManager()
52
+ self.object_manager = get_object_manager()
53
+ self.engine = None
54
+ self.sql_agent = None
55
+ self.db = None
56
+ # IDs para objetos não-serializáveis
57
+ self.agent_id = None
58
+ self.engine_id = None
59
+ self.db_id = None
60
+ self.cache_id = None
61
+ self._initialize_system()
62
+ self._build_graph()
63
+
64
+ def _initialize_system(self):
65
+ """Inicializa o sistema com banco e agente padrão"""
66
+ try:
67
+ # Para inicialização síncrona, vamos usar load_existing_database_node de forma síncrona
68
+ # ou criar uma versão síncrona temporária
69
+ import os
70
+ from sqlalchemy import create_engine
71
+
72
+ # Verifica se banco existe
73
+ if os.path.exists(SQL_DB_PATH):
74
+ # Carrega banco existente
75
+ self.engine = create_engine(f"sqlite:///{SQL_DB_PATH}")
76
+ db = create_sql_database(self.engine)
77
+ logging.info("Banco existente carregado")
78
+ else:
79
+ # Cria novo banco usando função síncrona temporária
80
+ csv_path = get_active_csv_path()
81
+ self.engine = self._create_engine_sync(csv_path)
82
+ db = create_sql_database(self.engine)
83
+ logging.info("Novo banco criado")
84
+
85
+ # Armazena banco de dados
86
+ self.db = db
87
+ self.db_id = self.object_manager.store_database(db)
88
+
89
+ # Cria agente SQL
90
+ self.sql_agent = SQLAgentManager(db)
91
+
92
+ # Armazena objetos no gerenciador
93
+ self.agent_id = self.object_manager.store_sql_agent(self.sql_agent, self.db_id)
94
+ self.engine_id = self.object_manager.store_engine(self.engine)
95
+ self.cache_id = self.object_manager.store_cache_manager(self.cache_manager)
96
+
97
+ logging.info("Sistema inicializado com sucesso")
98
+
99
+ except Exception as e:
100
+ logging.error(f"Erro ao inicializar sistema: {e}")
101
+ raise
102
+
103
+ def _create_engine_sync(self, csv_path: str):
104
+ """Cria engine de forma síncrona para inicialização"""
105
+ import pandas as pd
106
+ from sqlalchemy import create_engine
107
+ from sqlalchemy.types import DateTime, Integer, Float
108
+
109
+ # Lê CSV
110
+ df = pd.read_csv(csv_path, sep=';')
111
+
112
+ # Processamento inteligente de tipos
113
+ sql_types = {}
114
+ df = self._smart_type_conversion(df, sql_types)
115
+
116
+ # Cria engine e salva dados
117
+ engine = create_engine(f"sqlite:///{SQL_DB_PATH}")
118
+ df.to_sql("tabela", engine, index=False, if_exists="replace", dtype=sql_types)
119
+
120
+ logging.info(f"Banco criado com {len(df)} registros")
121
+ return engine
122
+
123
+ def _build_graph(self):
124
+ """Constrói o grafo LangGraph com nova arquitetura"""
125
+ try:
126
+ # Cria o StateGraph
127
+ workflow = StateGraph(AgentState)
128
+
129
+ # Adiciona nós de validação e preparação
130
+ workflow.add_node("validate_input", validate_query_input_node)
131
+ workflow.add_node("check_cache", check_cache_node)
132
+ workflow.add_node("prepare_context", prepare_query_context_node)
133
+ workflow.add_node("get_db_sample", get_database_sample_node)
134
+
135
+ # Adiciona nós de processamento
136
+ workflow.add_node("process_query", process_user_query_node)
137
+
138
+ # Adiciona nós de gráficos
139
+ workflow.add_node("graph_selection", graph_selection_node)
140
+ workflow.add_node("graph_generation", graph_generation_node)
141
+
142
+ # Adiciona nós de refinamento
143
+ workflow.add_node("refine_response", refine_response_node)
144
+ workflow.add_node("format_response", format_final_response_node)
145
+
146
+ # Adiciona nós de cache e histórico
147
+ workflow.add_node("cache_response", cache_response_node)
148
+ workflow.add_node("update_history", update_history_node)
149
+
150
+ # Define ponto de entrada
151
+ workflow.set_entry_point("validate_input")
152
+
153
+ # Fluxo principal
154
+ workflow.add_edge("validate_input", "check_cache")
155
+
156
+ # Condicional para cache hit
157
+ workflow.add_conditional_edges(
158
+ "check_cache",
159
+ lambda state: "update_history" if state.get("cache_hit") else "prepare_context"
160
+ )
161
+
162
+ workflow.add_edge("prepare_context", "get_db_sample")
163
+ workflow.add_edge("get_db_sample", "process_query")
164
+
165
+ # Condicional para gráficos (após AgentSQL)
166
+ workflow.add_conditional_edges(
167
+ "process_query",
168
+ should_generate_graph,
169
+ {
170
+ "graph_selection": "graph_selection",
171
+ "refine_response": "refine_response",
172
+ "cache_response": "cache_response"
173
+ }
174
+ )
175
+
176
+ # Fluxo dos gráficos
177
+ workflow.add_edge("graph_selection", "graph_generation")
178
+
179
+ # Após geração de gráfico, vai para refinamento ou cache
180
+ workflow.add_conditional_edges(
181
+ "graph_generation",
182
+ should_refine_response,
183
+ {
184
+ "refine_response": "refine_response",
185
+ "cache_response": "cache_response"
186
+ }
187
+ )
188
+
189
+ workflow.add_edge("refine_response", "format_response")
190
+ workflow.add_edge("format_response", "cache_response")
191
+ workflow.add_edge("cache_response", "update_history")
192
+ workflow.add_edge("update_history", END)
193
+
194
+ # Compila o grafo
195
+ memory = MemorySaver()
196
+ self.app = workflow.compile(checkpointer=memory)
197
+
198
+ logging.info("Grafo LangGraph construído com sucesso")
199
+
200
+ except Exception as e:
201
+ logging.error(f"Erro ao construir grafo: {e}")
202
+ raise
203
+
204
+ async def process_query(
205
+ self,
206
+ user_input: str,
207
+ selected_model: str = "GPT-4o-mini",
208
+ advanced_mode: bool = False,
209
+ thread_id: str = "default"
210
+ ) -> Dict[str, Any]:
211
+ """
212
+ Processa uma query do usuário através do grafo
213
+
214
+ Args:
215
+ user_input: Entrada do usuário
216
+ selected_model: Modelo LLM selecionado
217
+ advanced_mode: Se deve usar refinamento avançado
218
+ thread_id: ID da thread para checkpoint
219
+
220
+ Returns:
221
+ Resultado do processamento
222
+ """
223
+ try:
224
+ # Verifica se precisa recriar agente SQL com modelo diferente
225
+ current_sql_agent = self.object_manager.get_sql_agent(self.agent_id)
226
+ if current_sql_agent and current_sql_agent.model_name != selected_model:
227
+ logging.info(f"Recriando agente SQL com modelo {selected_model}")
228
+
229
+ # Recupera banco de dados associado ao agente
230
+ db_id = self.object_manager.get_db_id_for_agent(self.agent_id)
231
+ if db_id:
232
+ db = self.object_manager.get_database(db_id)
233
+ if db:
234
+ new_sql_agent = SQLAgentManager(db, selected_model)
235
+ self.agent_id = self.object_manager.store_sql_agent(new_sql_agent, db_id)
236
+ logging.info(f"Agente SQL recriado com sucesso para modelo {selected_model}")
237
+ else:
238
+ logging.error("Banco de dados não encontrado para recriar agente")
239
+ else:
240
+ logging.error("ID do banco de dados não encontrado para o agente")
241
+
242
+ # Prepara estado inicial com IDs serializáveis
243
+ initial_state = {
244
+ "user_input": user_input,
245
+ "selected_model": selected_model,
246
+ "response": "",
247
+ "advanced_mode": advanced_mode,
248
+ "execution_time": 0.0,
249
+ "error": None,
250
+ "intermediate_steps": [],
251
+ "db_sample_dict": {},
252
+ # IDs para recuperar objetos não-serializáveis
253
+ "agent_id": self.agent_id,
254
+ "engine_id": self.engine_id,
255
+ "db_id": self.db_id,
256
+ "cache_id": self.cache_id,
257
+ # Campos relacionados a gráficos
258
+ "query_type": "sql_query", # Será atualizado pela detecção
259
+ "sql_query_extracted": None,
260
+ "graph_type": None,
261
+ "graph_data": None,
262
+ "graph_image_id": None,
263
+ "graph_generated": False,
264
+ "graph_error": None
265
+ }
266
+
267
+ # Executa o grafo
268
+ config = {"configurable": {"thread_id": thread_id}}
269
+ result = await self.app.ainvoke(initial_state, config=config)
270
+
271
+ logging.info(f"Query processada com sucesso: {user_input[:50]}...")
272
+ return result
273
+
274
+ except Exception as e:
275
+ error_msg = f"Erro ao processar query: {e}"
276
+ logging.error(error_msg)
277
+ return {
278
+ "user_input": user_input,
279
+ "response": error_msg,
280
+ "error": error_msg,
281
+ "execution_time": 0.0
282
+ }
283
+
284
+ async def handle_csv_upload(self, file_path: str) -> Dict[str, Any]:
285
+ """
286
+ Processa upload de CSV usando nova arquitetura de nós
287
+
288
+ Args:
289
+ file_path: Caminho do arquivo CSV
290
+
291
+ Returns:
292
+ Resultado do upload
293
+ """
294
+ try:
295
+ # Etapa 1: Processa CSV
296
+ csv_state = {
297
+ "file_path": file_path,
298
+ "success": False,
299
+ "message": "",
300
+ "csv_data_sample": {},
301
+ "column_info": {},
302
+ "processing_stats": {}
303
+ }
304
+
305
+ csv_result = await csv_processing_node(csv_state)
306
+
307
+ if not csv_result["success"]:
308
+ return csv_result
309
+
310
+ # Etapa 2: Cria banco de dados
311
+ db_state = csv_result.copy()
312
+ db_result = await create_database_from_dataframe_node(db_state)
313
+
314
+ if not db_result["success"]:
315
+ return db_result
316
+
317
+ # Etapa 3: Atualiza sistema
318
+ if db_result["success"]:
319
+ # Atualiza IDs dos objetos
320
+ self.engine_id = db_result["engine_id"]
321
+ self.db_id = db_result["db_id"]
322
+
323
+ # Cria novo agente SQL
324
+ new_engine = self.object_manager.get_engine(self.engine_id)
325
+ new_db = self.object_manager.get_database(self.db_id)
326
+ new_sql_agent = SQLAgentManager(new_db)
327
+
328
+ # Atualiza agente
329
+ self.agent_id = self.object_manager.store_sql_agent(new_sql_agent, self.db_id)
330
+
331
+ # Limpa cache
332
+ cache_manager = self.object_manager.get_cache_manager(self.cache_id)
333
+ if cache_manager:
334
+ cache_manager.clear_cache()
335
+
336
+ logging.info("[UPLOAD] Sistema atualizado com novo CSV")
337
+
338
+ return db_result
339
+
340
+ except Exception as e:
341
+ error_msg = f"❌ Erro no upload de CSV: {e}"
342
+ logging.error(error_msg)
343
+ return {
344
+ "success": False,
345
+ "message": error_msg
346
+ }
347
+
348
+ async def reset_system(self) -> Dict[str, Any]:
349
+ """
350
+ Reseta o sistema ao estado inicial
351
+
352
+ Returns:
353
+ Resultado do reset
354
+ """
355
+ try:
356
+ # Usa nó de reset customizado
357
+ state = {
358
+ "success": False,
359
+ "message": "",
360
+ "engine_id": self.engine_id,
361
+ "agent_id": self.agent_id,
362
+ "cache_id": self.cache_id
363
+ }
364
+
365
+ result = await self.custom_node_manager.execute_node("system_reset", state)
366
+
367
+ # Se reset foi bem-sucedido, atualiza IDs
368
+ if result.get("success"):
369
+ self.engine_id = result.get("engine_id", self.engine_id)
370
+ self.agent_id = result.get("agent_id", self.agent_id)
371
+ # Cache ID permanece o mesmo, apenas é limpo
372
+
373
+ logging.info("[RESET] Sistema resetado com sucesso")
374
+
375
+ return result
376
+
377
+ except Exception as e:
378
+ error_msg = f"❌ Erro ao resetar sistema: {e}"
379
+ logging.error(error_msg)
380
+ return {
381
+ "success": False,
382
+ "message": error_msg
383
+ }
384
+
385
+ def toggle_advanced_mode(self, enabled: bool) -> str:
386
+ """
387
+ Alterna modo avançado
388
+
389
+ Args:
390
+ enabled: Se deve habilitar modo avançado
391
+
392
+ Returns:
393
+ Mensagem de status
394
+ """
395
+ message = "Modo avançado ativado." if enabled else "Modo avançado desativado."
396
+ logging.info(f"[MODO AVANÇADO] {'Ativado' if enabled else 'Desativado'}")
397
+ return message
398
+
399
+ def get_history(self) -> list:
400
+ """
401
+ Retorna histórico de conversas
402
+
403
+ Returns:
404
+ Lista com histórico
405
+ """
406
+ return self.cache_manager.get_history()
407
+
408
+ def clear_cache(self):
409
+ """Limpa cache do sistema"""
410
+ self.cache_manager.clear_cache()
411
+ logging.info("Cache limpo")
412
+
413
+ async def get_system_info(self) -> Dict[str, Any]:
414
+ """
415
+ Obtém informações do sistema
416
+
417
+ Returns:
418
+ Informações do sistema
419
+ """
420
+ state = {
421
+ "engine": self.engine,
422
+ "sql_agent": self.sql_agent,
423
+ "cache_manager": self.cache_manager
424
+ }
425
+
426
+ result = await self.custom_node_manager.execute_node("system_info", state)
427
+ return result.get("system_info", {})
428
+
429
+ async def validate_system(self) -> Dict[str, Any]:
430
+ """
431
+ Valida o estado do sistema
432
+
433
+ Returns:
434
+ Resultado da validação
435
+ """
436
+ state = {
437
+ "engine": self.engine,
438
+ "sql_agent": self.sql_agent,
439
+ "cache_manager": self.cache_manager
440
+ }
441
+
442
+ result = await self.custom_node_manager.execute_node("system_validation", state)
443
+ return result.get("validation", {})
444
+
445
+ def _smart_type_conversion(self, df, sql_types):
446
+ """
447
+ Conversão inteligente de tipos de dados com suporte a formatos brasileiros
448
+ """
449
+ import re
450
+
451
+ logging.info("[TYPE_CONVERSION] 🔧 Iniciando conversão inteligente de tipos")
452
+
453
+ for col in df.columns:
454
+ col_data = df[col].dropna() # Remove NaN para análise
455
+
456
+ if len(col_data) == 0:
457
+ continue
458
+
459
+ # Amostra para análise (primeiros 100 valores não-nulos)
460
+ sample = col_data.head(100).astype(str)
461
+
462
+ logging.debug(f"[TYPE_CONVERSION] 📊 Analisando coluna: {col}")
463
+
464
+ # 1. DETECTAR DATAS
465
+ if self._is_date_column(sample):
466
+ try:
467
+ df[col] = self._convert_to_date(df[col])
468
+ sql_types[col] = DateTime
469
+ logging.debug(f"[TYPE_CONVERSION] ✅ {col} → DATETIME")
470
+ continue
471
+ except Exception as e:
472
+ logging.warning(f"[TYPE_CONVERSION] ⚠️ Falha ao converter {col} para data: {e}")
473
+
474
+ # 2. DETECTAR NÚMEROS INTEIROS (PRIORIDADE ALTA)
475
+ if self._is_integer_column(sample):
476
+ try:
477
+ # Converter removendo caracteres não numéricos, mas mantendo negativos
478
+ def clean_integer(value):
479
+ if pd.isna(value):
480
+ return None
481
+ value_str = str(value).strip()
482
+ # Manter apenas dígitos e sinal negativo
483
+ clean_value = ''.join(c for c in value_str if c.isdigit() or c == '-')
484
+ if clean_value and clean_value != '-':
485
+ return int(clean_value)
486
+ return None
487
+
488
+ df[col] = df[col].apply(clean_integer).astype('Int64')
489
+ sql_types[col] = Integer
490
+ logging.debug(f"[TYPE_CONVERSION] ✅ {col} → INTEGER")
491
+ continue
492
+ except Exception as e:
493
+ logging.warning(f"[TYPE_CONVERSION] ⚠️ Falha ao converter {col} para inteiro: {e}")
494
+
495
+ # 3. DETECTAR VALORES MONETÁRIOS
496
+ if self._is_monetary_column(sample):
497
+ try:
498
+ df[col] = self._convert_to_monetary(df[col])
499
+ sql_types[col] = Float
500
+ logging.debug(f"[TYPE_CONVERSION] ✅ {col} → FLOAT (monetário)")
501
+ continue
502
+ except Exception as e:
503
+ logging.warning(f"[TYPE_CONVERSION] ⚠️ Falha ao converter {col} para monetário: {e}")
504
+
505
+ # 4. DETECTAR NÚMEROS DECIMAIS
506
+ if self._is_float_column(sample):
507
+ try:
508
+ df[col] = self._convert_to_float(df[col])
509
+ sql_types[col] = Float
510
+ logging.debug(f"[TYPE_CONVERSION] ✅ {col} → FLOAT")
511
+ continue
512
+ except Exception as e:
513
+ logging.warning(f"[TYPE_CONVERSION] ⚠️ Falha ao converter {col} para float: {e}")
514
+
515
+ # 5. MANTER COMO TEXTO (padrão)
516
+ logging.debug(f"[TYPE_CONVERSION] 📝 {col} → TEXT (padrão)")
517
+
518
+ # Resumo da conversão
519
+ type_summary = {}
520
+ for col, sql_type in sql_types.items():
521
+ type_name = sql_type.__name__ if hasattr(sql_type, '__name__') else str(sql_type).split('.')[-1].replace('>', '')
522
+ if type_name not in type_summary:
523
+ type_summary[type_name] = 0
524
+ type_summary[type_name] += 1
525
+
526
+ summary_text = ", ".join([f"{count} {type_name}" for type_name, count in type_summary.items()])
527
+ logging.info(f"[TYPE_CONVERSION] ✅ Conversão concluída: {summary_text}")
528
+ return df
529
+
530
+ def _is_date_column(self, sample):
531
+ """Detecta se uma coluna contém datas BASEADO APENAS NOS VALORES"""
532
+ import re
533
+
534
+ # Padrões de data brasileiros e internacionais
535
+ date_patterns = [
536
+ r'^\d{1,2}[\/\-\.]\d{1,2}[\/\-\.]\d{4}$', # DD/MM/YYYY ou DD-MM-YYYY
537
+ r'^\d{4}[\/\-\.]\d{1,2}[\/\-\.]\d{1,2}$', # YYYY/MM/DD ou YYYY-MM-DD
538
+ r'^\d{1,2}[\/\-\.]\d{1,2}[\/\-\.]\d{2}$', # DD/MM/YY
539
+ ]
540
+
541
+ # Verificar se pelo menos 70% dos valores seguem padrão de data
542
+ date_count = 0
543
+ for value in sample:
544
+ if pd.isna(value) or value == '':
545
+ continue
546
+ for pattern in date_patterns:
547
+ if re.match(pattern, str(value).strip()):
548
+ date_count += 1
549
+ break
550
+
551
+ return date_count / len(sample) >= 0.7
552
+
553
+ def _is_monetary_column(self, sample):
554
+ """Detecta se uma coluna contém valores monetários BASEADO APENAS NOS VALORES"""
555
+ import re
556
+
557
+ # Padrões monetários brasileiros e internacionais
558
+ money_patterns = [
559
+ r'^R\$\s*\d+[,\.]\d{2}$', # R$ 10,50 ou R$ 10.50
560
+ r'^\d+[,\.]\d{2}$', # 10,50 ou 10.50
561
+ r'^R\$\s*\d+$', # R$ 10
562
+ r'^\$\s*\d+[,\.]\d{2}$', # $ 10.50
563
+ r'^\$\s*\d+$', # $ 10
564
+ ]
565
+
566
+ # Verificar se pelo menos 60% dos valores seguem padrão monetário
567
+ money_count = 0
568
+ for value in sample:
569
+ if pd.isna(value) or value == '':
570
+ continue
571
+ value_str = str(value).strip()
572
+ for pattern in money_patterns:
573
+ if re.match(pattern, value_str):
574
+ money_count += 1
575
+ break
576
+
577
+ return money_count / len(sample) >= 0.6
578
+
579
+ def _is_integer_column(self, sample):
580
+ """Detecta se uma coluna contém números inteiros"""
581
+ try:
582
+ # Primeiro, verificar se há vírgulas ou pontos decimais nos valores
583
+ has_decimal_separators = False
584
+ valid_numeric_count = 0
585
+ integer_count = 0
586
+
587
+ for value in sample:
588
+ if pd.isna(value) or value == '':
589
+ continue
590
+
591
+ value_str = str(value).strip()
592
+
593
+ # Se contém vírgula ou ponto seguido de dígitos, é decimal
594
+ if (',' in value_str and any(c.isdigit() for c in value_str.split(',')[-1])) or \
595
+ ('.' in value_str and any(c.isdigit() for c in value_str.split('.')[-1])):
596
+ has_decimal_separators = True
597
+ break
598
+
599
+ # Tentar converter para número
600
+ try:
601
+ # Remover espaços e caracteres não numéricos (exceto - para negativos)
602
+ clean_value = ''.join(c for c in value_str if c.isdigit() or c == '-')
603
+ if clean_value and clean_value != '-':
604
+ num_value = int(clean_value)
605
+ valid_numeric_count += 1
606
+ integer_count += 1
607
+ except:
608
+ # Se não conseguir converter para int, tentar float
609
+ try:
610
+ float_value = float(value_str)
611
+ valid_numeric_count += 1
612
+ # Se o float é igual ao int, conta como inteiro
613
+ if float_value == int(float_value):
614
+ integer_count += 1
615
+ except:
616
+ continue
617
+
618
+ # Se encontrou separadores decimais, não é coluna de inteiros
619
+ if has_decimal_separators:
620
+ return False
621
+
622
+ # Verificar se pelo menos 80% são números válidos
623
+ if valid_numeric_count == 0 or valid_numeric_count / len(sample) < 0.8:
624
+ return False
625
+
626
+ # Verificar se pelo menos 95% dos números válidos são inteiros
627
+ return integer_count / valid_numeric_count >= 0.95
628
+
629
+ except Exception as e:
630
+ logging.debug(f"Erro na detecção de inteiros: {e}")
631
+ return False
632
+
633
+ def _is_float_column(self, sample):
634
+ """Detecta se uma coluna contém números decimais (com vírgula ou ponto)"""
635
+ try:
636
+ has_decimal_values = False
637
+ valid_numeric_count = 0
638
+
639
+ for value in sample:
640
+ if pd.isna(value) or value == '':
641
+ continue
642
+
643
+ value_str = str(value).strip()
644
+
645
+ # Verificar se contém separadores decimais com dígitos após
646
+ if (',' in value_str and any(c.isdigit() for c in value_str.split(',')[-1])) or \
647
+ ('.' in value_str and any(c.isdigit() for c in value_str.split('.')[-1])):
648
+ has_decimal_values = True
649
+
650
+ # Tentar converter para numérico (substituindo vírgula por ponto)
651
+ try:
652
+ clean_value = value_str.replace(',', '.')
653
+ float(clean_value)
654
+ valid_numeric_count += 1
655
+ except:
656
+ continue
657
+
658
+ # Só é float se tem separadores decimais E pelo menos 80% são números válidos
659
+ if not has_decimal_values:
660
+ return False
661
+
662
+ return valid_numeric_count / len(sample) >= 0.8
663
+
664
+ except Exception as e:
665
+ logging.debug(f"Erro na detecção de floats: {e}")
666
+ return False
667
+
668
+ def _convert_to_date(self, series):
669
+ """Converte série para datetime com formatos brasileiros"""
670
+ # Tentar diferentes formatos de data
671
+ date_formats = [
672
+ '%d/%m/%Y', # 31/12/2023
673
+ '%d-%m-%Y', # 31-12-2023
674
+ '%d.%m.%Y', # 31.12.2023
675
+ '%Y-%m-%d', # 2023-12-31
676
+ '%Y/%m/%d', # 2023/12/31
677
+ '%d/%m/%y', # 31/12/23
678
+ ]
679
+
680
+ for fmt in date_formats:
681
+ try:
682
+ return pd.to_datetime(series, format=fmt, errors='raise')
683
+ except:
684
+ continue
685
+
686
+ # Se nenhum formato específico funcionou, usar inferência automática
687
+ try:
688
+ return pd.to_datetime(series, dayfirst=True, errors='coerce')
689
+ except:
690
+ raise ValueError("Não foi possível converter para data")
691
+
692
+ def _convert_to_monetary(self, series):
693
+ """Converte série para valores monetários (float)"""
694
+ def clean_monetary(value):
695
+ if pd.isna(value):
696
+ return None
697
+
698
+ # Converter para string e limpar
699
+ value_str = str(value).strip()
700
+
701
+ # Remover símbolos monetários
702
+ value_str = value_str.replace('R$', '').replace('$', '').strip()
703
+
704
+ # Tratar formato brasileiro (vírgula como decimal)
705
+ if ',' in value_str and '.' in value_str:
706
+ # Formato: 1.234,56 → 1234.56
707
+ value_str = value_str.replace('.', '').replace(',', '.')
708
+ elif ',' in value_str:
709
+ # Formato: 1234,56 → 1234.56
710
+ value_str = value_str.replace(',', '.')
711
+
712
+ try:
713
+ return float(value_str)
714
+ except:
715
+ return None
716
+
717
+ return series.apply(clean_monetary)
718
+
719
+ def _convert_to_float(self, series):
720
+ """Converte série para float com formato brasileiro"""
721
+ def clean_float(value):
722
+ if pd.isna(value):
723
+ return None
724
+
725
+ value_str = str(value).strip()
726
+
727
+ # Tratar formato brasileiro
728
+ if ',' in value_str:
729
+ value_str = value_str.replace(',', '.')
730
+
731
+ try:
732
+ return float(value_str)
733
+ except:
734
+ return None
735
+
736
+ return series.apply(clean_float)
737
+
738
+ # Instância global do gerenciador
739
+ _graph_manager: Optional[AgentGraphManager] = None
740
+
741
+ def get_graph_manager() -> AgentGraphManager:
742
+ """
743
+ Retorna instância singleton do gerenciador de grafo
744
+
745
+ Returns:
746
+ AgentGraphManager
747
+ """
748
+ global _graph_manager
749
+ if _graph_manager is None:
750
+ _graph_manager = AgentGraphManager()
751
+ return _graph_manager
752
+
753
+ async def initialize_graph() -> AgentGraphManager:
754
+ """
755
+ Inicializa o grafo principal
756
+
757
+ Returns:
758
+ AgentGraphManager inicializado
759
+ """
760
+ try:
761
+ manager = get_graph_manager()
762
+
763
+ # Valida sistema
764
+ validation = await manager.validate_system()
765
+ if not validation.get("overall_valid", False):
766
+ logging.warning("Sistema não passou na validação completa")
767
+
768
+ logging.info("Grafo principal inicializado e validado")
769
+ return manager
770
+
771
+ except Exception as e:
772
+ logging.error(f"Erro ao inicializar grafo: {e}")
773
+ raise
774
+
775
+ # Classe GraphManager removida - funcionalidade movida para AgentGraphManager
nodes/agent_node.py ADDED
@@ -0,0 +1,126 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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 ADDED
@@ -0,0 +1,230 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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 ADDED
@@ -0,0 +1,815 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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 ADDED
@@ -0,0 +1,297 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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 ADDED
@@ -0,0 +1,226 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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 ADDED
@@ -0,0 +1,1015 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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 ADDED
@@ -0,0 +1,147 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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 ADDED
@@ -0,0 +1,232 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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 ADDED
@@ -0,0 +1,141 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
requirements.txt ADDED
@@ -0,0 +1,28 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Core LangGraph and LangChain
2
+ langgraph>=0.2.0
3
+ langchain>=0.3.0
4
+ langchain-openai>=0.2.0
5
+ langchain-anthropic>=0.2.0
6
+ langchain-community>=0.3.0
7
+ langchain-core>=0.3.0
8
+
9
+ # LangSmith for observability and tracing
10
+ langsmith>=0.1.39
11
+
12
+ # Database and Data Processing
13
+ pandas>=2.0.0
14
+ sqlalchemy>=2.0.0
15
+ numpy>=1.24.0
16
+
17
+ # AI/ML Libraries
18
+ huggingface_hub>=0.20.0
19
+
20
+ # Graph Generation Libraries
21
+ matplotlib>=3.7.0
22
+ pillow>=10.0.0
23
+
24
+ # Web Interface
25
+ gradio>=4.0.0
26
+
27
+ # Utilities
28
+ python-dotenv>=1.0.0
utils/config.py ADDED
@@ -0,0 +1,180 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Configurações e constantes do projeto AgentGraph
3
+ """
4
+ import os
5
+ from dotenv import load_dotenv
6
+ import logging
7
+
8
+ # Carrega variáveis de ambiente
9
+ load_dotenv()
10
+
11
+ # Configurações de API
12
+ HUGGINGFACE_API_KEY = os.getenv("HUGGINGFACE_API_KEY")
13
+ OPENAI_API_KEY = os.getenv("OPENAI_API_KEY")
14
+ ANTHROPIC_API_KEY = os.getenv("ANTHROPIC_API_KEY")
15
+
16
+ # Configurações do LangSmith (observabilidade)
17
+ LANGSMITH_API_KEY = os.getenv("LANGSMITH_API_KEY")
18
+ LANGSMITH_TRACING = os.getenv("LANGSMITH_TRACING", "false").lower() == "true"
19
+ LANGSMITH_ENDPOINT = os.getenv("LANGSMITH_ENDPOINT", "https://api.smith.langchain.com")
20
+ LANGSMITH_PROJECT = os.getenv("LANGSMITH_PROJECT", "agentgraph-project")
21
+
22
+ # Configurações de arquivos e diretórios
23
+ UPLOAD_DIR = os.getenv("UPLOAD_DIR", "uploaded_data")
24
+ DEFAULT_CSV_PATH = os.getenv("DEFAULT_CSV_PATH", "tabela.csv")
25
+ SQL_DB_PATH = os.getenv("SQL_DB_PATH", "data.db")
26
+ UPLOADED_CSV_PATH = os.path.join(UPLOAD_DIR, "tabela.csv")
27
+
28
+ # Modelos disponíveis para seleção (usados no agentSQL)
29
+ AVAILABLE_MODELS = {
30
+ "GPT-o3-mini": "o3-mini",
31
+ "GPT-4o-mini": "gpt-4o-mini",
32
+ "GPT-4o": "gpt-4o",
33
+ "Claude-3.5-Sonnet": "claude-3-5-sonnet-20241022"
34
+ }
35
+
36
+ # Modelos para refinamento (apenas uso interno)
37
+ REFINEMENT_MODELS = {
38
+ "LLaMA 70B": "meta-llama/Llama-3.3-70B-Instruct",
39
+ "LlaMA 8B": "meta-llama/Llama-3.1-8B-Instruct",
40
+ "DeepSeek-R1": "deepseek-ai/DeepSeek-R1-0528"
41
+ }
42
+
43
+ # Mapeamento completo de modelos (para compatibilidade)
44
+ LLAMA_MODELS = {**AVAILABLE_MODELS, **REFINEMENT_MODELS}
45
+
46
+ MAX_TOKENS_MAP = {
47
+ # Modelos de refinamento
48
+ "meta-llama/Llama-3.3-70B-Instruct": 900,
49
+ "meta-llama/Llama-3.1-8B-Instruct": 700,
50
+ "deepseek-ai/DeepSeek-R1-0528": 8192,
51
+ # Modelos do agentSQL
52
+ "o3-mini": 4096,
53
+ "gpt-4o-mini": 4096,
54
+ "gpt-4o": 4096,
55
+ "claude-3-5-sonnet-20241022": 1024
56
+ }
57
+
58
+ # Modelos que usam OpenAI (GPT)
59
+ OPENAI_MODELS = {
60
+ "o3-mini",
61
+ "gpt-4o-mini"
62
+ "gpt-4o",
63
+ }
64
+
65
+ # Modelos que usam Anthropic (Claude)
66
+ ANTHROPIC_MODELS = {
67
+ "claude-3-5-sonnet-20241022"
68
+ }
69
+
70
+ # Modelos que usam HuggingFace (para refinamento)
71
+ HUGGINGFACE_MODELS = {
72
+ "meta-llama/Llama-3.3-70B-Instruct",
73
+ "meta-llama/Llama-3.1-8B-Instruct",
74
+ "deepseek-ai/DeepSeek-R1-0528"
75
+ }
76
+
77
+ # Configurações do agente
78
+ DEFAULT_MODEL = os.getenv("DEFAULT_MODEL", "GPT-4o-mini")
79
+ MAX_ITERATIONS = int(os.getenv("MAX_ITERATIONS", "40"))
80
+ TEMPERATURE = float(os.getenv("TEMPERATURE", "0"))
81
+
82
+ # Configurações do Gradio
83
+ GRADIO_SHARE = os.getenv("GRADIO_SHARE", "False").lower() == "true"
84
+ GRADIO_PORT = int(os.getenv("GRADIO_PORT", "7860"))
85
+
86
+ # Configurações de logging
87
+ LOG_LEVEL = os.getenv("LOG_LEVEL", "INFO")
88
+
89
+ # Configuração do logging
90
+ logging.basicConfig(
91
+ level=getattr(logging, LOG_LEVEL.upper()),
92
+ format='%(asctime)s - %(levelname)s - %(message)s'
93
+ )
94
+
95
+ # Cria diretório de upload se não existir
96
+ os.makedirs(UPLOAD_DIR, exist_ok=True)
97
+
98
+ # Configuração das variáveis de ambiente para OpenAI
99
+ if OPENAI_API_KEY:
100
+ os.environ["OPENAI_API_KEY"] = OPENAI_API_KEY
101
+
102
+ # Configuração das variáveis de ambiente para Anthropic
103
+ if ANTHROPIC_API_KEY:
104
+ os.environ["ANTHROPIC_API_KEY"] = ANTHROPIC_API_KEY
105
+
106
+ # Configuração das variáveis de ambiente para LangSmith
107
+ if LANGSMITH_API_KEY:
108
+ os.environ["LANGSMITH_API_KEY"] = LANGSMITH_API_KEY
109
+ os.environ["LANGSMITH_TRACING"] = str(LANGSMITH_TRACING).lower()
110
+ os.environ["LANGSMITH_ENDPOINT"] = LANGSMITH_ENDPOINT
111
+ os.environ["LANGSMITH_PROJECT"] = LANGSMITH_PROJECT
112
+ logging.info(f"LangSmith configurado: projeto='{LANGSMITH_PROJECT}', tracing={LANGSMITH_TRACING}")
113
+ else:
114
+ logging.info("LangSmith não configurado (LANGSMITH_API_KEY não encontrada)")
115
+
116
+ def get_active_csv_path():
117
+ """Retorna o CSV ativo: o carregado ou o padrão."""
118
+ if os.path.exists(UPLOADED_CSV_PATH):
119
+ logging.info(f"[CSV] Usando arquivo CSV carregado: {UPLOADED_CSV_PATH}")
120
+ return UPLOADED_CSV_PATH
121
+ else:
122
+ logging.info(f"[CSV] Usando arquivo CSV padrão: {DEFAULT_CSV_PATH}")
123
+ return DEFAULT_CSV_PATH
124
+
125
+ def validate_config():
126
+ """Valida se as configurações necessárias estão presentes."""
127
+ errors = []
128
+ warnings = []
129
+
130
+ if not HUGGINGFACE_API_KEY:
131
+ errors.append("HUGGINGFACE_API_KEY não configurada")
132
+
133
+ if not OPENAI_API_KEY:
134
+ errors.append("OPENAI_API_KEY não configurada")
135
+
136
+ if not ANTHROPIC_API_KEY:
137
+ errors.append("ANTHROPIC_API_KEY não configurada")
138
+
139
+ if not os.path.exists(DEFAULT_CSV_PATH):
140
+ errors.append(f"Arquivo CSV padrão não encontrado: {DEFAULT_CSV_PATH}")
141
+
142
+ # LangSmith é opcional - apenas aviso se não configurado
143
+ if not LANGSMITH_API_KEY:
144
+ warnings.append("LANGSMITH_API_KEY não configurada - observabilidade desabilitada")
145
+
146
+ if errors:
147
+ raise ValueError(f"Erros de configuração: {', '.join(errors)}")
148
+
149
+ if warnings:
150
+ for warning in warnings:
151
+ logging.warning(warning)
152
+
153
+ logging.info("Configurações validadas com sucesso")
154
+ return True
155
+
156
+ def is_langsmith_enabled() -> bool:
157
+ """
158
+ Verifica se o LangSmith está habilitado e configurado
159
+
160
+ Returns:
161
+ True se LangSmith estiver habilitado, False caso contrário
162
+ """
163
+ return bool(LANGSMITH_API_KEY and LANGSMITH_TRACING)
164
+
165
+ def get_langsmith_metadata() -> dict:
166
+ """
167
+ Retorna metadados padrão para traces do LangSmith
168
+
169
+ Returns:
170
+ Dicionário com metadados do projeto
171
+ """
172
+ if not is_langsmith_enabled():
173
+ return {}
174
+
175
+ return {
176
+ "project": LANGSMITH_PROJECT,
177
+ "application": "AgentGraph",
178
+ "version": "1.0.0",
179
+ "environment": "production"
180
+ }
utils/database.py ADDED
@@ -0,0 +1,93 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Funções para gerenciamento de banco de dados e processamento de CSV
3
+ """
4
+ import os
5
+ import pandas as pd
6
+ from sqlalchemy import create_engine
7
+ from sqlalchemy.types import DateTime, Integer, Float
8
+ from langchain_community.utilities import SQLDatabase
9
+ import logging
10
+ from typing import Optional
11
+
12
+ from utils.config import SQL_DB_PATH
13
+
14
+ # FUNÇÃO REMOVIDA: create_engine_and_load_db
15
+ # Esta função foi substituída pela nova arquitetura de nós
16
+ # Use: csv_processing_node.py + database_node.py
17
+
18
+ def create_engine_from_processed_dataframe(processed_df: pd.DataFrame, sql_types: dict, sql_db_path: str = SQL_DB_PATH):
19
+ """
20
+ Cria engine SQLAlchemy a partir de DataFrame já processado
21
+ NOVA VERSÃO - usa processamento genérico
22
+
23
+ Args:
24
+ processed_df: DataFrame já processado
25
+ sql_types: Dicionário com tipos SQL para as colunas
26
+ sql_db_path: Caminho para o banco SQLite
27
+
28
+ Returns:
29
+ SQLAlchemy Engine
30
+ """
31
+ logging.info("Criando banco de dados a partir de DataFrame processado...")
32
+ engine = create_engine(f"sqlite:///{sql_db_path}")
33
+
34
+ logging.info("[DEBUG] Tipos das colunas processadas:")
35
+ logging.info(processed_df.dtypes)
36
+
37
+ # Salva no banco SQLite
38
+ processed_df.to_sql("tabela", engine, index=False, if_exists="replace", dtype=sql_types)
39
+ logging.info(f"Banco de dados SQL criado com sucesso! {len(processed_df)} registros salvos")
40
+ return engine
41
+
42
+ def create_sql_database(engine) -> SQLDatabase:
43
+ """
44
+ Cria objeto SQLDatabase do LangChain a partir de uma engine
45
+
46
+ Args:
47
+ engine: SQLAlchemy Engine
48
+
49
+ Returns:
50
+ SQLDatabase do LangChain
51
+ """
52
+ return SQLDatabase(engine=engine)
53
+
54
+ def get_sample_data(engine, limit: int = 10) -> pd.DataFrame:
55
+ """
56
+ Obtém dados de amostra do banco para contexto
57
+
58
+ Args:
59
+ engine: SQLAlchemy Engine
60
+ limit: Número de linhas para retornar
61
+
62
+ Returns:
63
+ DataFrame com dados de amostra
64
+ """
65
+ try:
66
+ return pd.read_sql_query(f"SELECT * FROM tabela LIMIT {limit}", engine)
67
+ except Exception as e:
68
+ logging.error(f"Erro ao obter dados de amostra: {e}")
69
+ return pd.DataFrame()
70
+
71
+ def validate_database(engine) -> bool:
72
+ """
73
+ Valida se o banco de dados está funcionando corretamente
74
+
75
+ Args:
76
+ engine: SQLAlchemy Engine
77
+
78
+ Returns:
79
+ True se válido, False caso contrário
80
+ """
81
+ try:
82
+ # Testa uma query simples
83
+ result = pd.read_sql_query("SELECT COUNT(*) as count FROM tabela", engine)
84
+ count = result.iloc[0]['count']
85
+ logging.info(f"Banco validado: {count} registros encontrados")
86
+ return count > 0
87
+ except Exception as e:
88
+ logging.error(f"Erro na validação do banco: {e}")
89
+ return False
90
+
91
+ # FUNÇÃO REMOVIDA: async_create_engine_and_load_db
92
+ # Esta função foi removida junto com create_engine_and_load_db
93
+ # Use a nova arquitetura de nós: csv_processing_node.py + database_node.py
utils/object_manager.py ADDED
@@ -0,0 +1,147 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Gerenciador de objetos não-serializáveis para LangGraph
3
+ """
4
+ import uuid
5
+ from typing import Dict, Any, Optional
6
+ import logging
7
+
8
+ class ObjectManager:
9
+ """
10
+ Gerencia objetos não-serializáveis que não podem ser incluídos no estado do LangGraph
11
+ """
12
+
13
+ def __init__(self):
14
+ self._objects: Dict[str, Any] = {}
15
+ self._sql_agents: Dict[str, Any] = {}
16
+ self._engines: Dict[str, Any] = {}
17
+ self._databases: Dict[str, Any] = {}
18
+ self._cache_managers: Dict[str, Any] = {}
19
+ # Mapeamento para relacionar agentes com seus bancos
20
+ self._agent_db_mapping: Dict[str, str] = {}
21
+
22
+ def store_sql_agent(self, agent: Any, db_id: str = None) -> str:
23
+ """Armazena agente SQL e retorna ID"""
24
+ agent_id = str(uuid.uuid4())
25
+ self._sql_agents[agent_id] = agent
26
+
27
+ # Mapeia agente com seu banco se fornecido
28
+ if db_id:
29
+ self._agent_db_mapping[agent_id] = db_id
30
+
31
+ logging.info(f"Agente SQL armazenado com ID: {agent_id}")
32
+ return agent_id
33
+
34
+ def get_sql_agent(self, agent_id: str) -> Optional[Any]:
35
+ """Recupera agente SQL pelo ID"""
36
+ return self._sql_agents.get(agent_id)
37
+
38
+ def store_engine(self, engine: Any) -> str:
39
+ """Armazena engine e retorna ID"""
40
+ engine_id = str(uuid.uuid4())
41
+ self._engines[engine_id] = engine
42
+ logging.info(f"Engine armazenada com ID: {engine_id}")
43
+ return engine_id
44
+
45
+ def get_engine(self, engine_id: str) -> Optional[Any]:
46
+ """Recupera engine pelo ID"""
47
+ return self._engines.get(engine_id)
48
+
49
+ def store_database(self, database: Any) -> str:
50
+ """Armazena banco de dados e retorna ID"""
51
+ db_id = str(uuid.uuid4())
52
+ self._databases[db_id] = database
53
+ logging.info(f"Banco de dados armazenado com ID: {db_id}")
54
+ return db_id
55
+
56
+ def get_database(self, db_id: str) -> Optional[Any]:
57
+ """Recupera banco de dados pelo ID"""
58
+ return self._databases.get(db_id)
59
+
60
+ def get_db_id_for_agent(self, agent_id: str) -> Optional[str]:
61
+ """Recupera ID do banco associado ao agente"""
62
+ return self._agent_db_mapping.get(agent_id)
63
+
64
+ def store_cache_manager(self, cache_manager: Any) -> str:
65
+ """Armazena cache manager e retorna ID"""
66
+ cache_id = str(uuid.uuid4())
67
+ self._cache_managers[cache_id] = cache_manager
68
+ logging.info(f"Cache manager armazenado com ID: {cache_id}")
69
+ return cache_id
70
+
71
+ def get_cache_manager(self, cache_id: str) -> Optional[Any]:
72
+ """Recupera cache manager pelo ID"""
73
+ return self._cache_managers.get(cache_id)
74
+
75
+ def store_object(self, obj: Any, category: str = "general") -> str:
76
+ """Armazena objeto genérico e retorna ID"""
77
+ obj_id = str(uuid.uuid4())
78
+ self._objects[obj_id] = {"object": obj, "category": category}
79
+ logging.info(f"Objeto {category} armazenado com ID: {obj_id}")
80
+ return obj_id
81
+
82
+ def get_object(self, obj_id: str) -> Optional[Any]:
83
+ """Recupera objeto pelo ID"""
84
+ obj_data = self._objects.get(obj_id)
85
+ return obj_data["object"] if obj_data else None
86
+
87
+ def update_sql_agent(self, agent_id: str, new_agent: Any) -> bool:
88
+ """Atualiza agente SQL existente"""
89
+ if agent_id in self._sql_agents:
90
+ self._sql_agents[agent_id] = new_agent
91
+ logging.info(f"Agente SQL atualizado: {agent_id}")
92
+ return True
93
+ return False
94
+
95
+ def update_engine(self, engine_id: str, new_engine: Any) -> bool:
96
+ """Atualiza engine existente"""
97
+ if engine_id in self._engines:
98
+ self._engines[engine_id] = new_engine
99
+ logging.info(f"Engine atualizada: {engine_id}")
100
+ return True
101
+ return False
102
+
103
+ def update_cache_manager(self, cache_id: str, new_cache_manager: Any) -> bool:
104
+ """Atualiza cache manager existente"""
105
+ if cache_id in self._cache_managers:
106
+ self._cache_managers[cache_id] = new_cache_manager
107
+ logging.info(f"Cache manager atualizado: {cache_id}")
108
+ return True
109
+ return False
110
+
111
+ def clear_all(self):
112
+ """Limpa todos os objetos armazenados"""
113
+ self._objects.clear()
114
+ self._sql_agents.clear()
115
+ self._engines.clear()
116
+ self._databases.clear()
117
+ self._cache_managers.clear()
118
+ self._agent_db_mapping.clear()
119
+ logging.info("Todos os objetos foram limpos do gerenciador")
120
+
121
+ def get_stats(self) -> Dict[str, int]:
122
+ """Retorna estatísticas dos objetos armazenados"""
123
+ return {
124
+ "sql_agents": len(self._sql_agents),
125
+ "engines": len(self._engines),
126
+ "databases": len(self._databases),
127
+ "cache_managers": len(self._cache_managers),
128
+ "general_objects": len(self._objects),
129
+ "agent_db_mappings": len(self._agent_db_mapping)
130
+ }
131
+
132
+ # Instância global do gerenciador
133
+ _object_manager: Optional[ObjectManager] = None
134
+
135
+ def get_object_manager() -> ObjectManager:
136
+ """Retorna instância singleton do gerenciador de objetos"""
137
+ global _object_manager
138
+ if _object_manager is None:
139
+ _object_manager = ObjectManager()
140
+ return _object_manager
141
+
142
+ def reset_object_manager():
143
+ """Reseta o gerenciador de objetos"""
144
+ global _object_manager
145
+ if _object_manager:
146
+ _object_manager.clear_all()
147
+ _object_manager = ObjectManager()