|
|
""" |
|
|
Ferramentas para o agente SQL |
|
|
""" |
|
|
import time |
|
|
import logging |
|
|
import re |
|
|
from typing import Dict, Any, Optional, List |
|
|
from huggingface_hub import InferenceClient |
|
|
from langchain_community.utilities import SQLDatabase |
|
|
from langchain_openai import ChatOpenAI |
|
|
from langchain_anthropic import ChatAnthropic |
|
|
import pandas as pd |
|
|
import sqlalchemy as sa |
|
|
|
|
|
from utils.config import ( |
|
|
HUGGINGFACE_API_KEY, |
|
|
OPENAI_API_KEY, |
|
|
ANTHROPIC_API_KEY, |
|
|
AVAILABLE_MODELS, |
|
|
REFINEMENT_MODELS, |
|
|
LLAMA_MODELS, |
|
|
MAX_TOKENS_MAP, |
|
|
OPENAI_MODELS, |
|
|
ANTHROPIC_MODELS, |
|
|
HUGGINGFACE_MODELS |
|
|
) |
|
|
|
|
|
|
|
|
hf_client = InferenceClient( |
|
|
provider="together", |
|
|
api_key=HUGGINGFACE_API_KEY |
|
|
) |
|
|
|
|
|
|
|
|
openai_client = None |
|
|
if OPENAI_API_KEY: |
|
|
openai_client = ChatOpenAI( |
|
|
api_key=OPENAI_API_KEY, |
|
|
temperature=0 |
|
|
) |
|
|
|
|
|
|
|
|
anthropic_client = None |
|
|
if ANTHROPIC_API_KEY: |
|
|
anthropic_client = ChatAnthropic( |
|
|
model="claude-3-5-sonnet-20241022", |
|
|
api_key=ANTHROPIC_API_KEY, |
|
|
temperature=0 |
|
|
) |
|
|
|
|
|
|
|
|
|
|
|
def is_greeting(user_query: str) -> bool: |
|
|
""" |
|
|
Verifica se a query do usuário é uma saudação |
|
|
|
|
|
Args: |
|
|
user_query: Query do usuário |
|
|
|
|
|
Returns: |
|
|
True se for saudação, False caso contrário |
|
|
""" |
|
|
greetings = ["olá", "oi", "bom dia", "boa tarde", "boa noite", "oi, tudo bem?"] |
|
|
return user_query.lower().strip() in greetings |
|
|
|
|
|
def detect_query_type(user_query: str) -> str: |
|
|
""" |
|
|
Detecta o tipo de processamento necessário para a query do usuário |
|
|
|
|
|
Args: |
|
|
user_query: Pergunta do usuário |
|
|
|
|
|
Returns: |
|
|
Tipo de processamento: 'sql_query', 'sql_query_graphic', 'prediction', 'chart' |
|
|
""" |
|
|
query_lower = user_query.lower().strip() |
|
|
|
|
|
|
|
|
prediction_keywords = ['prever', 'predizer', 'previsão', 'forecast', 'predict', 'tendência', 'projeção'] |
|
|
|
|
|
|
|
|
chart_keywords = [ |
|
|
'gráfico', 'grafico', 'chart', 'plot', 'visualizar', 'visualização', 'visualizacao', |
|
|
'mostrar gráfico', 'mostrar grafico', 'gerar gráfico', 'gerar grafico', |
|
|
'criar gráfico', 'criar grafico', 'plotar', 'desenhar gráfico', 'desenhar grafico', |
|
|
'exibir gráfico', 'exibir grafico', 'fazer gráfico', 'fazer grafico', |
|
|
'gráfico de', 'grafico de', 'em gráfico', 'em grafico', |
|
|
'barras', 'linha', 'pizza', 'área', 'area', 'histograma', |
|
|
'scatter', 'dispersão', 'dispersao', 'boxplot', 'heatmap' |
|
|
] |
|
|
|
|
|
|
|
|
has_chart_request = any(keyword in query_lower for keyword in chart_keywords) |
|
|
|
|
|
|
|
|
has_prediction_request = any(keyword in query_lower for keyword in prediction_keywords) |
|
|
|
|
|
|
|
|
if has_prediction_request: |
|
|
return 'prediction' |
|
|
elif has_chart_request: |
|
|
return 'sql_query_graphic' |
|
|
else: |
|
|
return 'sql_query' |
|
|
|
|
|
def prepare_processing_context(user_query: str, columns_data: dict, connection_type: str = "csv", single_table_mode: bool = False, selected_table: str = None, available_tables: list = None) -> str: |
|
|
""" |
|
|
Prepara o contexto inicial para o Processing Agent de forma dinâmica |
|
|
|
|
|
NOVA IMPLEMENTAÇÃO: Recebe dados já processados pelo processing_node.py |
|
|
para evitar consultas redundantes e garantir consistência |
|
|
|
|
|
Args: |
|
|
user_query: Pergunta do usuário |
|
|
columns_data: Dados das colunas já processados pelo processing_node.py |
|
|
Formato: {"table_name": [{"column": "nome", "type": "tipo", "examples": "exemplos", "stats": "estatísticas"}]} |
|
|
connection_type: Tipo de conexão ("csv" ou "postgresql") |
|
|
single_table_mode: Se está em modo tabela única (PostgreSQL) |
|
|
selected_table: Tabela selecionada (PostgreSQL modo único) |
|
|
available_tables: Lista de tabelas disponíveis (PostgreSQL) |
|
|
|
|
|
Returns: |
|
|
Contexto formatado para o Processing Agent |
|
|
""" |
|
|
logging.info(f"[TOOLS] ===== PREPARANDO CONTEXTO =====") |
|
|
logging.info(f"[TOOLS] Connection type: {connection_type}") |
|
|
logging.info(f"[TOOLS] Single table mode: {single_table_mode}") |
|
|
logging.info(f"[TOOLS] Selected table: {selected_table}") |
|
|
logging.info(f"[TOOLS] Available tables: {available_tables}") |
|
|
logging.info(f"[TOOLS] Columns data keys: {list(columns_data.keys()) if columns_data else 'None'}") |
|
|
|
|
|
|
|
|
column_info = [] |
|
|
|
|
|
if connection_type.lower() == "postgresql": |
|
|
logging.info(f"[TOOLS] 🔵 PROCESSANDO POSTGRESQL") |
|
|
|
|
|
if single_table_mode and selected_table: |
|
|
|
|
|
logging.info(f"[TOOLS] 🔵 PostgreSQL MODO ÚNICO - tabela: {selected_table}") |
|
|
|
|
|
table_data = columns_data.get(selected_table, []) |
|
|
if table_data: |
|
|
for col_info in table_data: |
|
|
column_line = f"- {col_info['column']} ({col_info['type']})" |
|
|
if col_info.get('examples'): |
|
|
column_line += f": {col_info['examples']}" |
|
|
if col_info.get('stats'): |
|
|
column_line += f"{col_info['stats']}" |
|
|
column_info.append(column_line) |
|
|
|
|
|
logging.info(f"[TOOLS] ✅ PostgreSQL modo único processado: {len(column_info)} colunas") |
|
|
else: |
|
|
logging.warning(f"[TOOLS] ⚠️ Nenhum dado encontrado para tabela {selected_table}") |
|
|
|
|
|
else: |
|
|
|
|
|
logging.info(f"[TOOLS] 🔵 PostgreSQL MODO MULTI-TABELA - {len(columns_data)} tabelas") |
|
|
|
|
|
for table_name, table_columns in columns_data.items(): |
|
|
column_info.append(f"\n**Tabela: {table_name}**") |
|
|
|
|
|
if table_columns: |
|
|
for col_info in table_columns: |
|
|
column_line = f"- {col_info['column']} ({col_info['type']})" |
|
|
if col_info.get('examples'): |
|
|
column_line += f": {col_info['examples']}" |
|
|
if col_info.get('stats'): |
|
|
column_line += f"{col_info['stats']}" |
|
|
column_info.append(column_line) |
|
|
else: |
|
|
column_info.append("- (Tabela sem dados ou colunas)") |
|
|
|
|
|
logging.info(f"[TOOLS] ✅ Tabela {table_name} processada: {len(table_columns)} colunas") |
|
|
|
|
|
logging.info(f"[TOOLS] ✅ PostgreSQL multi-tabela processado: {len(column_info)} itens") |
|
|
|
|
|
else: |
|
|
|
|
|
logging.info(f"[TOOLS] 🟡 PROCESSANDO CSV/SQLITE") |
|
|
|
|
|
|
|
|
for table_name, table_columns in columns_data.items(): |
|
|
for col_info in table_columns: |
|
|
column_line = f"- {col_info['column']} ({col_info['type']})" |
|
|
if col_info.get('examples'): |
|
|
column_line += f": {col_info['examples']}" |
|
|
if col_info.get('stats'): |
|
|
column_line += f"{col_info['stats']}" |
|
|
column_info.append(column_line) |
|
|
|
|
|
logging.info(f"[TOOLS] ✅ CSV/SQLite processado: {len(column_info)} colunas") |
|
|
|
|
|
columns_description = "\n".join(column_info) |
|
|
logging.info(f"[TOOLS] ===== CONTEXTO FINAL =====") |
|
|
logging.info(f"[TOOLS] Total de itens no contexto: {len(column_info)}") |
|
|
logging.info(f"[TOOLS] ===== FIM PREPARAÇÃO =====") |
|
|
|
|
|
|
|
|
if connection_type.lower() == "postgresql": |
|
|
if single_table_mode and selected_table: |
|
|
|
|
|
table_info = f"Nome da tabela: {selected_table}" |
|
|
table_instructions = f'Use apenas as colunas que existem na tabela "{selected_table}".' |
|
|
context_note = f"MODO TABELA ÚNICA ATIVO - Trabalhando apenas com a tabela '{selected_table}'" |
|
|
else: |
|
|
|
|
|
if available_tables: |
|
|
tables_list = ", ".join(available_tables) |
|
|
table_info = f"Tabelas disponíveis: {tables_list}" |
|
|
else: |
|
|
table_info = "Múltiplas tabelas disponíveis no PostgreSQL" |
|
|
table_instructions = "Use as tabelas disponíveis no PostgreSQL. Pode fazer JOINs entre tabelas quando necessário." |
|
|
context_note = "MODO MULTI-TABELA ATIVO - Pode usar todas as tabelas e fazer JOINs" |
|
|
else: |
|
|
|
|
|
table_info = "Nome da tabela: tabela" |
|
|
table_instructions = 'Use apenas as colunas que existem na tabela "tabela".' |
|
|
context_note = "CONEXÃO CSV/SQLite - Dados convertidos para tabela única" |
|
|
|
|
|
context = f""" |
|
|
Você é um especialista em SQL que deve analisar a pergunta do usuário e gerar uma query SQL otimizada. |
|
|
|
|
|
INSTRUÇÕES IMPORTANTES: |
|
|
1. Analise a pergunta do usuário e o contexto dos dados |
|
|
2. Gere uma query SQL precisa e otimizada |
|
|
3. {table_instructions} |
|
|
4. Para cálculos complexos, use CTEs quando necessário |
|
|
5. Inclua LIMIT quando apropriado para evitar resultados excessivos |
|
|
6. Considere os tipos de dados e valores de exemplo |
|
|
|
|
|
CONTEXTO DOS DADOS: |
|
|
{context_note} |
|
|
{table_info} |
|
|
|
|
|
Colunas disponíveis com tipos e exemplos (baseado na amostra atual): |
|
|
{columns_description} |
|
|
|
|
|
PERGUNTA DO USUÁRIO: |
|
|
{user_query} |
|
|
|
|
|
Responda somente nesse formato: |
|
|
|
|
|
Opção de querySQL: [QuerySQL] |
|
|
Observações: [Observações] |
|
|
""" |
|
|
|
|
|
return context.strip() |
|
|
|
|
|
def prepare_sql_context(user_query: str, db_sample: pd.DataFrame, suggested_query: str = "", query_observations: str = "") -> str: |
|
|
""" |
|
|
Prepara o contexto inicial para ser enviado diretamente ao agentSQL |
|
|
|
|
|
Args: |
|
|
user_query: Pergunta do usuário |
|
|
db_sample: Amostra dos dados do banco |
|
|
suggested_query: Query SQL sugerida pelo Processing Agent (opcional) |
|
|
query_observations: Observações sobre a query sugerida (opcional) |
|
|
|
|
|
Returns: |
|
|
Contexto formatado para o agentSQL |
|
|
""" |
|
|
import logging |
|
|
|
|
|
|
|
|
contexto_base = ( |
|
|
"Você é um assistente especializado em consultas SQL, geração de querySQL e análise de dados.\n" |
|
|
"Sua tarefa é responder à pergunta do usuário abaixo, gerando uma query SQL que retorne os dados necessários para responder a pergunta.\n\n" |
|
|
) |
|
|
|
|
|
|
|
|
contexto_opcao_query = "" |
|
|
if suggested_query and suggested_query.strip(): |
|
|
logging.info(f"[SQL CONTEXT] 🎯 Incluindo query sugerida no contexto do SQL Agent") |
|
|
|
|
|
|
|
|
contexto_opcao_query = f"Opção de querySQL:\n```sql\n{suggested_query}\n```\n\n" |
|
|
|
|
|
if query_observations and query_observations.strip(): |
|
|
contexto_opcao_query += f"Observações:\n{query_observations}\n\n" |
|
|
|
|
|
contexto_opcao_query += "Você pode usar esta opção de query se ela estiver correta, ou criar sua própria query.\n\n" |
|
|
logging.info(f"[SQL CONTEXT] ✅ Contexto do SQL Agent preparado COM sugestão de query") |
|
|
else: |
|
|
logging.info(f"[SQL CONTEXT] ℹ️ Contexto do SQL Agent preparado SEM sugestão de query") |
|
|
|
|
|
|
|
|
context = contexto_base + contexto_opcao_query + f"Pergunta do usuário: \n{user_query}" |
|
|
|
|
|
return context |
|
|
|
|
|
async def refine_response_with_llm( |
|
|
user_question: str, |
|
|
sql_response: str, |
|
|
chart_md: str = "" |
|
|
) -> str: |
|
|
""" |
|
|
Refina a resposta usando um modelo LLM adicional |
|
|
|
|
|
Args: |
|
|
user_question: Pergunta original do usuário |
|
|
sql_response: Resposta do agente SQL |
|
|
chart_md: Markdown de gráficos (opcional) |
|
|
|
|
|
Returns: |
|
|
Resposta refinada |
|
|
""" |
|
|
prompt = ( |
|
|
f"Pergunta do usuário:\n{user_question}\n\n" |
|
|
f"Resposta gerada pelo agente SQL:\n{sql_response}\n\n" |
|
|
"Sua tarefa é refinar a resposta para deixá-la mais clara, completa e compreensível em português, " |
|
|
"mantendo a resposta original no início do texto e adicionando insights úteis sobre logística de entregas de produtos, " |
|
|
"por exemplo: comparar com padrões típicos, identificar possíveis problemas ou sugerir ações para melhorar atrasos, performance ou custos. " |
|
|
"Evite repetir informações sem necessidade e não invente dados." |
|
|
) |
|
|
|
|
|
logging.info(f"[DEBUG] Prompt enviado ao modelo de refinamento:\n{prompt}\n") |
|
|
|
|
|
try: |
|
|
response = hf_client.chat.completions.create( |
|
|
model=REFINEMENT_MODELS["LLaMA 70B"], |
|
|
messages=[{"role": "system", "content": prompt}], |
|
|
max_tokens=1200, |
|
|
stream=False |
|
|
) |
|
|
improved_response = response["choices"][0]["message"]["content"] |
|
|
logging.info(f"[DEBUG] Resposta do modelo de refinamento:\n{improved_response}\n") |
|
|
return improved_response + ("\n\n" + chart_md if chart_md else "") |
|
|
|
|
|
except Exception as e: |
|
|
logging.error(f"[ERRO] Falha ao refinar resposta com LLM: {e}") |
|
|
return sql_response + ("\n\n" + chart_md if chart_md else "") |
|
|
|
|
|
class CacheManager: |
|
|
"""Gerenciador de cache para queries""" |
|
|
|
|
|
def __init__(self): |
|
|
self.query_cache: Dict[str, str] = {} |
|
|
self.history_log: List[Dict[str, Any]] = [] |
|
|
self.recent_history: List[Dict[str, str]] = [] |
|
|
|
|
|
def get_cached_response(self, query: str) -> Optional[str]: |
|
|
"""Obtém resposta do cache""" |
|
|
return self.query_cache.get(query) |
|
|
|
|
|
def cache_response(self, query: str, response: str): |
|
|
"""Armazena resposta no cache""" |
|
|
self.query_cache[query] = response |
|
|
|
|
|
def add_to_history(self, entry: Dict[str, Any]): |
|
|
"""Adiciona entrada ao histórico""" |
|
|
self.history_log.append(entry) |
|
|
|
|
|
def update_recent_history(self, user_input: str, response: str): |
|
|
"""Atualiza histórico recente""" |
|
|
self.recent_history.append({"role": "user", "content": user_input}) |
|
|
self.recent_history.append({"role": "assistant", "content": response}) |
|
|
|
|
|
|
|
|
if len(self.recent_history) > 4: |
|
|
self.recent_history.pop(0) |
|
|
self.recent_history.pop(0) |
|
|
|
|
|
def clear_cache(self): |
|
|
"""Limpa todo o cache""" |
|
|
self.query_cache.clear() |
|
|
self.history_log.clear() |
|
|
self.recent_history.clear() |
|
|
|
|
|
def get_history(self) -> List[Dict[str, Any]]: |
|
|
"""Retorna histórico completo""" |
|
|
return self.history_log |
|
|
|
|
|
|
|
|
|
|
|
def generate_graph_type_context(user_query: str, sql_query: str, df_columns: List[str], df_sample: pd.DataFrame) -> str: |
|
|
""" |
|
|
Gera contexto para LLM escolher o tipo de gráfico mais adequado |
|
|
|
|
|
Args: |
|
|
user_query: Pergunta original do usuário |
|
|
sql_query: Query SQL gerada pelo agente |
|
|
df_columns: Lista de colunas retornadas pela query |
|
|
df_sample: Amostra dos dados para análise |
|
|
|
|
|
Returns: |
|
|
Contexto formatado para a LLM |
|
|
""" |
|
|
|
|
|
data_description = "" |
|
|
if not df_sample.empty: |
|
|
|
|
|
numeric_cols = [] |
|
|
date_cols = [] |
|
|
categorical_cols = [] |
|
|
|
|
|
for col in df_sample.columns: |
|
|
col_data = df_sample[col] |
|
|
|
|
|
|
|
|
try: |
|
|
|
|
|
if col_data.dtype == 'object': |
|
|
test_numeric = pd.to_numeric(col_data.astype(str).str.replace(',', '.'), errors='coerce') |
|
|
if test_numeric.notna().sum() > len(col_data) * 0.8: |
|
|
numeric_cols.append(col) |
|
|
else: |
|
|
categorical_cols.append(col) |
|
|
elif pd.api.types.is_numeric_dtype(col_data): |
|
|
numeric_cols.append(col) |
|
|
elif pd.api.types.is_datetime64_any_dtype(col_data) or 'data' in col.lower(): |
|
|
date_cols.append(col) |
|
|
else: |
|
|
categorical_cols.append(col) |
|
|
except: |
|
|
categorical_cols.append(col) |
|
|
|
|
|
|
|
|
data_description = "\nAmostra dos dados (primeiras 3 linhas):\n" |
|
|
data_description += df_sample.head(3).to_string(index=False) |
|
|
|
|
|
|
|
|
data_description += f"\n\nAnálise dos dados ({len(df_sample)} linhas total):" |
|
|
data_description += f"\n- Total de colunas: {len(df_sample.columns)}" |
|
|
|
|
|
if numeric_cols: |
|
|
data_description += f"\n- Colunas NUMÉRICAS ({len(numeric_cols)}): {', '.join(numeric_cols)}" |
|
|
|
|
|
for col in numeric_cols[:2]: |
|
|
try: |
|
|
if df_sample[col].dtype == 'object': |
|
|
|
|
|
numeric_values = pd.to_numeric(df_sample[col].astype(str).str.replace(',', '.'), errors='coerce') |
|
|
min_val, max_val = numeric_values.min(), numeric_values.max() |
|
|
else: |
|
|
min_val, max_val = df_sample[col].min(), df_sample[col].max() |
|
|
data_description += f"\n • {col}: valores de {min_val} a {max_val}" |
|
|
except: |
|
|
pass |
|
|
|
|
|
if date_cols: |
|
|
data_description += f"\n- Colunas de DATA/TEMPO ({len(date_cols)}): {', '.join(date_cols)}" |
|
|
|
|
|
if categorical_cols: |
|
|
data_description += f"\n- Colunas CATEGÓRICAS ({len(categorical_cols)}): {', '.join(categorical_cols)}" |
|
|
|
|
|
for col in categorical_cols[:3]: |
|
|
unique_count = df_sample[col].nunique() |
|
|
data_description += f"\n • {col}: {unique_count} valores únicos" |
|
|
|
|
|
|
|
|
if len(categorical_cols) >= 2 and len(numeric_cols) >= 1: |
|
|
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!" |
|
|
|
|
|
|
|
|
return ( |
|
|
f"Escolha o gráfico mais adequado e de acordo com pergunta do usuário e os dados:\n\n" |
|
|
f"COLUNAS RETORNADAS: {', '.join(df_columns)}\n\n" |
|
|
f"DADOS: {data_description}\n\n" |
|
|
f"PERGUNTA: {user_query}\n\n" |
|
|
f"OPÇÕES DE GRÁFICOS::\n" |
|
|
f"1. Linha - evolução temporal\n" |
|
|
f"2. Multilinhas - múltiplas tendências\n" |
|
|
f"3. Área - volume temporal\n" |
|
|
f"4. Barras Verticais - comparar categorias (nomes curtos)\n" |
|
|
f"5. Barras Horizontais - comparar categorias (nomes longos)\n" |
|
|
f"6. Barras Agrupadas - múltiplas métricas\n" |
|
|
f"7. Barras Empilhadas - partes de um todo\n" |
|
|
f"8. Pizza - proporções (poucas categorias)\n" |
|
|
f"9. Dona - proporções (muitas categorias)\n" |
|
|
f"10. Pizzas Múltiplas - proporções por grupos\n\n" |
|
|
f"Responda apenas o número (1-10)." |
|
|
"\n\nINSTRUÇÕES FINAIS:\n" |
|
|
"1. PRIMEIRO: Verifique se o usuário especificou um tipo de gráfico na pergunta do usuário\n" |
|
|
"2. SE SIM: Use o gráfico solicitado (consulte o mapeamento acima)\n" |
|
|
"3. SE NÃO: Escolha o gráfico mais adequado\n\n" |
|
|
) |
|
|
|
|
|
def extract_sql_query_from_response(agent_response: str) -> Optional[str]: |
|
|
""" |
|
|
Extrai a query SQL da resposta do agente SQL |
|
|
|
|
|
Args: |
|
|
agent_response: Resposta completa do agente SQL |
|
|
|
|
|
Returns: |
|
|
Query SQL extraída ou None se não encontrada |
|
|
""" |
|
|
if not agent_response: |
|
|
return None |
|
|
|
|
|
|
|
|
sql_patterns = [ |
|
|
|
|
|
r"```sql\s*(.*?)\s*```", |
|
|
|
|
|
r"```\s*(SELECT.*?)\s*```", |
|
|
|
|
|
r"(SELECT\s+.*?;)", |
|
|
|
|
|
r"(SELECT\s+.*?)(?:\n\s*\n|\n\s*$|\n\s*Agora|\n\s*Em seguida)", |
|
|
|
|
|
r"Query:\s*(SELECT.*?)(?:\n|$|;)", |
|
|
r"SQL:\s*(SELECT.*?)(?:\n|$|;)", |
|
|
r"Consulta:\s*(SELECT.*?)(?:\n|$|;)", |
|
|
|
|
|
r"(SELECT\s+[^\n]+)", |
|
|
] |
|
|
|
|
|
for i, pattern in enumerate(sql_patterns): |
|
|
matches = re.findall(pattern, agent_response, re.DOTALL | re.IGNORECASE) |
|
|
if matches: |
|
|
|
|
|
query = matches[0].strip() |
|
|
|
|
|
|
|
|
query = clean_sql_query(query) |
|
|
|
|
|
|
|
|
if is_valid_sql_query(query): |
|
|
logging.info(f"[GRAPH] Query SQL extraída (padrão {i+1}): {query[:100]}...") |
|
|
return query |
|
|
|
|
|
|
|
|
logging.warning(f"[GRAPH] Não foi possível extrair query SQL. Resposta (primeiros 200 chars): {agent_response[:200]}...") |
|
|
return None |
|
|
|
|
|
def clean_sql_query(query: str) -> str: |
|
|
""" |
|
|
Limpa e normaliza a query SQL extraída |
|
|
|
|
|
Args: |
|
|
query: Query SQL bruta |
|
|
|
|
|
Returns: |
|
|
Query SQL limpa |
|
|
""" |
|
|
if not query: |
|
|
return "" |
|
|
|
|
|
|
|
|
query = re.sub(r'\s+', ' ', query.strip()) |
|
|
|
|
|
|
|
|
if query.endswith(';'): |
|
|
query = query[:-1].strip() |
|
|
|
|
|
|
|
|
query = query.strip('`"\'') |
|
|
|
|
|
return query |
|
|
|
|
|
def is_valid_sql_query(query: str) -> bool: |
|
|
""" |
|
|
Verifica se a string é uma query SQL válida |
|
|
|
|
|
Args: |
|
|
query: String para verificar |
|
|
|
|
|
Returns: |
|
|
True se for uma query SQL válida |
|
|
""" |
|
|
if not query or len(query.strip()) < 6: |
|
|
return False |
|
|
|
|
|
|
|
|
sql_commands = ['SELECT', 'INSERT', 'UPDATE', 'DELETE', 'WITH'] |
|
|
query_upper = query.strip().upper() |
|
|
|
|
|
return any(query_upper.startswith(cmd) for cmd in sql_commands) |
|
|
|