File size: 11,729 Bytes
410dc31
dce7dab
 
 
 
 
410dc31
 
65165a0
0c323cb
410dc31
0c323cb
dce7dab
410dc31
dce7dab
 
 
410dc31
dce7dab
410dc31
dce7dab
 
410dc31
0c3fa43
410dc31
0c3fa43
 
dce7dab
 
0c3fa43
410dc31
0c3fa43
 
 
 
 
 
 
 
 
 
 
 
4b1ce16
4f32660
 
0c3fa43
410dc31
786df8a
0c3fa43
410dc31
0c3fa43
 
 
786df8a
dce7dab
0c3fa43
 
dce7dab
410dc31
0c3fa43
dce7dab
 
 
410dc31
dce7dab
4f32660
 
410dc31
 
 
4b1ce16
0c3fa43
410dc31
9fb5b11
410dc31
4f32660
 
 
410dc31
4f32660
 
 
 
 
9fb5b11
4f32660
 
 
 
 
410dc31
4f32660
410dc31
4f32660
 
410dc31
 
9fb5b11
4b1ce16
 
410dc31
4b1ce16
 
 
 
dce7dab
410dc31
dce7dab
 
 
0c323cb
dce7dab
410dc31
dce7dab
 
 
410dc31
dce7dab
 
 
410dc31
dce7dab
410dc31
dce7dab
 
0c323cb
dce7dab
410dc31
0c3fa43
410dc31
4f32660
 
410dc31
4f32660
 
 
410dc31
4f32660
 
 
 
 
410dc31
 
4f32660
410dc31
4f32660
 
 
 
 
 
 
410dc31
0c3fa43
410dc31
dce7dab
 
 
0c3fa43
0c323cb
410dc31
0c3fa43
410dc31
0c3fa43
 
dce7dab
0c3fa43
dce7dab
410dc31
 
4f32660
410dc31
 
0c3fa43
 
 
410dc31
0c3fa43
4aeebe8
dce7dab
0c3fa43
dce7dab
410dc31
0c3fa43
 
410dc31
 
0c3fa43
410dc31
dce7dab
786df8a
dce7dab
410dc31
0c3fa43
 
410dc31
 
 
 
 
 
 
 
 
 
 
786df8a
0c3fa43
410dc31
 
 
 
786df8a
0c3fa43
dce7dab
410dc31
4f32660
786df8a
410dc31
0c3fa43
4f32660
410dc31
4f32660
 
410dc31
4f32660
 
410dc31
 
4f32660
4b1ce16
410dc31
4f32660
 
410dc31
4f32660
0c3fa43
410dc31
0c3fa43
 
 
410dc31
 
0c3fa43
 
 
410dc31
 
 
 
 
 
4f32660
410dc31
 
0c3fa43
410dc31
0c3fa43
 
4f32660
0c3fa43
410dc31
0c3fa43
410dc31
0c3fa43
410dc31
 
4f32660
410dc31
0c3fa43
410dc31
0c3fa43
 
 
 
 
 
 
410dc31
 
0c3fa43
 
410dc31
0c3fa43
 
 
 
 
 
 
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
# modules/contexto.py
import logging
import re
import random
import time
import sqlite3
import json
from typing import Optional, List, Dict, Tuple, Any
import modules.config as config
from .database import Database
from .treinamento import Treinamento

try:
    from sentence_transformers import SentenceTransformer
except Exception as e:
    logging.warning(f"sentence_transformers não disponível: {e}")
    SentenceTransformer = None

try:
    import psutil
except Exception:
    psutil = None

try:
    import structlog
except Exception:
    structlog = None

logger = logging.getLogger(__name__)
logging.basicConfig(level=logging.INFO, format='%(asctime)s [%(levelname)s] %(message)s')

if structlog:
    structlog.configure(
        processors=[
            structlog.processors.TimeStamper(fmt="iso"),
            structlog.stdlib.add_log_level,
            structlog.processors.JSONRenderer()
        ],
        context_class=dict,
        logger_factory=structlog.stdlib.LoggerFactory(),
        wrapper_class=structlog.stdlib.BoundLogger,
    )

# Palavras para análise de sentimento heurística
PALAVRAS_POSITIVAS = ['bom', 'ótimo', 'incrível', 'feliz', 'adorei', 'top', 'fixe', 'bué', 'show', 'legal', 'bacana']
PALAVRAS_NEGATIVAS = ['ruim', 'péssimo', 'triste', 'ódio', 'raiva', 'chateado', 'merda', 'porra', 'odeio']


class Contexto:
    """
    Classe para gerenciar o contexto da conversa, análise de intenções e aprendizado
    dinâmico de termos regionais/gírias para cada usuário.
    """
    def __init__(self, db: Database, usuario: Optional[str] = None):
        self.db = db
        self.usuario = usuario
        self.model: Optional[SentenceTransformer] = None
        self.embeddings: Optional[Dict[str, Any]] = None
        self._treinador: Optional[Treinamento] = None

        # Estado de conversa
        self.emocao_atual = "neutra"
        self.espírito_crítico = False
        self.base_conhecimento = {}

        # Garante que termo_contexto seja sempre um dicionário
        self.termo_contexto: Dict[str, Dict] = {}
        self.atualizar_aprendizados_do_banco()

        logger.info("Inicializando Contexto (com NLP avançado, aprendizado de gírias e emoções) ...")

        # Cache para termos regionais e gírias
        self.cache_girias: Dict[str, Any] = {}

    def atualizar_aprendizados_do_banco(self):
        """Carrega todos os dados de aprendizado persistentes do banco."""
        try:
            termos_aprendidos = self.db.recuperar_girias_usuario(self.usuario) if self.usuario else []
            self.termo_contexto = {
                termo['giria']: {"significado": termo['significado'], "frequencia": termo['frequencia']}
                for termo in termos_aprendidos
            }
        except Exception as e:
            logger.warning(f"Falha ao carregar termos/gírias do DB: {e}")
            self.termo_contexto = {}

        try:
            emocao_salva = self.db.recuperar_aprendizado_detalhado(self.usuario, "emocao_atual") if self.usuario else None
            if emocao_salva:
                emocao_dict = json.loads(emocao_salva)
                if isinstance(emocao_dict, dict) and 'emocao' in emocao_dict:
                    self.emocao_atual = emocao_dict['emocao']
                elif isinstance(emocao_salva, str):
                    self.emocao_atual = emocao_salva
        except Exception as e:
            logger.warning(f"Falha ao carregar emoção do DB: {e}")

        logger.info(f"Aprendizados carregados para {self.usuario}.")

    @property
    def ton_predominante(self) -> Optional[str]:
        """Retorna o tom predominante do usuário (acessa o DB)."""
        if self.usuario:
            return self.db.obter_tom_predominante(self.usuario)
        return None

    def get_or_create_treinador(self, interval_hours: int = 24) -> Treinamento:
        """Retorna um treinador associado, criando se necessário."""
        if self._treinador is None:
            self._treinador = Treinamento(self.db, contexto=self, interval_hours=interval_hours)
        return self._treinador

    def _load_model(self):
        """Carrega o modelo SentenceTransformer sob demanda."""
        if self.model is not None:
            return
        if SentenceTransformer is None:
            logger.warning("SentenceTransformer não instalado")
            return
        try:
            self.model = SentenceTransformer('all-MiniLM-L6-v2')
            logger.info("Modelo SentenceTransformer carregado")
        except Exception as e:
            logger.error(f"Erro ao carregar modelo: {e}")
            self.model = None
        self._check_embeddings()

    def _check_embeddings(self):
        """Verifica ou cria embeddings no banco."""
        if self.model and not self.embeddings:
            self.embeddings = {"conhecimento_base": "placeholder"}

    def analisar_emocoes_mensagem(self, mensagem: str) -> Dict[str, Any]:
        """Analisa sentimento e emoção da mensagem (heurística)."""
        mensagem_lower = mensagem.strip().lower()
        pos_count = sum(mensagem_lower.count(w) for w in PALAVRAS_POSITIVAS)
        neg_count = sum(mensagem_lower.count(w) for w in PALAVRAS_NEGATIVAS)

        sentimento = "neutro"
        if pos_count > neg_count:
            sentimento = "positivo"
        elif neg_count > pos_count:
            sentimento = "negativo"

        emocao_predominante = "alegria" if sentimento == "positivo" else "frustração" if sentimento == "negativo" else "neutra"
        self.emocao_atual = emocao_predominante

        return {
            "sentimento_detectado": sentimento,
            "emocao_predominante": emocao_predominante,
            "intensidade_positiva": pos_count,
            "intensidade_negativa": neg_count,
            "tom_sugerido": "casual" if sentimento != "neutro" else "neutro"
        }

    def analisar_intencao_e_normalizar(self, mensagem: str, historico: List[Tuple[str, str]]) -> Dict[str, Any]:
        """Analisa intenção, normaliza e detecta estilo."""
        self._load_model()
        if not isinstance(mensagem, str):
            mensagem = str(mensagem)
        mensagem_lower = mensagem.strip().lower()

        # Intenção
        intencao = "pergunta"
        if '?' not in mensagem_lower and 'porquê' not in mensagem_lower and 'porque' not in mensagem_lower:
            intencao = "afirmacao"
        if any(w in mensagem_lower for w in ['ola', 'oi', 'bom dia', 'boa tarde', 'boa noite', 'como vai']):
            intencao = "saudacao"
        if any(w in mensagem_lower for w in ['tchau', 'ate mais', 'adeus', 'fim', 'parar']):
            intencao = "despedida"

        # Sentimento
        analise_emocional = self.analisar_emocoes_mensagem(mensagem_lower)

        # Estilo
        estilo = "informal"
        if len(re.findall(r'[A-ZÀ-Ÿ]{3,}', mensagem)) >= 2 or re.search(r'\b(Senhor|Doutor|Atenciosamente)\b', mensagem, re.IGNORECASE):
            estilo = "formal"

        usar_nome = random.random() < getattr(config, 'USAR_NOME_PROBABILIDADE', 0.7)

        return {
            "texto_normalizado": mensagem_lower,
            "intencao": intencao,
            "sentimento": analise_emocional['sentimento_detectado'],
            "estilo": estilo,
            "contexto_ajustado": self.substituir_termos_aprendidos(mensagem_lower),
            "ironia": False,
            "meia_frase": False,
            "usar_nome": usar_nome,
            "emocao": self.emocao_atual
        }

    def obter_historico(self, limite: int = 5) -> List[Tuple[str, str]]:
        """Recupera histórico do banco."""
        if not self.usuario:
            return []
        raw = self.db.recuperar_mensagens(self.usuario, limite=limite)
        return raw if raw else []

    def obter_historico_para_llm(self) -> List[Dict]:
        """Formato esperado pelo LLMManager.generate()"""
        raw = self.obter_historico(limite=10)
        history = []
        for user_msg, bot_msg in raw:
            history.append({"role": "user", "content": user_msg})
            history.append({"role": "assistant", "content": bot_msg})
        return history

    def atualizar_contexto(self, mensagem: str, resposta: str, numero: Optional[str] = None):
        """Salva interação e aprende."""
        usuario = self.usuario or 'anonimo'
        final_numero = numero or self.usuario

        try:
            self.db.salvar_mensagem(usuario, mensagem, resposta, numero=final_numero)
            historico = self.obter_historico(limite=10)
            self.aprender_do_historico(mensagem, resposta, historico)
            self.salvar_estado_contexto_no_db(final_numero)
        except Exception as e:
            logger.warning(f'Falha ao salvar: {e}')

    def salvar_estado_contexto_no_db(self, user_key: str):
        """Persiste estado no DB."""
        termos_json = json.dumps(self.termo_contexto)
        try:
            self.db.salvar_aprendizado_detalhado(user_key, "emocao_atual", json.dumps({"emocao": self.emocao_atual}))
            self.db.salvar_contexto(
                user_key=user_key,
                historico="[]",
                emocao_atual=self.emocao_atual,
                termos=termos_json,
                girias=termos_json,
                tom=self.emocao_atual
            )
        except Exception as e:
            logger.error(f"Falha ao salvar contexto: {e}")

    def aprender_do_historico(self, mensagem: str, resposta: str, historico: List[Tuple[str, str]]):
        """Aprende gírias do histórico."""
        if not self.usuario:
            return
        mensagem_lower = mensagem.lower()
        girias_angolanas_simples = ['ya', 'bué', 'fixe', 'puto', 'kota', 'mwangolé']

        for giria in girias_angolanas_simples:
            if giria in mensagem_lower:
                try:
                    significado = f'termo regional para {giria}'
                    self.db.salvar_giria_aprendida(self.usuario, giria, significado, mensagem[:50])
                    self.termo_contexto[giria] = {
                        "significado": significado,
                        "frequencia": self.termo_contexto.get(giria, {}).get("frequencia", 0) + 1
                    }
                except Exception as e:
                    logger.warning(f"Erro ao salvar gíria: {e}")

    def substituir_termos_aprendidos(self, mensagem: str) -> str:
        """Substitui termos aprendidos."""
        for termo, info in self.termo_contexto.items():
            if isinstance(info, dict) and "significado" in info:
                mensagem = re.sub(r'\b' + re.escape(termo) + r'\b', info["significado"], mensagem, flags=re.IGNORECASE)
        return mensagem

    def obter_aprendizado_detalhado(self, chave: str) -> Optional[Dict]:
        """Recupera aprendizado detalhado."""
        try:
            raw = self.db.recuperar_aprendizado_detalhado(self.usuario, chave)
            return json.loads(raw) if raw else None
        except Exception as e:
            logger.warning(f"Erro ao obter aprendizado: {e}")
            return None

    def obter_emocao_atual(self) -> str:
        return self.emocao_atual

    def ativar_espírito_crítico(self):
        self.espírito_crítico = True

    def obter_aprendizados(self) -> Dict[str, Any]:
        """Retorna todos os aprendizados."""
        return {
            "termos": self.termo_contexto,
            "emocao_preferida": self.emocao_atual,
            "ton_predominante": self.ton_predominante
        }

    def salvar_conhecimento_base(self, chave: str, valor: Any):
        self.base_conhecimento[chave] = valor

    def obter_conhecimento_base(self, chave: str) -> Optional[Any]:
        return self.base_conhecimento.get(chave)