akra35567 commited on
Commit
6ca7a85
·
1 Parent(s): f3f8380

Update modules/web_search.py

Browse files
Files changed (1) hide show
  1. modules/web_search.py +158 -223
modules/web_search.py CHANGED
@@ -1,236 +1,171 @@
1
- # modules/treinamento.py
2
  """
3
- Sistema de treinamento avançado para Akira IA.
4
- - Implementação de um sistema de treinamento periódico e aprendizado em tempo real.
5
- - Usa um modelo NLP pesado para gerar embeddings.
 
 
6
  """
7
- import threading
8
  import time
9
- import logging
10
- import sqlite3
11
  import re
12
- import json
13
- import collections
14
- from typing import Optional, Any, List, Dict, Tuple
15
- from dataclasses import dataclass
16
- # Importa o módulo de configuração (necessário para constantes como DB_PATH)
17
- import modules.config as config
18
- from .database import Database # Importação adicionada para type hints (necessário para o método __init__)
19
-
20
- logger = logging.getLogger(__name__)
21
-
22
- # MODELO PESADO (Solicitado): paraphrase-multilingual-mpnet-base-v2
23
- try:
24
- from sentence_transformers import SentenceTransformer
25
- MODEL_NAME = "sentence-transformers/paraphrase-multilingual-mpnet-base-v2"
26
- logger.info(f"Modelo NLP carregado: {MODEL_NAME}")
27
- except Exception as e:
28
- logger.warning(f"sentence_transformers não disponível: {e}")
29
- SentenceTransformer = None
30
- MODEL_NAME = None
31
-
32
- # Listas angolanas (Conforme fornecido pelo usuário)
33
- PALAVRAS_POSITIVAS = ['bom', 'ótimo', 'incrível', 'feliz', 'alegre', 'fixe', 'bué', 'top', 'show', 'adoro', 'rsrs', 'kkk']
34
- PALAVRAS_NEGATIVAS = ['ruim', 'péssimo', 'triste', 'ódio', 'puto', 'merda', 'caralho', 'chateado']
35
- GIRIAS_ANGOLANAS = ['mano', 'puto', 'cota', 'mwangolé', 'kota', 'oroh', 'bué', 'fixe', 'baza', 'kuduro']
36
- PALAVRAS_RUDES = ['caralho', 'puto', 'merda', 'fdp', 'vsf', 'burro', 'idiota', 'parvo']
37
-
38
- @dataclass
39
- class Interacao:
40
- usuario: str
41
- mensagem: str
42
- resposta: str
43
- numero: str
44
- is_reply: bool = False
45
- mensagem_original: str = ""
46
-
47
- class Treinamento:
48
- """
49
- Treinamento contínuo da Akira:
50
- - Registra interações
51
- - Analisa tom, emoção, gírias
52
- - Heurístico para LLMs (sem fine-tuning pesado)
53
- """
54
- def __init__(self, db: Database, contexto: Optional[Any] = None, interval_hours: int = 1):
55
- self.db = db
56
- # O contexto é opcional, mas se existir, deve ser uma instância de Contexto
57
- self.contexto = contexto
58
- self.interval_hours = interval_hours
59
- self._thread = None
60
- self._running = False
61
- self._model = None
62
- # Usuários privilegiados
63
- self.privileged_users = ['244937035662', 'isaac', 'isaac quarenta']
64
-
65
- def _ensure_nlp_model(self):
66
- """Carrega o modelo NLP pesado se ainda não estiver carregado."""
67
- if self._model is not None:
68
- return
69
- if SentenceTransformer is None:
70
- return
71
  try:
72
- logger.info(f"Carregando modelo NLP pesado: {MODEL_NAME}...")
73
- # Usa o MODEL_NAME definido globalmente
74
- self._model = SentenceTransformer(MODEL_NAME)
75
- logger.info(f"Modelo NLP carregado com sucesso: {MODEL_NAME}")
 
 
 
 
 
 
 
 
 
 
76
  except Exception as e:
77
- logger.error(f"Falha ao carregar modelo NLP ({MODEL_NAME}): {e}")
78
- logger.warning("Verifique se seu ambiente tem o HF_TOKEN correto e acesso à internet.")
79
- self._model = None
80
 
81
- def registrar_interacao(self, usuario: str, mensagem: str, resposta: str, numero: str = '', is_reply: bool = False, mensagem_original: str = ''):
82
- """Salva + aprende na hora"""
83
  try:
84
- # Assumindo que db.salvar_mensagem agora aceita todos os 6 parâmetros
85
- self.db.salvar_mensagem(usuario, mensagem, resposta, numero, is_reply, mensagem_original)
86
- self._aprender_em_tempo_real(numero, mensagem, resposta)
87
- logger.info(f"Interação registrada e aprendida em tempo real: {numero}")
 
 
 
 
 
 
 
 
 
88
  except Exception as e:
89
- logger.warning(f'Erro ao registrar interação: {e}')
90
-
91
- def _aprender_em_tempo_real(self, numero: str, msg: str, resp: str):
92
- """Executa análise heurística e NLP em tempo real."""
93
- if not numero or numero == 'unknown':
94
- return
95
- texto = f"{msg} {resp}".lower()
96
-
97
- # === ANÁLISE NLP (se disponível) ===
98
- self._ensure_nlp_model()
99
- if self._model:
100
- try:
101
- # O numpy.tobytes() garante que o embedding é salvo como BLOB (bytes)
102
- emb = self._model.encode(texto).tobytes()
103
- # Uso do novo método db.salvar_embedding (assumindo que existe na Database)
104
- # O embedding é salvo com o texto (key) para referência futura
105
- self.db.salvar_embedding(texto, emb)
106
- except Exception as e:
107
- logger.warning(f"Erro ao gerar/salvar embedding: {e}")
108
- pass
109
-
110
- # === ANÁLISE HEURÍSTICA ===
111
- rude = any(p in texto for p in PALAVRAS_RUDES)
112
- tom = 'rude' if rude else 'casual'
113
-
114
- # Filtra palavras comuns e conta a frequência para top_girias
115
- palavras = [p for p in re.findall(r'\b\w{4,}\b', texto)
116
- if p not in {'não', 'que', 'com', 'pra', 'uma', 'ele', 'ela', 'por', 'pra', 'tudo', 'bem', 'como'}]
117
- contador = collections.Counter(palavras)
118
- # Seleciona palavras com frequência > 1, excluindo as gírias conhecidas
119
- top_girias = [w for w, c in contador.most_common(5) if c > 1 and w not in GIRIAS_ANGOLANAS]
120
-
121
- # Salvar tom
122
- intensidade = 0.9 if rude else 0.6
123
- # Uso do novo método db.registrar_tom_usuario (assumindo que existe)
124
- self.db.registrar_tom_usuario(numero, tom, intensidade, texto[:100])
125
-
126
- # Salvar gírias
127
- for giria in top_girias:
128
- significado = "gíria rude" if rude else "gíria local"
129
- # Uso do novo método db.salvar_giria_aprendida (assumindo que existe)
130
- self.db.salvar_giria_aprendida(numero, giria, significado, texto[:100])
131
-
132
- # Emoção: Puxa do contexto
133
- if self.contexto and hasattr(self.contexto, 'analisar_emocoes_mensagem'):
134
- # O Contexto.analisar_emocoes_mensagem retorna str ('feliz', 'triste', etc.)
135
- emocao_str = self.contexto.analisar_emocoes_mensagem(msg)
136
- # Adapta para o formato JSON esperado por salvar_aprendizado_detalhado
137
- analise = {emocao_str: 1.0, "texto_original": msg}
138
- self.db.salvar_aprendizado_detalhado(numero, "emocao_recente", json.dumps(analise))
139
- else:
140
- logger.debug(f"Contexto não disponível para análise de emoção em tempo real para {numero}.")
141
-
142
-
143
- # ================================================================
144
- # HEURÍSTICO PARA MISTRAL (TREINAMENTO PERIÓDICO)
145
- # ================================================================
146
- def _prepare_prompt_for_mistral(self, interacoes: List[Interacao]) -> str:
147
- """Prepara prompt para LLM baseado em interações (exemplo de adaptação)."""
148
- examples = []
149
- for i in interacoes:
150
- # Adaptando para um formato mais 'chat'
151
- prompt = f"U: {i.mensagem}\nA: {i.resposta}"
152
- examples.append(prompt)
153
- return "\n".join(examples)
154
-
155
- def train_once(self):
156
- """Treinamento heurístico periódico: analisa usuários e salva preferências."""
157
- logger.info("Treinamento heurístico iniciado...")
158
- self._analisar_usuarios()
159
- self._salvar_ultimo_treino()
160
- logger.info("Treinamento concluído.")
161
-
162
- def _analisar_usuarios(self):
163
- """Analisa o histórico dos usuários para determinar tom e gírias."""
164
- usuarios = set()
165
- # Recupera números distintos de usuários que interagiram
166
- rows = self.db._execute_with_retry("SELECT DISTINCT numero FROM mensagens WHERE numero IS NOT NULL AND numero != ''")
167
- if rows:
168
- for r in rows:
169
- usuarios.add(r[0])
170
-
171
- for num in usuarios:
172
- # Recupera as 20 últimas mensagens (entrada e resposta)
173
- msgs = self.db.recuperar_mensagens(num, limite=20)
174
- if len(msgs) < 3: continue
175
-
176
- tom = self._detectar_tom(msgs, num)
177
-
178
- # Uso do novo método db.salvar_preferencia_tom (assumindo que existe)
179
- self.db.salvar_preferencia_tom(num, tom)
180
-
181
- def _detectar_tom(self, mensagens: List[Tuple], numero: str) -> str:
182
- """Detecta o tom predominante do usuário."""
183
- if numero in self.privileged_users:
184
- return 'formal'
185
-
186
- counter = collections.Counter()
187
- for msg, _ in mensagens: # (mensagem_usuario, resposta_ia)
188
- msg_l = (msg or '').lower()
189
- if any(p in msg_l for p in PALAVRAS_RUDES):
190
- counter['rude'] += 1
191
- elif any(p in msg_l for p in ['por favor', 'obrigado', 'excelência']):
192
- counter['formal'] += 1
193
- elif any(p in msg_l for p in GIRIAS_ANGOLANAS):
194
- counter['casual'] += 1
195
- else:
196
- counter['neutro'] += 1
197
- return counter.most_common(1)[0][0] if counter else 'neutro'
198
-
199
- def _salvar_ultimo_treino(self):
200
- """Salva o timestamp do último treinamento."""
201
  try:
202
- self.db.salvar_info_geral('ultimo_treino', str(time.time()))
203
- except Exception:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
204
  pass
205
 
206
- # ================================================================
207
- # LOOP DE TREINAMENTO (Mantido)
208
- # ================================================================
209
- def _run_loop(self):
210
- """Loop principal do treinamento periódico."""
211
- interval = max(1, self.interval_hours) * 3600
212
- logger.info(f"Treinamento heurístico a cada {self.interval_hours}h")
213
- while self._running:
214
- try:
215
- self.train_once()
216
- except Exception as e:
217
- logger.exception(f"Erro no treinamento: {e}")
218
- for _ in range(int(interval)):
219
- if not self._running:
220
  break
221
- time.sleep(1)
222
- logger.info("Treinamento parado.")
223
-
224
- def start_periodic_training(self):
225
- """Inicia o treinamento periódico em uma thread separada."""
226
- if self._running: return
227
- self._running = True
228
- self._thread = threading.Thread(target=self._run_loop, daemon=True)
229
- self._thread.start()
230
-
231
- def stop(self):
232
- """Para o treinamento periódico."""
233
- self._running = False
234
- if self._thread:
235
- # Tenta juntar a thread por 5 segundos
236
- self._thread.join(timeout=5)
 
 
1
  """
2
+ WebSearch Busca notícias recentes de Angola
3
+ - Fontes: Angop, Novo Jornal, Jornal de Angola, etc.
4
+ - Cache: 15 minutos
5
+ - Fallback: "Sem notícias recentes."
6
+ - 100% seguro (requests + try/except)
7
  """
 
8
  import time
 
 
9
  import re
10
+ import requests
11
+ from typing import List, Dict
12
+ from loguru import logger
13
+ from bs4 import BeautifulSoup
14
+ import modules.config as config
15
+
16
+
17
+ class SimpleCache:
18
+ def __init__(self, ttl: int = 900): # 15 min
19
+ self.ttl = ttl
20
+ self._data = {}
21
+
22
+ def get(self, key: str):
23
+ if key in self._data:
24
+ value, timestamp = self._data[key]
25
+ if time.time() - timestamp < self.ttl:
26
+ return value
27
+ del self._data[key]
28
+ return None
29
+
30
+ def set(self, key: str, value):
31
+ self._data[key] = (value, time.time())
32
+
33
+
34
+ class WebSearch:
35
+ def __init__(self):
36
+ self.cache = SimpleCache(ttl=900)
37
+ self.session = requests.Session()
38
+ self.session.headers.update({
39
+ "User-Agent": "Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36"
40
+ })
41
+ self.fontes = [
42
+ "https://www.angop.ao/ultimas",
43
+ "https://www.novojornal.co.ao/",
44
+ "https://www.jornaldeangola.ao/",
45
+ "https://www.verangola.net/va/noticias"
46
+ ]
47
+
48
+ def _limpar_texto(self, texto: str) -> str:
49
+ if not texto: return ""
50
+ texto = re.sub(r'\s+', ' ', texto)
51
+ texto = re.sub(r'[^a-zA-ZÀ-ÿ0-9\s\.,!?]', '', texto)
52
+ return texto.strip()[:200]
53
+
54
+ def _buscar_angop(self) -> List[Dict]:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
55
  try:
56
+ r = self.session.get(self.fontes[0], timeout=8)
57
+ if r.status_code != 200: return []
58
+ soup = BeautifulSoup(r.text, 'html.parser')
59
+ itens = soup.select('.ultimas-noticias .item')[:3]
60
+ noticias = []
61
+ for item in itens:
62
+ titulo = item.select_one('h3 a')
63
+ link = item.select_one('a')
64
+ if titulo and link:
65
+ noticias.append({
66
+ "titulo": self._limpar_texto(titulo.get_text()),
67
+ "link": "https://www.angop.ao" + link.get('href', '') if link.get('href', '').startswith('/') else link.get('href', '')
68
+ })
69
+ return noticias
70
  except Exception as e:
71
+ logger.warning(f"Angop falhou: {e}")
72
+ return []
 
73
 
74
+ def _buscar_novojornal(self) -> List[Dict]:
 
75
  try:
76
+ r = self.session.get(self.fontes[1], timeout=8)
77
+ if r.status_code != 200: return []
78
+ soup = BeautifulSoup(r.text, 'html.parser')
79
+ itens = soup.select('.noticia-lista .titulo')[:3]
80
+ noticias = []
81
+ for item in itens:
82
+ a = item.find('a')
83
+ if a:
84
+ noticias.append({
85
+ "titulo": self._limpar_texto(a.get_text()),
86
+ "link": a.get('href', '')
87
+ })
88
+ return noticias
89
  except Exception as e:
90
+ logger.warning(f"Novo Jornal falhou: {e}")
91
+ return []
92
+
93
+ def _buscar_jornaldeangola(self) -> List[Dict]:
94
+ try:
95
+ r = self.session.get(self.fontes[2], timeout=8)
96
+ if r.status_code != 200: return []
97
+ soup = BeautifulSoup(r.text, 'html.parser')
98
+ itens = soup.select('.ultimas .titulo a')[:3]
99
+ noticias = []
100
+ for a in itens:
101
+ noticias.append({
102
+ "titulo": self._limpar_texto(a.get_text()),
103
+ "link": a.get('href', '')
104
+ })
105
+ return noticias
106
+ except Exception as e:
107
+ logger.warning(f"Jornal de Angola falhou: {e}")
108
+ return []
109
+
110
+ def _buscar_verangola(self) -> List[Dict]:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
111
  try:
112
+ r = self.session.get(self.fontes[3], timeout=8)
113
+ if r.status_code != 200: return []
114
+ soup = BeautifulSoup(r.text, 'html.parser')
115
+ itens = soup.select('.noticia-item')[:3]
116
+ noticias = []
117
+ for item in itens:
118
+ titulo = item.select_one('h3 a')
119
+ if titulo:
120
+ link = titulo.get('href', '')
121
+ noticias.append({
122
+ "titulo": self._limpar_texto(titulo.get_text()),
123
+ "link": link if link.startswith('http') else "https://www.verangola.net" + link
124
+ })
125
+ return noticias
126
+ except Exception as e:
127
+ logger.warning(f"VerAngola falhou: {e}")
128
+ return []
129
+
130
+ def pesquisar_noticias_angola(self) -> str:
131
+ cache_key = "noticias_angola"
132
+ cached = self.cache.get(cache_key)
133
+ if cached:
134
+ return cached
135
+
136
+ todas = []
137
+ try:
138
+ todas.extend(self._buscar_angop())
139
+ todas.extend(self._buscar_novojornal())
140
+ todas.extend(self._buscar_jornaldeangola())
141
+ todas.extend(self._buscar_verangola())
142
+ except:
143
  pass
144
 
145
+ if not todas:
146
+ fallback = "Sem notícias recentes."
147
+ self.cache.set(cache_key, fallback)
148
+ return fallback
149
+
150
+ # Remove duplicatas por título
151
+ vistos = set()
152
+ unicas = []
153
+ for n in todas:
154
+ t = n["titulo"].lower()
155
+ if t not in vistos and len(t) > 20:
156
+ vistos.add(t)
157
+ unicas.append(n)
158
+ if len(unicas) >= 3:
159
  break
160
+
161
+ if not unicas:
162
+ fallback = "Sem notícias relevantes."
163
+ self.cache.set(cache_key, fallback)
164
+ return fallback
165
+
166
+ texto = "NOTÍCIAS RECENTES:\n"
167
+ for i, n in enumerate(unicas, 1):
168
+ texto += f"{i}. {n['titulo']}\n"
169
+
170
+ self.cache.set(cache_key, texto.strip())
171
+ return texto.strip()