rwayz commited on
Commit
4c06429
·
verified ·
1 Parent(s): 392b5c9

Delete agents

Browse files
Files changed (2) hide show
  1. agents/sql_agent.py +0 -392
  2. agents/tools.py +0 -432
agents/sql_agent.py DELETED
@@ -1,392 +0,0 @@
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 DELETED
@@ -1,432 +0,0 @@
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
- "1. Nunca altere o idioma de nomes ou palavras na pergunta do usuário.\n"
65
- "2. Para buscar texto parcial use LIKE '%termo%'.\n"
66
- "3. Para NULL use IS NULL ou IS NOT NULL (nunca = NULL).\n"
67
- "4. Em agregações (SUM, COUNT, AVG) use GROUP BY nas colunas não agregadas.\n"
68
- "5. Para datas use formato 'YYYY-MM-DD' ou funções date() do SQLite.\n"
69
- "6. Nomes de colunas devem ser EXATAMENTE como mostrado.\n"
70
- "- Estamos usando o idioma Português do Brasil.\n"
71
- )
72
-
73
- def is_greeting(user_query: str) -> bool:
74
- """
75
- Verifica se a query do usuário é uma saudação
76
-
77
- Args:
78
- user_query: Query do usuário
79
-
80
- Returns:
81
- True se for saudação, False caso contrário
82
- """
83
- greetings = ["olá", "oi", "bom dia", "boa tarde", "boa noite", "oi, tudo bem?"]
84
- return user_query.lower().strip() in greetings
85
-
86
- def detect_query_type(user_query: str) -> str:
87
- """
88
- Detecta o tipo de processamento necessário para a query do usuário
89
-
90
- Args:
91
- user_query: Pergunta do usuário
92
-
93
- Returns:
94
- Tipo de processamento: 'sql_query', 'sql_query_graphic', 'prediction', 'chart'
95
- """
96
- query_lower = user_query.lower().strip()
97
-
98
- # Palavras-chave para diferentes tipos
99
- prediction_keywords = ['prever', 'predizer', 'previsão', 'forecast', 'predict', 'tendência', 'projeção']
100
-
101
- # Palavras-chave para gráficos - expandida para melhor detecção
102
- chart_keywords = [
103
- 'gráfico', 'grafico', 'chart', 'plot', 'visualizar', 'visualização', 'visualizacao',
104
- 'mostrar gráfico', 'mostrar grafico', 'gerar gráfico', 'gerar grafico',
105
- 'criar gráfico', 'criar grafico', 'plotar', 'desenhar gráfico', 'desenhar grafico',
106
- 'exibir gráfico', 'exibir grafico', 'fazer gráfico', 'fazer grafico',
107
- 'gráfico de', 'grafico de', 'em gráfico', 'em grafico',
108
- 'barras', 'linha', 'pizza', 'área', 'area', 'histograma',
109
- 'scatter', 'dispersão', 'dispersao', 'boxplot', 'heatmap'
110
- ]
111
-
112
- # Verifica se há solicitação de gráfico
113
- has_chart_request = any(keyword in query_lower for keyword in chart_keywords)
114
-
115
- # Verifica se há solicitação de previsão
116
- has_prediction_request = any(keyword in query_lower for keyword in prediction_keywords)
117
-
118
- # Lógica de detecção
119
- if has_prediction_request:
120
- return 'prediction' # Futuro: agente de ML/previsões
121
- elif has_chart_request:
122
- return 'sql_query_graphic' # SQL + Gráfico
123
- else:
124
- return 'sql_query' # SQL normal
125
-
126
- def prepare_sql_context(user_query: str, db_sample: pd.DataFrame) -> str:
127
- """
128
- Prepara o contexto inicial para ser enviado diretamente ao agentSQL
129
-
130
- Args:
131
- user_query: Pergunta do usuário
132
- db_sample: Amostra dos dados do banco
133
-
134
- Returns:
135
- Contexto formatado para o agentSQL
136
- """
137
- # Usa o contexto base do generate_initial_context
138
- base_context = generate_initial_context(db_sample)
139
-
140
- context = (
141
- f"""
142
- Você é um assistente especializado em consultas SQL e análise de dados.
143
-
144
- REGRAS OBRIGATORIAS:
145
- - “Retorne os resultados da consulta em formato legível, sem incluir o texto da query SQL.”
146
- - “Estamos atulamente no ano de 2025.“
147
- """
148
- "\n\n"
149
- f"**PERGUNTA DO USUÁRIO**:\n{user_query}"
150
- )
151
-
152
- return context
153
-
154
- async def refine_response_with_llm(
155
- user_question: str,
156
- sql_response: str,
157
- chart_md: str = ""
158
- ) -> str:
159
- """
160
- Refina a resposta usando um modelo LLM adicional
161
-
162
- Args:
163
- user_question: Pergunta original do usuário
164
- sql_response: Resposta do agente SQL
165
- chart_md: Markdown de gráficos (opcional)
166
-
167
- Returns:
168
- Resposta refinada
169
- """
170
- prompt = (
171
- f"Pergunta do usuário:\n{user_question}\n\n"
172
- f"Resposta gerada pelo agente SQL:\n{sql_response}\n\n"
173
- "Sua tarefa é refinar a resposta para deixá-la mais clara, completa e compreensível em português, "
174
- "mantendo a resposta original no início do texto e adicionando insights úteis sobre logística de entregas de produtos, "
175
- "por exemplo: comparar com padrões típicos, identificar possíveis problemas ou sugerir ações para melhorar atrasos, performance ou custos. "
176
- "Evite repetir informações sem necessidade e não invente dados."
177
- )
178
-
179
- logging.info(f"[DEBUG] Prompt enviado ao modelo de refinamento:\n{prompt}\n")
180
-
181
- try:
182
- response = hf_client.chat.completions.create(
183
- model=REFINEMENT_MODELS["LLaMA 70B"],
184
- messages=[{"role": "system", "content": prompt}],
185
- max_tokens=1200,
186
- stream=False
187
- )
188
- improved_response = response["choices"][0]["message"]["content"]
189
- logging.info(f"[DEBUG] Resposta do modelo de refinamento:\n{improved_response}\n")
190
- return improved_response + ("\n\n" + chart_md if chart_md else "")
191
-
192
- except Exception as e:
193
- logging.error(f"[ERRO] Falha ao refinar resposta com LLM: {e}")
194
- return sql_response + ("\n\n" + chart_md if chart_md else "")
195
-
196
- class CacheManager:
197
- """Gerenciador de cache para queries"""
198
-
199
- def __init__(self):
200
- self.query_cache: Dict[str, str] = {}
201
- self.history_log: List[Dict[str, Any]] = []
202
- self.recent_history: List[Dict[str, str]] = []
203
-
204
- def get_cached_response(self, query: str) -> Optional[str]:
205
- """Obtém resposta do cache"""
206
- return self.query_cache.get(query)
207
-
208
- def cache_response(self, query: str, response: str):
209
- """Armazena resposta no cache"""
210
- self.query_cache[query] = response
211
-
212
- def add_to_history(self, entry: Dict[str, Any]):
213
- """Adiciona entrada ao histórico"""
214
- self.history_log.append(entry)
215
-
216
- def update_recent_history(self, user_input: str, response: str):
217
- """Atualiza histórico recente"""
218
- self.recent_history.append({"role": "user", "content": user_input})
219
- self.recent_history.append({"role": "assistant", "content": response})
220
-
221
- # Mantém apenas as últimas 4 entradas (2 pares pergunta-resposta)
222
- if len(self.recent_history) > 4:
223
- self.recent_history.pop(0)
224
- self.recent_history.pop(0)
225
-
226
- def clear_cache(self):
227
- """Limpa todo o cache"""
228
- self.query_cache.clear()
229
- self.history_log.clear()
230
- self.recent_history.clear()
231
-
232
- def get_history(self) -> List[Dict[str, Any]]:
233
- """Retorna histórico completo"""
234
- return self.history_log
235
-
236
- # ==================== FUNÇÕES DE GRÁFICOS ====================
237
-
238
- def generate_graph_type_context(user_query: str, sql_query: str, df_columns: List[str], df_sample: pd.DataFrame) -> str:
239
- """
240
- Gera contexto para LLM escolher o tipo de gráfico mais adequado
241
-
242
- Args:
243
- user_query: Pergunta original do usuário
244
- sql_query: Query SQL gerada pelo agente
245
- df_columns: Lista de colunas retornadas pela query
246
- df_sample: Amostra dos dados para análise
247
-
248
- Returns:
249
- Contexto formatado para a LLM
250
- """
251
- # Criar uma descrição detalhada dos dados para ajudar a LLM a entender melhor a estrutura
252
- data_description = ""
253
- if not df_sample.empty:
254
- # Verificar tipos de dados de forma mais robusta
255
- numeric_cols = []
256
- date_cols = []
257
- categorical_cols = []
258
-
259
- for col in df_sample.columns:
260
- col_data = df_sample[col]
261
-
262
- # Verifica se é numérico (incluindo strings que representam números)
263
- try:
264
- # Tenta converter para numérico, tratando vírgulas como separador decimal
265
- if col_data.dtype == 'object':
266
- test_numeric = pd.to_numeric(col_data.astype(str).str.replace(',', '.'), errors='coerce')
267
- if test_numeric.notna().sum() > len(col_data) * 0.8: # 80% são números válidos
268
- numeric_cols.append(col)
269
- else:
270
- categorical_cols.append(col)
271
- elif pd.api.types.is_numeric_dtype(col_data):
272
- numeric_cols.append(col)
273
- elif pd.api.types.is_datetime64_any_dtype(col_data) or 'data' in col.lower():
274
- date_cols.append(col)
275
- else:
276
- categorical_cols.append(col)
277
- except:
278
- categorical_cols.append(col)
279
-
280
- # Adicionar informações sobre os primeiros valores de cada coluna
281
- data_description = "\nAmostra dos dados (primeiras 3 linhas):\n"
282
- data_description += df_sample.head(3).to_string(index=False)
283
-
284
- # Adicionar análise detalhada dos tipos de dados
285
- data_description += f"\n\nAnálise dos dados ({len(df_sample)} linhas total):"
286
- data_description += f"\n- Total de colunas: {len(df_sample.columns)}"
287
-
288
- if numeric_cols:
289
- data_description += f"\n- Colunas NUMÉRICAS ({len(numeric_cols)}): {', '.join(numeric_cols)}"
290
- # Adiciona informação sobre valores numéricos
291
- for col in numeric_cols[:2]: # Máximo 2 colunas para não ficar muito longo
292
- try:
293
- if df_sample[col].dtype == 'object':
294
- # Converte strings para números
295
- numeric_values = pd.to_numeric(df_sample[col].astype(str).str.replace(',', '.'), errors='coerce')
296
- min_val, max_val = numeric_values.min(), numeric_values.max()
297
- else:
298
- min_val, max_val = df_sample[col].min(), df_sample[col].max()
299
- data_description += f"\n • {col}: valores de {min_val} a {max_val}"
300
- except:
301
- pass
302
-
303
- if date_cols:
304
- data_description += f"\n- Colunas de DATA/TEMPO ({len(date_cols)}): {', '.join(date_cols)}"
305
-
306
- if categorical_cols:
307
- data_description += f"\n- Colunas CATEGÓRICAS ({len(categorical_cols)}): {', '.join(categorical_cols)}"
308
- # Adiciona informação sobre categorias únicas
309
- for col in categorical_cols[:3]: # Máximo 3 colunas
310
- unique_count = df_sample[col].nunique()
311
- data_description += f"\n • {col}: {unique_count} valores únicos"
312
-
313
- # Destaque especial para múltiplas categóricas importantes
314
- if len(categorical_cols) >= 2 and len(numeric_cols) >= 1:
315
- 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!"
316
-
317
- # Prompt ULTRA SIMPLIFICADO
318
- return (
319
- f"Escolha o gráfico mais adequado e de acordo com pergunta do usuário e os dados:\n\n"
320
- f"COLUNAS RETORNADAS: {', '.join(df_columns)}\n\n"
321
- f"DADOS: {data_description}\n\n"
322
- f"PERGUNTA: {user_query}\n\n"
323
- f"OPÇÕES DE GRÁFICOS::\n"
324
- f"1. Linha - evolução temporal\n"
325
- f"2. Multilinhas - múltiplas tendências\n"
326
- f"3. Área - volume temporal\n"
327
- f"4. Barras Verticais - comparar categorias (nomes curtos)\n"
328
- f"5. Barras Horizontais - comparar categorias (nomes longos)\n"
329
- f"6. Barras Agrupadas - múltiplas métricas\n"
330
- f"7. Barras Empilhadas - partes de um todo\n"
331
- f"8. Pizza - proporções (poucas categorias)\n"
332
- f"9. Dona - proporções (muitas categorias)\n"
333
- f"10. Pizzas Múltiplas - proporções por grupos\n\n"
334
- f"Responda apenas o número (1-10)."
335
- "\n\nINSTRUÇÕES FINAIS:\n"
336
- "1. PRIMEIRO: Verifique se o usuário especificou um tipo de gráfico na pergunta do usuário\n"
337
- "2. SE SIM: Use o gráfico solicitado (consulte o mapeamento acima)\n"
338
- "3. SE NÃO: Escolha o gráfico mais adequado\n\n"
339
- )
340
-
341
- def extract_sql_query_from_response(agent_response: str) -> Optional[str]:
342
- """
343
- Extrai a query SQL da resposta do agente SQL
344
-
345
- Args:
346
- agent_response: Resposta completa do agente SQL
347
-
348
- Returns:
349
- Query SQL extraída ou None se não encontrada
350
- """
351
- if not agent_response:
352
- return None
353
-
354
- # Padrões para encontrar SQL na resposta - ordem de prioridade
355
- sql_patterns = [
356
- # Padrão mais comum: ```sql ... ``` (multiline)
357
- r"```sql\s*(.*?)\s*```",
358
- # Padrão alternativo: ``` ... ``` com SELECT (multiline)
359
- r"```\s*(SELECT.*?)\s*```",
360
- # SELECT com múltiplas linhas até ponto e vírgula
361
- r"(SELECT\s+.*?;)",
362
- # SELECT com múltiplas linhas até quebra dupla ou final
363
- r"(SELECT\s+.*?)(?:\n\s*\n|\n\s*$|\n\s*Agora|\n\s*Em seguida)",
364
- # Padrões com prefixos específicos
365
- r"Query:\s*(SELECT.*?)(?:\n|$|;)",
366
- r"SQL:\s*(SELECT.*?)(?:\n|$|;)",
367
- r"Consulta:\s*(SELECT.*?)(?:\n|$|;)",
368
- # SELECT em uma linha
369
- r"(SELECT\s+[^\n]+)",
370
- ]
371
-
372
- for i, pattern in enumerate(sql_patterns):
373
- matches = re.findall(pattern, agent_response, re.DOTALL | re.IGNORECASE)
374
- if matches:
375
- # Pega a primeira query encontrada
376
- query = matches[0].strip()
377
-
378
- # Limpa a query
379
- query = clean_sql_query(query)
380
-
381
- # Verifica se é uma query válida
382
- if is_valid_sql_query(query):
383
- logging.info(f"[GRAPH] Query SQL extraída (padrão {i+1}): {query[:100]}...")
384
- return query
385
-
386
- # Log da resposta para debug se não encontrar SQL
387
- logging.warning(f"[GRAPH] Não foi possível extrair query SQL. Resposta (primeiros 200 chars): {agent_response[:200]}...")
388
- return None
389
-
390
- def clean_sql_query(query: str) -> str:
391
- """
392
- Limpa e normaliza a query SQL extraída
393
-
394
- Args:
395
- query: Query SQL bruta
396
-
397
- Returns:
398
- Query SQL limpa
399
- """
400
- if not query:
401
- return ""
402
-
403
- # Remove espaços extras e quebras de linha desnecessárias
404
- query = re.sub(r'\s+', ' ', query.strip())
405
-
406
- # Remove ponto e vírgula no final se existir
407
- if query.endswith(';'):
408
- query = query[:-1].strip()
409
-
410
- # Remove aspas ou caracteres especiais no início/fim
411
- query = query.strip('`"\'')
412
-
413
- return query
414
-
415
- def is_valid_sql_query(query: str) -> bool:
416
- """
417
- Verifica se a string é uma query SQL válida
418
-
419
- Args:
420
- query: String para verificar
421
-
422
- Returns:
423
- True se for uma query SQL válida
424
- """
425
- if not query or len(query.strip()) < 6: # Mínimo para "SELECT"
426
- return False
427
-
428
- # Verifica se começa com comando SQL válido
429
- sql_commands = ['SELECT', 'INSERT', 'UPDATE', 'DELETE', 'WITH']
430
- query_upper = query.strip().upper()
431
-
432
- return any(query_upper.startswith(cmd) for cmd in sql_commands)