File size: 8,764 Bytes
fe2621e
21fae0a
 
 
b70cf63
21fae0a
fe2621e
21fae0a
fe2621e
f3f8380
6ca7a85
b70cf63
6ca7a85
 
b70cf63
21fae0a
 
 
b70cf63
21fae0a
 
 
 
 
 
6ca7a85
b70cf63
 
 
6ca7a85
 
21fae0a
6ca7a85
 
b70cf63
6ca7a85
 
 
 
 
 
 
 
 
b70cf63
6ca7a85
 
 
 
b70cf63
21fae0a
6ca7a85
 
 
b70cf63
6ca7a85
b70cf63
 
6ca7a85
b70cf63
 
6ca7a85
 
 
 
 
 
 
b70cf63
6ca7a85
b70cf63
 
 
6ca7a85
 
b70cf63
21fae0a
 
b70cf63
21fae0a
b70cf63
 
 
21fae0a
 
 
 
 
 
b70cf63
 
 
 
 
 
 
21fae0a
 
 
 
 
 
6ca7a85
b70cf63
fe2621e
b70cf63
6ca7a85
 
 
 
 
 
 
 
 
 
 
 
 
f3f8380
6ca7a85
 
fe2621e
6ca7a85
b70cf63
fe2621e
b70cf63
6ca7a85
 
 
 
 
 
 
 
 
 
 
 
fe2621e
6ca7a85
 
 
 
b70cf63
6ca7a85
b70cf63
6ca7a85
 
 
 
 
 
 
 
 
 
 
 
 
 
 
b70cf63
f3f8380
b70cf63
6ca7a85
 
b70cf63
 
6ca7a85
 
 
 
 
 
 
 
 
 
 
 
 
 
 
b70cf63
 
 
 
6ca7a85
 
 
 
 
 
 
 
 
 
 
b70cf63
 
6ca7a85
b70cf63
6ca7a85
 
 
 
 
 
 
b70cf63
f3f8380
6ca7a85
 
b70cf63
6ca7a85
 
 
b70cf63
 
6ca7a85
b70cf63
 
21fae0a
6ca7a85
 
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
"""
WebSearch — Módulo para busca de notícias (WebScraping) e pesquisa geral (API Placeholder).

- Angola News: Fontes fixas (Angop, Novo Jornal, Jornal de Angola, etc.)
- Busca Geral: Placeholder para integração de API externa (ex: Google Search API, Serper API)
- Cache: 15 minutos (900 segundos)
"""

import time
import re
import requests
from typing import List, Dict, Any
from loguru import logger
from bs4 import BeautifulSoup
import os

# Importa o config para possível uso futuro de chaves de API
try:
    # Assumindo que o config está em modules/config.py
    import modules.config as config
except ImportError:
    # Fallback se config.py não estiver disponível
    class ConfigMock:
        pass
    config = ConfigMock()

# Configuração do logger para este módulo
logger.add("web_search.log", rotation="10 MB", level="INFO")


class SimpleCache:
    """Cache simples em memória com Time-To-Live (TTL)."""
    def __init__(self, ttl: int = 900):  # 15 min
        self.ttl = ttl
        self._data: Dict[str, Any] = {}

    def get(self, key: str):
        if key in self._data:
            value, timestamp = self._data[key]
            if time.time() - timestamp < self.ttl:
                return value
            del self._data[key]
        return None

    def set(self, key: str, value: Any):
        self._data[key] = (value, time.time())


class WebSearch:
    """Gerenciador de buscas para notícias de Angola e pesquisa geral."""
    
    def __init__(self):
        self.cache = SimpleCache(ttl=900)
        self.session = requests.Session()
        # Header para simular um navegador real e evitar bloqueios de scraping
        self.session.headers.update({
            "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.0.0 Safari/537.36",
            "Accept-Language": "pt-BR,pt;q=0.9,en-US;q=0.8,en;q=0.7"
        })
        # Fontes de notícias de Angola (Web Scraping)
        self.fontes_angola = [
            "https://www.angop.ao/ultimas",
            "https://www.novojornal.co.ao/",
            "https://www.jornaldeangola.ao/",
            "https://www.verangola.net/va/noticias"
        ]

    def _limpar_texto(self, texto: str) -> str:
        """Limpa e formata o texto para o LLM."""
        if not texto: return ""
        # Remove espaços múltiplos, quebras de linha e caracteres de formatação
        texto = re.sub(r'[\s\n\t]+', ' ', texto)
        # Limita o tamanho para o contexto do LLM
        return texto.strip()[:200]

    # --- FUNÇÃO PRINCIPAL DE BUSCA GERAL (PLACEHOLDER) ---
    def buscar_geral(self, query: str) -> str:
        """
        Retorna resultados de pesquisa na web para cultura geral.
        
        ATENÇÃO: Esta função é um PLACEHOLDER. Para funcionar, você DEVE
        integrar uma API de busca externa paga (ex: Serper, Google Search API,
        ou outra) para substituir o bloco de fallback.
        """
        cache_key = f"busca_geral_{query.lower()}"
        cached = self.cache.get(cache_key)
        if cached:
            return cached
        
        logger.warning(f"PLACEHOLDER: Executando busca geral para '{query}'. É necessária integração de API externa.")
        
        # O BLOCO ABAIXO DEVE SER SUBSTITUÍDO PELA CHAMADA REAL DA API DE BUSCA
        
        # --- COMEÇO DO PLACEHOLDER ---
        fallback_response = "Sem informações de cultura geral disponíveis. Para ativar a pesquisa em tempo real, configure e integre uma API de busca (como Serper ou Google Search API) na função 'buscar_geral' do web_search.py."
        # --- FIM DO PLACEHOLDER ---
        
        self.cache.set(cache_key, fallback_response)
        return fallback_response

    # --- IMPLEMENTAÇÃO DE BUSCA DE NOTÍCIAS DE ANGOLA (WEB SCRAPING) ---

    def _buscar_angop(self) -> List[Dict]:
        """Extrai notícias da Angop."""
        try:
            r = self.session.get(self.fontes_angola[0], timeout=8)
            if r.status_code != 200: return []
            soup = BeautifulSoup(r.text, 'html.parser')
            itens = soup.select('.ultimas-noticias .item')[:3]
            noticias = []
            for item in itens:
                titulo = item.select_one('h3 a')
                link = item.select_one('a')
                if titulo and link:
                    noticias.append({
                        "titulo": self._limpar_texto(titulo.get_text()),
                        "link": "https://www.angop.ao" + link.get('href', '') if link.get('href', '').startswith('/') else link.get('href', '')
                    })
            return noticias
        except Exception as e:
            logger.warning(f"Angop falhou: {e}")
            return []

    def _buscar_novojornal(self) -> List[Dict]:
        """Extrai notícias do Novo Jornal."""
        try:
            r = self.session.get(self.fontes_angola[1], timeout=8)
            if r.status_code != 200: return []
            soup = BeautifulSoup(r.text, 'html.parser')
            itens = soup.select('.noticia-lista .titulo')[:3]
            noticias = []
            for item in itens:
                a = item.find('a')
                if a:
                    noticias.append({
                        "titulo": self._limpar_texto(a.get_text()),
                        "link": a.get('href', '')
                    })
            return noticias
        except Exception as e:
            logger.warning(f"Novo Jornal falhou: {e}")
            return []

    def _buscar_jornaldeangola(self) -> List[Dict]:
        """Extrai notícias do Jornal de Angola."""
        try:
            r = self.session.get(self.fontes_angola[2], timeout=8)
            if r.status_code != 200: return []
            soup = BeautifulSoup(r.text, 'html.parser')
            itens = soup.select('.ultimas .titulo a')[:3]
            noticias = []
            for a in itens:
                noticias.append({
                    "titulo": self._limpar_texto(a.get_text()),
                    "link": a.get('href', '')
                })
            return noticias
        except Exception as e:
            logger.warning(f"Jornal de Angola falhou: {e}")
            return []

    def _buscar_verangola(self) -> List[Dict]:
        """Extrai notícias do VerAngola."""
        try:
            r = self.session.get(self.fontes_angola[3], timeout=8)
            if r.status_code != 200: return []
            soup = BeautifulSoup(r.text, 'html.parser')
            # Seletores podem mudar, mas .noticia-item geralmente é um bom ponto de partida
            itens = soup.select('.noticia-item')[:3] 
            noticias = []
            for item in itens:
                titulo = item.select_one('h3 a')
                if titulo:
                    link = titulo.get('href', '')
                    noticias.append({
                        "titulo": self._limpar_texto(titulo.get_text()),
                        "link": link if link.startswith('http') else "https://www.verangola.net" + link
                    })
            return noticias
        except Exception as e:
            logger.warning(f"VerAngola falhou: {e}")
            return []

    def pesquisar_noticias_angola(self) -> str:
        """
        Retorna as notícias mais recentes de Angola através de Web Scraping.
        Esta é a função usada no api.py quando detecta intenção de notícias.
        """
        cache_key = "noticias_angola"
        cached = self.cache.get(cache_key)
        if cached:
            return cached

        todas = []
        try:
            todas.extend(self._buscar_angop())
            todas.extend(self._buscar_novojornal())
            todas.extend(self._buscar_jornaldeangola())
            todas.extend(self._buscar_verangola())
        except Exception as e:
            logger.error(f"Erro no pipeline de scraping: {e}")

        # Filtra e remove duplicatas
        vistos = set()
        unicas = []
        for n in todas:
            t = n["titulo"].lower()
            if t not in vistos and len(t) > 20:
                vistos.add(t)
                unicas.append(n)
                if len(unicas) >= 5:
                    break

        if not unicas:
            fallback = "Sem notícias recentes de Angola disponíveis no momento."
            self.cache.set(cache_key, fallback)
            return fallback

        # Formata a resposta para injeção no prompt do LLM
        texto = "NOTÍCIAS RECENTES DE ANGOLA (CONTEXTO):\n"
        for i, n in enumerate(unicas, 1):
            # Apenas o título é relevante para o contexto do LLM
            texto += f"[{i}] {n['titulo']}\n" 
            
        self.cache.set(cache_key, texto.strip())
        return texto.strip()