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"