akra35567 commited on
Commit
65165a0
·
1 Parent(s): 85b104e

Update modules/contexto.py

Browse files
Files changed (1) hide show
  1. modules/contexto.py +188 -302
modules/contexto.py CHANGED
@@ -1,302 +1,188 @@
1
- import logging
2
- import re
3
- try:
4
- # sentence-transformers é uma dependência opcional; Pylance pode não encontrá-la
5
- # em alguns ambientes. Silenciaremos o aviso de import faltante para manter
6
- # o comportamento resiliente em tempo de execução.
7
- from sentence_transformers import SentenceTransformer # type: ignore[reportMissingImports]
8
- except Exception as e:
9
- logging.warning(f"sentence_transformers não disponível: {e}")
10
- SentenceTransformer = None
11
-
12
- from modules.database import Database
13
- import random
14
- import modules.config as config
15
- try:
16
- import psutil # type: ignore[reportMissingImports]
17
- except Exception:
18
- psutil = None
19
- import time
20
- try:
21
- import structlog # type: ignore[reportMissingImports]
22
- except Exception:
23
- structlog = None
24
- import sqlite3
25
- from typing import Optional
26
- from modules.treinamento import Treinamento
27
-
28
- # Configuração do logging (fallback se structlog ausente)
29
- logging.basicConfig(level=logging.INFO, format='%(asctime)s [%(levelname)s] %(message)s')
30
- if structlog:
31
- structlog.configure(
32
- processors=[
33
- structlog.processors.TimeStamper(fmt="iso"),
34
- structlog.stdlib.add_log_level,
35
- structlog.processors.JSONRenderer()
36
- ],
37
- context_class=dict,
38
- logger_factory=structlog.stdlib.LoggerFactory(),
39
- wrapper_class=structlog.stdlib.BoundLogger,
40
- cache_logger_on_first_use=True
41
- )
42
- logger = structlog.get_logger(__name__)
43
- else:
44
- logger = logging.getLogger(__name__)
45
-
46
- class Contexto:
47
- """Classe para gerenciar o contexto da conversa, análise de intenções e aprendizado dinâmico de termos regionais/gírias."""
48
- def __init__(self, db: Database, usuario=None):
49
- self.db = db
50
- self.usuario = usuario
51
- self.model = None
52
- self.embeddings = None
53
- self._treinador: Optional[Treinamento] = None
54
- self.emocao_atual = "neutra" # Emoções: neutra, feliz, irritada, crítica
55
- self.espírito_crítico = False # Ativar espírito crítico para respostas questionadoras
56
- self.base_conhecimento = {} # Conhecimento geral aprendido
57
- # Garantir que termo_contexto seja sempre um dicionário
58
- termos = self.obter_aprendizado_detalhado("termos")
59
- self.termo_contexto = termos if isinstance(termos, dict) else {}
60
- logger.info("🟢 Inicializando Contexto (com NLP avançado, aprendizado de gírias e emoções) ...")
61
-
62
- def get_or_create_treinador(self, interval_hours: int = 24) -> Treinamento:
63
- """Retorna um treinador associado a este contexto, criando se necessário."""
64
- if self._treinador is None:
65
- self._treinador = Treinamento(self.db, contexto=self, interval_hours=interval_hours)
66
- return self._treinador
67
-
68
- def _load_model(self):
69
- """Carrega o modelo SentenceTransformer e embeddings sob demanda."""
70
- if self.model is not None:
71
- return
72
- start_time = time.time()
73
- if psutil:
74
- try:
75
- process = psutil.Process()
76
- mem_before = process.memory_info().rss / 1024 / 1024
77
- cpu_percent = psutil.cpu_percent()
78
- logger.info({"event": "Before model load", "cpu_percent": cpu_percent, "memory_mb": mem_before})
79
- except Exception as e:
80
- logger.warning(f"Erro ao coletar métricas de sistema: {e}")
81
- else:
82
- logger.debug("psutil não disponível para métricas de sistema")
83
- if SentenceTransformer is None:
84
- logger.warning({"event": "Modelo SentenceTransformer não será carregado", "reason": "Biblioteca não instalada"})
85
- return
86
- try:
87
- self.model = SentenceTransformer('all-MiniLM-L6-v2')
88
- logger.info({"event": "Modelo SentenceTransformer carregado com sucesso"})
89
- except Exception as e:
90
- logger.error({"event": "Erro ao carregar modelo", "error": str(e)})
91
- self.model = None
92
- self._check_embeddings()
93
- duration = time.time() - start_time
94
- logger.info({"event": "Modelo carregado", "duration_seconds": duration})
95
-
96
- def _check_embeddings(self):
97
- """Verifica ou cria embeddings no banco de dados SQLite, se a tabela existir."""
98
- if self.model is None:
99
- logger.warning({"event": "Embeddings não serão verificados", "reason": "Modelo não carregado"})
100
- return
101
- logger.info({"event": "Verificando embeddings no banco de dados"})
102
- try:
103
- with sqlite3.connect(self.db.db_path) as conn:
104
- c = conn.cursor()
105
- c.execute("""SELECT name FROM sqlite_master WHERE type='table' AND name='embeddings'""")
106
- table_exists = c.fetchone()
107
- if not table_exists:
108
- logger.warning({"event": "Tabela embeddings não encontrada", "action": "Ignorando verificação de embeddings"})
109
- return
110
- c.execute("SELECT COUNT(*) FROM embeddings")
111
- if c.fetchone()[0] == 0:
112
- logger.info({"event": "Criando embeddings iniciais"})
113
- sentences = ["oi", "tchau", "fixe", "puto"]
114
- embeddings = self.model.encode(sentences)
115
- for sentence, embedding in zip(sentences, embeddings):
116
- c.execute("INSERT INTO embeddings (texto, embedding) VALUES (?, ?)", (sentence, embedding.tobytes()))
117
- conn.commit()
118
- logger.info({"event": "Embeddings iniciais criados"})
119
- else:
120
- logger.info({"event": "Embeddings já existem no banco de dados"})
121
- except Exception as e:
122
- logger.error({"event": "Erro ao verificar embeddings", "error": str(e)})
123
-
124
- def analisar_intencao_e_normalizar(self, mensagem: str, historico: list) -> dict:
125
- """Analisa a intenção, normaliza a mensagem, substitui termos aprendidos e detecta ironias e meias frases."""
126
- self._load_model() # Carrega o modelo apenas quando necessário
127
-
128
- # ALTERAÇÃO: Handling de encoding UTF-8 para preservar acentos e caracteres especiais
129
- if not isinstance(mensagem, str):
130
- mensagem = str(mensagem)
131
- mensagem = mensagem.encode('utf-8', 'ignore').decode('utf-8') # Limpa encoding ruins
132
-
133
- # ALTERAÇÃO: Regex Unicode-safe (permite letras acentuadas, números, etc.)
134
- mensagem = re.sub(r'[^\w\s\.,!?😅👍]', '', mensagem.lower(), flags=re.UNICODE).strip()
135
-
136
- # Substituir termos aprendidos antes da análise
137
- mensagem = self.substituir_termos_aprendidos(mensagem)
138
- intencao = "neutro"
139
- sentimento = "neutro"
140
- ironia = False
141
- meia_frase = False
142
- # Detecção de meia frase (frases curtas ou incompletas)
143
- if len(mensagem.split()) <= 3 or "..." in mensagem:
144
- meia_frase = True
145
- # Detecção de intenção
146
- if any(word in mensagem for word in ["oi", "olá", "eai", "eae"]):
147
- intencao = "saudacao"
148
- elif any(word in mensagem for word in ["tchau", "flw", "bazar", "até"]):
149
- intencao = "despedida"
150
- elif any(word in mensagem for word in ["como", "tô", "tá", "bem"]):
151
- intencao = "responder_bem_estar"
152
- # Detecção de sentimento
153
- if any(word in mensagem for word in ["fixe", "legal", "bom", "😊", "", "kkk", "rsrs"]):
154
- sentimento = "positivo"
155
- elif any(word in mensagem for word in ["ruim", "chato", "droga", "😡", "😢"]):
156
- sentimento = "negativo"
157
- # Detecção de ironia (exemplo: tom positivo com conteúdo negativo ou vice-versa)
158
- if ("fixe" in mensagem or "bom" in mensagem) and ("perdi" in mensagem or "droga" in mensagem):
159
- ironia = True
160
- sentimento = "negativo" # Ajustar sentimento para refletir o real
161
- estilo = "informal" if any(g in mensagem for g in ['kkk', 'rsrs', 'puto']) else "normal"
162
- # Analisar emoção baseada no sentimento
163
- self.analisar_emocao(mensagem, sentimento)
164
- contexto_ajustado = f"Mensagem: {mensagem} | Histórico: {historico[-2:] if len(historico) > 1 else historico}"
165
- if ironia:
166
- contexto_ajustado += " | Possível ironia detectada."
167
- if meia_frase:
168
- contexto_ajustado += " | Mensagem parece incompleta (meia frase)."
169
- # Decidir se usar o nome do usuário em respostas (saudações, agradecimentos e despedidas)
170
- # Tornar probabílistico com base na configuração para evitar sempre usar o nome completo
171
- usar_nome = False
172
- prob = getattr(config, 'USAR_NOME_PROBABILIDADE', 0.7)
173
- if intencao in ["saudacao", "despedida"] or any(w in mensagem for w in ["obrigado", "valeu", "thanks"]):
174
- try:
175
- usar_nome = random.random() < float(prob)
176
- except Exception:
177
- usar_nome = random.random() < 0.7
178
-
179
- return {
180
- "texto_normalizado": mensagem,
181
- "intencao": intencao,
182
- "sentimento": sentimento,
183
- "estilo": estilo,
184
- "contexto_ajustado": contexto_ajustado,
185
- "ironia": ironia,
186
- "meia_frase": meia_frase,
187
- "usar_nome": usar_nome
188
- }
189
-
190
- def balancear_contexto(self, mensagem_atual: str, nome_usuario: str, numero_usuario: str, mensagem_original: str, limite_historico: int, limite_contexto: int, is_reply: bool) -> str:
191
- """Balanceia o contexto com base no histórico e mensagem atual."""
192
- historico = self.db.recuperar_mensagens(nome_usuario, limite=limite_historico)
193
- contexto = f"Usuário: {nome_usuario} (ID: {numero_usuario}) | Mensagem atual: {mensagem_atual}"
194
- if is_reply and mensagem_original:
195
- contexto += f" | Resposta a: {mensagem_original}"
196
- if historico:
197
- contexto += f" | Histórico recente: {historico[-limite_contexto:]}"
198
- return contexto
199
-
200
- def selecionar_resposta_predefinida(self, contexto: str) -> str:
201
- """Seleciona uma resposta predefinida com base no contexto."""
202
- contexto_lower = contexto.lower()
203
- # Respostas muito curtas e neutras para saudações/despedidas
204
- if any(w in contexto_lower for w in [" oi", "oi", "olá", "eai", "eae"]):
205
- return "Oi! Tudo fixe?"
206
- elif any(w in contexto_lower for w in ["tchau", "flw", "até"]):
207
- return "Tchau! Fica bem."
208
- return "" # String vazia quando não há resposta predefinida
209
-
210
- # Métodos de integração com banco e aprendizado detalhado
211
- def registrar_aprendizado_detalhado(self, chave, valor):
212
- if not self.usuario:
213
- logger.warning("Usuário não definido para aprendizado detalhado.")
214
- return
215
- self.db.salvar_aprendizado_detalhado(self.usuario, chave, valor)
216
-
217
- def obter_aprendizado_detalhado(self, chave=None):
218
- if not self.usuario:
219
- logger.warning("Usuário não definido para consulta de aprendizado detalhado.")
220
- return None
221
- return self.db.recuperar_aprendizado_detalhado(self.usuario, chave)
222
-
223
- def obter_historico(self, limite=5):
224
- if not self.usuario:
225
- logger.warning("Usuário não definido para histórico.")
226
- return []
227
- result = self.db.recuperar_mensagens(self.usuario, limite=limite)
228
- return result if result else []
229
-
230
- def atualizar_contexto(self, mensagem, resposta):
231
- """Salva a interação no banco de mensagens e aciona aprendizado de termos."""
232
- if not self.usuario:
233
- logger.warning("Usuário não definido para atualizar contexto; salvando como 'anonimo'.")
234
- usuario = 'anonimo'
235
- else:
236
- usuario = self.usuario
237
- try:
238
- self.db.salvar_mensagem(usuario, mensagem, resposta)
239
- # Aprender termos do histórico
240
- historico = self.obter_historico(limite=10) # Últimas 10 mensagens
241
- self.aprender_do_historico(mensagem, resposta, historico)
242
- except Exception as e:
243
- logger.warning(f'Falha ao salvar mensagem no DB: {e}')
244
-
245
- def registrar_aprendizado(self, dado, valor):
246
- if not self.usuario:
247
- logger.warning("Usuário não definido para aprendizado simples.")
248
- return
249
- self.db.salvar_aprendizado(self.usuario, dado, valor)
250
-
251
- def obter_aprendizado(self, dado):
252
- if not self.usuario:
253
- logger.warning("Usuário não definido para consulta de aprendizado simples.")
254
- return None
255
- return self.db.recuperar_aprendizado(self.usuario, dado)
256
-
257
- def aprender_termo_regional(self, termo, contexto, significado):
258
- """Aprende um termo regional/gíria baseado no contexto."""
259
- self.termo_contexto[termo] = {"contexto": contexto, "significado": significado}
260
- self.registrar_aprendizado_detalhado("termos", self.termo_contexto)
261
- logger.info(f"Termo '{termo}' aprendido: {significado} no contexto {contexto}")
262
-
263
- def analisar_emocao(self, mensagem, sentimento):
264
- """Analisa e atualiza a emoção da IA baseada na mensagem e sentimento."""
265
- if sentimento == "positivo":
266
- self.emocao_atual = "feliz"
267
- elif sentimento == "negativo":
268
- self.emocao_atual = "irritada"
269
- else:
270
- self.emocao_atual = "neutra"
271
- return self.emocao_atual
272
-
273
- def ativar_espírito_crítico(self):
274
- """Ativa o espírito crítico para respostas questionadoras."""
275
- self.espírito_crítico = True
276
- return "Espírito crítico ativado para respostas questionadoras."
277
-
278
- def aprender_do_historico(self, mensagem, resposta, historico):
279
- """Aprende termos do histórico de conversas."""
280
- if len(historico) >= 2:
281
- prev_msg = historico[-2][0].lower()
282
- if "como vai" in prev_msg or "tudo bem" in prev_msg:
283
- if "indo" in mensagem.lower():
284
- self.aprender_termo_regional("indo", "bem_estar", "bem")
285
- # Adicionar mais padrões aqui para outras gírias e contextos
286
- # Ex.: if "blz" in mensagem.lower(): self.aprender_termo_regional("blz", "afirmacao", "beleza")
287
-
288
- def substituir_termos_aprendidos(self, mensagem):
289
- """Substitui termos aprendidos na mensagem."""
290
- for termo, info in self.termo_contexto.items():
291
- if termo in mensagem:
292
- mensagem = mensagem.replace(termo, info["significado"])
293
- return mensagem
294
-
295
- def obter_aprendizados(self):
296
- """Retorna os aprendizados do usuário, incluindo termos e emoções."""
297
- aprendizados = {
298
- "termos": self.termo_contexto,
299
- "emocao_atual": self.emocao_atual,
300
- "espírito_crítico": self.espírito_crítico
301
- }
302
- return aprendizados
 
1
+ import logging
2
+ import re
3
+ import random
4
+ import time
5
+ import sqlite3
6
+ from typing import List, Dict, Any, Optional, Tuple
7
+
8
+ try:
9
+ from sentence_transformers import SentenceTransformer # type: ignore
10
+ except Exception as e:
11
+ logging.warning(f"sentence_transformers não disponível: {e}")
12
+ SentenceTransformer = None
13
+
14
+ from modules.database import Database
15
+ import modules.config as config
16
+
17
+ # Logging
18
+ logging.basicConfig(level=logging.INFO, format='%(asctime)s [%(levelname)s] %(message)s')
19
+ logger = logging.getLogger(__name__)
20
+
21
+
22
+ class Contexto:
23
+ """
24
+ Gerencia contexto, histórico, aprendizado dinâmico, gírias, tom, emoção.
25
+ Consulta o banco a cada interação → aprendizado em tempo real.
26
+ """
27
+
28
+ def __init__(self, db: Database, user_id: str):
29
+ self.db = db
30
+ self.user_id = user_id # número ou nome do usuário
31
+ self.historico: List[Tuple[str, str]] = []
32
+ self.girias_aprendidas: List[Dict[str, Any]] = []
33
+ self.ton_predominante: str = "neutro"
34
+ self.emocao_atual: str = "neutra"
35
+ self.termo_contexto: Dict[str, Dict] = {}
36
+ self.model = None
37
+
38
+ # Carrega tudo do banco na primeira vez
39
+ self._atualizar_do_banco()
40
+
41
+ def _atualizar_do_banco(self):
42
+ """Consulta o banco e atualiza tudo (chamado a cada interação)"""
43
+ try:
44
+ # Histórico
45
+ msgs = self.db.recuperar_mensagens(self.user_id, limite=10)
46
+ self.historico = [(m, r) for m, r in (msgs or [])][::-1][-5:] # últimas 5
47
+
48
+ # Gírias aprendidas
49
+ self.girias_aprendidas = self.db.recuperar_girias_usuario(self.user_id)
50
+
51
+ # Tom predominante
52
+ self.ton_predominante = self.db.obter_tom_predominante(self.user_id) or "neutro"
53
+
54
+ # Termos aprendidos (antigo termo_contexto)
55
+ termos_raw = self.db.recuperar_aprendizado_detalhado(self.user_id, "termos")
56
+ if termos_raw and isinstance(termos_raw, str):
57
+ try:
58
+ import json
59
+ self.termo_contexto = json.loads(termos_raw)
60
+ except:
61
+ self.termo_contexto = {}
62
+ else:
63
+ self.termo_contexto = {}
64
+
65
+ # Emoção atual (baseada na última interação)
66
+ ultima = self.historico[-1] if self.historico else ("", "")
67
+ analise = self.db.analisar_emocoes_mensagem(ultima[0] + " " + ultima[1])
68
+ self.emocao_atual = analise["emocao"]
69
+
70
+ except Exception as e:
71
+ logger.error(f"Erro ao atualizar contexto do banco: {e}")
72
+ self.historico = []
73
+ self.girias_aprendidas = []
74
+ self.ton_predominante = "neutro"
75
+ self.termo_contexto = {}
76
+
77
+ def atualizar_aprendizados_do_banco(self):
78
+ """FORÇA ATUALIZAÇÃO DO BANCO A CADA CHAMADA (usado na api.py)"""
79
+ self._atualizar_do_banco()
80
+
81
+ def obter_historico(self) -> List[Tuple[str, str]]:
82
+ return self.historico
83
+
84
+ def atualizar_contexto(self, mensagem: str, resposta: str):
85
+ """Salva no banco + atualiza memória local"""
86
+ try:
87
+ self.db.salvar_mensagem(self.user_id, mensagem, resposta, numero=self.user_id)
88
+ self.historico.append((mensagem, resposta))
89
+ if len(self.historico) > 5:
90
+ self.historico.pop(0)
91
+
92
+ # Atualiza emoção
93
+ analise = self.db.analisar_emocoes_mensagem(mensagem + " " + resposta)
94
+ self.emocao_atual = analise["emocao"]
95
+
96
+ # Extrai gírias e salva no banco
97
+ self._extrair_e_salvar_girias(mensagem, resposta)
98
+
99
+ except Exception as e:
100
+ logger.warning(f"Erro ao atualizar contexto: {e}")
101
+
102
+ def _extrair_e_salvar_girias(self, msg: str, resp: str):
103
+ """Extrai gírias e salva no banco"""
104
+ texto = f"{msg} {resp}".lower()
105
+ palavras = [p for p in re.findall(r'\b\w{4,}\b', texto)
106
+ if p not in {'não', 'que', 'com', 'pra', 'pro', 'uma', 'ele', 'ela'}]
107
+
108
+ contador = collections.Counter(palavras)
109
+ for palavra, freq in contador.most_common(5):
110
+ if freq > 1:
111
+ significado = "gíria local" if any(x in texto for x in ['puto', 'caralho', 'merda']) else "expressão comum"
112
+ self.db.salvar_giria_aprendida(self.user_id, palavra, significado, texto[:100])
113
+
114
+ def analisar_intencao_e_normalizar(self, mensagem: str, historico: List) -> Dict[str, Any]:
115
+ """Normaliza, detecta intenção, sentimento, ironia, meia frase, etc."""
116
+ mensagem = mensagem.strip()
117
+ if not mensagem:
118
+ return {"intencao": "vazia", "sentimento": "neutro", "usar_nome": False}
119
+
120
+ # Normalização
121
+ msg_lower = re.sub(r'[^\w\s\.,!?]', '', mensagem.lower())
122
+ msg_normalizada = self._substituir_termos_aprendidos(msg_lower)
123
+
124
+ # Detecção básica
125
+ saudacao = any(p in msg_lower for p in ["oi", "olá", "eai", "eae", "bom dia"])
126
+ despedida = any(p in msg_lower for p in ["tchau", "flw", "até logo", "bazar"])
127
+ pergunta = "?" in mensagem or any(p in msg_lower for p in ["como", "onde", "quem", "por que"])
128
+ grosseria = any(p in msg_lower for p in ["caralho", "puto", "merda", "fdp", "burro", "idiota"])
129
+
130
+ # Sentimento
131
+ positivo = any(p in msg_lower for p in ["fixe", "bué", "bom", "legal", "gosto", "adoro", "kkk", "rsrs"])
132
+ negativo = any(p in msg_lower for p in ["ruim", "chato", "droga", "ódio", "triste", "puto"])
133
+
134
+ sentimento = "positivo" if positivo else ("negativo" if negativo else "neutro")
135
+
136
+ # Ironia
137
+ ironia = (positivo and "perdi" in msg_lower) or (negativo and "melhor" in msg_lower)
138
+
139
+ # Meia frase
140
+ meia_frase = len(msg_lower.split()) <= 3 or "..." in mensagem
141
+
142
+ # Usar nome?
143
+ usar_nome = False
144
+ if saudacao or despedida or any(p in msg_lower for p in ["obrigado", "valeu"]):
145
+ prob = getattr(config, 'USAR_NOME_PROBABILIDADE', 0.4)
146
+ usar_nome = random.random() < prob
147
+
148
+ # Estilo
149
+ estilo = "curto" if len(mensagem) < 30 else "normal"
150
+ if grosseria:
151
+ estilo = "rude"
152
+
153
+ return {
154
+ "texto_normalizado": msg_normalizada,
155
+ "intencao": "saudacao" if saudacao else ("despedida" if despedida else ("pergunta" if pergunta else "conversa")),
156
+ "sentimento": sentimento,
157
+ "estilo": estilo,
158
+ "ironia": ironia,
159
+ "meia_frase": meia_frase,
160
+ "usar_nome": usar_nome,
161
+ "grosseria": grosseria
162
+ }
163
+
164
+ def _substituir_termos_aprendidos(self, texto: str) -> str:
165
+ """Substitui gírias aprendidas por significado (opcional, para análise)"""
166
+ for termo, info in self.termo_contexto.items():
167
+ if termo in texto:
168
+ texto = texto.replace(termo, info.get("significado", termo))
169
+ return texto
170
+
171
+ def aprender_termo_regional(self, termo: str, contexto: str, significado: str):
172
+ """Registra novo termo no banco e na memória"""
173
+ self.termo_contexto[termo] = {"contexto": contexto, "significado": significado}
174
+ try:
175
+ import json
176
+ self.db.salvar_aprendizado_detalhado(self.user_id, "termos", json.dumps(self.termo_contexto))
177
+ except:
178
+ pass
179
+
180
+ def obter_aprendizados(self) -> Dict[str, Any]:
181
+ """Retorna tudo que a IA aprendeu sobre o usuário"""
182
+ return {
183
+ "historico_recente": [f"U: {u} | A: {a}" for u, a in self.historico[-3:]],
184
+ "girias": [g["giria"] for g in self.girias_aprendidas[:5]],
185
+ "tom_predominante": self.ton_predominante,
186
+ "emocao_atual": self.emocao_atual,
187
+ "termos_aprendidos": list(self.termo_contexto.keys())[:5]
188
+ }