Spaces:
Sleeping
Sleeping
File size: 13,806 Bytes
e982206 |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 |
"""
Nó para refinamento de perguntas usando GPT-4o
"""
import logging
import re
from typing import Dict, Any, Optional
from langchain_openai import ChatOpenAI
from langchain.schema import HumanMessage
from utils.config import OPENAI_API_KEY
from utils.object_manager import get_object_manager
async def question_refinement_node(state: Dict[str, Any]) -> Dict[str, Any]:
"""
Nó para refinar perguntas usando GPT-4o para melhorar clareza sem alterar lógica
Args:
state: Estado atual do agente
Returns:
Estado atualizado com pergunta refinada
"""
try:
logging.info("[QUESTION_REFINEMENT] Iniciando refinamento de pergunta")
# Verifica se refinamento já foi aplicado (evita loop infinito)
if state.get("question_refinement_applied", False):
logging.info("[QUESTION_REFINEMENT] Refinamento já aplicado, pulando")
return state
# Verifica se refinamento está habilitado
question_refinement_enabled = state.get("question_refinement_enabled", False)
if not question_refinement_enabled:
logging.info("[QUESTION_REFINEMENT] Refinamento desabilitado, mantendo pergunta original")
state.update({
"refined_question": state.get("user_input", ""),
"question_refinement_applied": False,
"question_refinement_changes": [],
"question_refinement_success": True
})
return state
# Recupera pergunta original
original_question = state.get("user_input", "")
if not original_question or not original_question.strip():
error_msg = "Pergunta original não encontrada ou vazia"
logging.error(f"[QUESTION_REFINEMENT] {error_msg}")
state.update({
"refined_question": original_question,
"question_refinement_applied": False,
"question_refinement_error": error_msg,
"question_refinement_success": False
})
return state
# Verifica se OpenAI API está disponível
if not OPENAI_API_KEY:
error_msg = "OpenAI API Key não configurada para Question Refinement"
logging.error(f"[QUESTION_REFINEMENT] {error_msg}")
state.update({
"refined_question": original_question,
"question_refinement_applied": False,
"question_refinement_error": error_msg,
"question_refinement_success": False
})
return state
# Obtém informações de contexto dos dados
db_sample_dict = state.get("db_sample_dict", {})
context_info = _build_context_info(db_sample_dict)
# Executa refinamento
refinement_result = await _refine_question_with_gpt4o(original_question, context_info)
if refinement_result["success"]:
# Atualiza estado com pergunta refinada
refined_question = refinement_result["refined_question"]
# Atualiza user_input para que toda a pipeline use a pergunta refinada
state.update({
"user_input": refined_question, # ← Substitui a pergunta original
"original_user_input": original_question, # ← Preserva a original
"refined_question": refined_question,
"question_refinement_applied": True,
"question_refinement_changes": refinement_result.get("changes_made", []),
"question_refinement_justification": refinement_result.get("justification", ""),
"question_refinement_success": True,
"question_refinement_has_significant_change": refinement_result.get("has_significant_change", False)
})
logging.info(f"[QUESTION_REFINEMENT] Pergunta refinada com sucesso")
logging.info(f"[QUESTION_REFINEMENT] Original: {original_question}")
logging.info(f"[QUESTION_REFINEMENT] Refinada: {refined_question}")
else:
# Falha no refinamento, mantém pergunta original
error_msg = refinement_result.get("reason", "Erro desconhecido no refinamento")
logging.error(f"[QUESTION_REFINEMENT] {error_msg}")
state.update({
"refined_question": original_question,
"question_refinement_applied": False,
"question_refinement_error": error_msg,
"question_refinement_success": False
})
return state
except Exception as e:
error_msg = f"Erro no nó de refinamento de pergunta: {e}"
logging.error(f"[QUESTION_REFINEMENT] {error_msg}")
# Em caso de erro, mantém pergunta original
original_question = state.get("user_input", "")
state.update({
"refined_question": original_question,
"question_refinement_applied": False,
"question_refinement_error": error_msg,
"question_refinement_success": False
})
return state
def _build_context_info(db_sample_dict: Dict[str, Any]) -> str:
"""
Constrói informações de contexto sobre os dados para o refinamento
Args:
db_sample_dict: Dicionário com amostra dos dados
Returns:
String com informações de contexto
"""
try:
if not db_sample_dict or not db_sample_dict.get("data"):
return "Dados tabulares genéricos"
# Obtém informações básicas dos dados
data = db_sample_dict.get("data", [])
if not data:
return "Dados tabulares genéricos"
# Obtém colunas disponíveis
columns = list(data[0].keys()) if data else []
num_records = len(data)
# Constrói contexto
context_parts = [
f"Tabela com {num_records} registros de amostra",
f"Colunas disponíveis: {', '.join(columns[:10])}" # Limita a 10 colunas
]
if len(columns) > 10:
context_parts.append(f"(e mais {len(columns) - 10} colunas)")
return ". ".join(context_parts)
except Exception as e:
logging.warning(f"Erro ao construir contexto dos dados: {e}")
return "Dados tabulares genéricos"
async def _refine_question_with_gpt4o(original_question: str, context_info: str) -> Dict[str, Any]:
"""
Refina pergunta usando GPT-4o
Args:
original_question: Pergunta original
context_info: Informações de contexto
Returns:
Resultado do refinamento
"""
try:
# Inicializa LLM
llm = ChatOpenAI(
model="gpt-4o",
temperature=0.1, # Baixa temperatura para consistência
max_tokens=500, # Perguntas refinadas devem ser concisas
api_key=OPENAI_API_KEY
)
# Prompt especializado para refinamento
refinement_prompt = f"""
Você é um especialista em SQL e análise de dados. Sua tarefa é refinar uma pergunta para torná-la mais clara e precisa para um agente SQL, SEM ALTERAR a lógica ou intenção original.
PERGUNTA ORIGINAL:
{original_question}
CONTEXTO DOS DADOS:
{context_info}
OBJETIVOS DO REFINAMENTO:
1. Remover ambiguidades que possam gerar interpretações diferentes
2. Tornar a pergunta mais específica e clara
3. Usar terminologia mais precisa para consultas SQL
4. Manter EXATAMENTE a mesma intenção e lógica original
5. Não adicionar filtros ou condições que não estavam implícitos
REGRAS IMPORTANTES:
- NÃO altere o escopo ou objetivo da pergunta
- NÃO adicione filtros temporais se não estavam na pergunta original
- NÃO assuma colunas específicas que não foram mencionadas
- NÃO mude o tipo de análise solicitada
- MANTENHA a linguagem natural e compreensível
- Se a pergunta já está clara, retorne ela praticamente igual
EXEMPLOS DE REFINAMENTO:
Original: "Mostre os dados"
Refinado: "Mostre todos os registros da tabela principal"
Original: "Qual o maior valor?"
Refinado: "Qual é o maior valor numérico encontrado nos dados?"
Original: "Produtos com problema"
Refinado: "Quais produtos apresentam algum tipo de problema ou irregularidade?"
RESPONDA EXATAMENTE NESTE FORMATO:
PERGUNTA_REFINADA: [pergunta melhorada]
MUDANÇAS: [lista das principais mudanças feitas, ou "Nenhuma mudança significativa"]
JUSTIFICATIVA: [breve explicação do refinamento]
"""
# Executa refinamento
message = HumanMessage(content=refinement_prompt)
response_llm = await llm.ainvoke([message])
# Extrai e valida resultado
return _parse_refinement_result(response_llm.content, original_question)
except Exception as e:
logging.error(f"Erro no refinamento com GPT-4o: {e}")
return {
'success': False,
'refined_question': original_question,
'reason': f'Erro no refinamento: {e}'
}
def _parse_refinement_result(llm_response: str, original_question: str) -> Dict[str, Any]:
"""
Extrai resultado do refinamento da resposta do LLM
Args:
llm_response: Resposta do LLM
original_question: Pergunta original para fallback
Returns:
Resultado parseado do refinamento
"""
try:
# Padrões para extrair informações
question_pattern = r'PERGUNTA_REFINADA:\s*(.+?)(?:\n|MUDANÇAS:|$)'
changes_pattern = r'MUDANÇAS:\s*(.+?)(?:\n|JUSTIFICATIVA:|$)'
justification_pattern = r'JUSTIFICATIVA:\s*(.+?)(?:\n|$)'
# Extrai pergunta refinada
question_match = re.search(question_pattern, llm_response, re.IGNORECASE | re.DOTALL)
refined_question = question_match.group(1).strip() if question_match else original_question
# Remove possíveis aspas ou formatação extra
refined_question = refined_question.strip('"\'[]')
# Extrai mudanças
changes_match = re.search(changes_pattern, llm_response, re.IGNORECASE | re.DOTALL)
changes_text = changes_match.group(1).strip() if changes_match else "Não especificado"
# Extrai justificativa
justification_match = re.search(justification_pattern, llm_response, re.IGNORECASE | re.DOTALL)
justification = justification_match.group(1).strip() if justification_match else "Não especificado"
# Processa mudanças em lista
changes_made = []
if changes_text and changes_text.lower() not in ['nenhuma mudança significativa', 'nenhuma', 'não especificado']:
changes_made = [change.strip() for change in changes_text.split(',') if change.strip()]
# Verifica se houve mudança real
has_significant_change = (
refined_question.lower().strip() != original_question.lower().strip() and
len(changes_made) > 0
)
# Valida refinamento
validation = _validate_refinement(original_question, refined_question)
if not validation['valid']:
logging.warning(f"Refinamento inválido: {validation['reason']}")
return {
'success': False,
'refined_question': original_question,
'reason': f'Refinamento inválido: {validation["reason"]}'
}
return {
'success': True,
'refined_question': refined_question,
'original_question': original_question,
'changes_made': changes_made,
'justification': justification,
'has_significant_change': has_significant_change,
'raw_response': llm_response
}
except Exception as e:
logging.error(f"Erro ao parsear resultado do refinamento: {e}")
return {
'success': False,
'refined_question': original_question,
'reason': f'Erro ao parsear resposta: {e}'
}
def _validate_refinement(original: str, refined: str) -> Dict[str, Any]:
"""
Valida se o refinamento mantém a intenção original
Args:
original: Pergunta original
refined: Pergunta refinada
Returns:
Resultado da validação
"""
try:
# Verificações básicas de qualidade (muito permissivas)
checks = {
'not_empty': bool(refined and refined.strip()),
'reasonable_length': 3 <= len(refined) <= 2000, # Muito flexível
'has_content': len(refined.strip()) >= 3, # Pelo menos 3 caracteres
'no_obvious_errors': not any(error in refined.lower() for error in ['erro', 'error', 'desculpe', 'não posso', 'não é possível'])
}
all_valid = all(checks.values())
# Se a pergunta refinada é igual à original, também é válido
if refined.strip().lower() == original.strip().lower():
all_valid = True
return {
'valid': all_valid,
'checks': checks,
'reason': 'Refinamento válido' if all_valid else f'Problemas detectados: {[k for k, v in checks.items() if not v]}'
}
except Exception as e:
return {
'valid': False,
'reason': f'Erro na validação: {e}'
}
def route_after_question_refinement(state: Dict[str, Any]) -> str:
"""
Roteamento após refinamento de pergunta
Args:
state: Estado atual
Returns:
Nome do próximo nó
"""
# Após refinamento, sempre vai para validação de processing
return "validate_processing"
|