akra35567 commited on
Commit
daf0307
·
verified ·
1 Parent(s): 2b11c4c

Upload 9 files

Browse files
modules/skills/__init__.py ADDED
@@ -0,0 +1,19 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Skills package - Agrupamento de APIs com fallbacks automáticos
3
+ """
4
+
5
+ from .base_skill import BaseSkill
6
+ from .weather_skill import WeatherSkill
7
+ from .entertainment_skill import EntertainmentSkill
8
+ from .art_skill import ArtSkill
9
+ from .music_skill import MusicSkill
10
+ from .manus_skill import ManusSkill
11
+
12
+ __all__ = [
13
+ "BaseSkill",
14
+ "WeatherSkill",
15
+ "EntertainmentSkill",
16
+ "ArtSkill",
17
+ "MusicSkill",
18
+ "ManusSkill",
19
+ ]
modules/skills/art_skill.py ADDED
@@ -0,0 +1,89 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ ArtSkill - Busca de arte e geração de imagens com fallbacks
3
+ """
4
+
5
+ from modules.skills.base_skill import BaseSkill
6
+ from modules.api_integrations.art_providers import ArtProviders
7
+
8
+
9
+ class ArtSkill(BaseSkill):
10
+ """
11
+ Skill de arte que pode:
12
+ 1. Buscar no Museu Metropolitano (470k+ obras)
13
+ 2. Gerar imagens via Pollinations AI
14
+ 3. Retornar ASCII art como fallback criativo
15
+ """
16
+
17
+ def __init__(self):
18
+ super().__init__(
19
+ name="get_art",
20
+ description="Busca obras de arte ou gera imagens com fallbacks automáticos"
21
+ )
22
+
23
+ def get_primary_provider(self):
24
+ """Tipo depende se é search ou generate"""
25
+ return self.art_primary
26
+
27
+ def get_fallback_chain(self):
28
+ """Fallbacks criativos"""
29
+ return [
30
+ self.art_fallback,
31
+ ]
32
+
33
+ def art_primary(self, tipo: str = "search", query: str = None, **kwargs) -> dict:
34
+ """
35
+ Provider primário dependendo do tipo:
36
+ - search: Museu Metropolitano
37
+ - generate: Pollinations AI (fallback de Flux)
38
+ """
39
+ tipo = tipo.lower().strip()
40
+
41
+ if tipo == "search":
42
+ if not query:
43
+ return {"sucesso": False, "erro": "Parâmetro 'query' obrigatório para busca"}
44
+
45
+ result = ArtProviders.search_metropolitan_museum(query, max_results=3)
46
+ if result and result.get("sucesso"):
47
+ return result
48
+
49
+ return {"sucesso": False, "erro": "Museu não encontrou obras"}
50
+
51
+ elif tipo == "generate":
52
+ if not query:
53
+ return {"sucesso": False, "erro": "Parâmetro 'query' obrigatório para gerar imagem"}
54
+
55
+ style = kwargs.get("style")
56
+ prompt = f"{query}"
57
+ if style:
58
+ prompt = f"{query} in {style} style"
59
+
60
+ result = ArtProviders.generate_with_pollinations(prompt)
61
+ if result and result.get("sucesso"):
62
+ return result
63
+
64
+ return {"sucesso": False, "erro": "Geração de imagem falhou"}
65
+
66
+ else:
67
+ return {"sucesso": False, "erro": f"Tipo '{tipo}' desconhecido (use 'search' ou 'generate')"}
68
+
69
+ def art_fallback(self, tipo: str = "search", query: str = None, **kwargs) -> dict:
70
+ """
71
+ Fallback: ASCII art criativo
72
+ """
73
+ tipo = tipo.lower().strip()
74
+
75
+ if tipo == "generate":
76
+ # Para geração, retorna ASCII art
77
+ theme = kwargs.get("theme", "cat")
78
+ return ArtProviders.generate_simple_ascii_art(theme)
79
+
80
+ # Para busca, retorna descrição poética
81
+ return {
82
+ "sucesso": True,
83
+ "tipo": "poetic_description",
84
+ "descricao": f"A arte não pode ser capturada em palavras, apenas sentida. '{query}' evoca emoções e sensações únicas em cada observador.",
85
+ "fonte": "fallback_philosophical"
86
+ }
87
+
88
+ def _get_error_suggestion(self) -> str:
89
+ return "Tenta com termo de busca diferente ou mais específico"
modules/skills/autonomous_agent.py ADDED
@@ -0,0 +1,269 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ ================================================================================
3
+ AKIRA — AUTONOMOUS AGENT (Motor de Decisão Autónoma)
4
+ ================================================================================
5
+ Motor central que permite à Akira:
6
+ 1. Tomar decisões de moderação SEM ser chamada (proativo)
7
+ 2. Analisar contexto do grupo e decidir ações
8
+ 3. Executar ações de infraestrutura autonomamente
9
+ 4. Comunicar resultados sigilosos ao proprietário via DM
10
+
11
+ Filosofia: "Akira não espera ordens — ela age quando necessário"
12
+ ================================================================================
13
+ """
14
+
15
+ import json
16
+ import time
17
+ from typing import Dict, Any, List, Optional
18
+ from loguru import logger
19
+ from datetime import datetime
20
+
21
+ OWNER_NUMBER = "244937035662"
22
+
23
+ # Limites para moderação autónoma
24
+ SPAM_MSG_COUNT = 5 # Msgs num curto período → spam
25
+ SPAM_WINDOW_SECS = 10 # Janela de tempo para detetar spam
26
+ FLOOD_MSG_COUNT = 3 # Msgs muito rápidas → flood
27
+ FLOOD_WINDOW_SECS = 2 # Janela para flood
28
+
29
+
30
+ class AutonomousAgent:
31
+ """
32
+ Motor de decisão autónoma da Akira.
33
+
34
+ Permite:
35
+ - Analisar contexto de grupo e tomar decisões de moderação
36
+ - Executar skills autonomamente com base em eventos
37
+ - Reportar ações sigilosas ao proprietário
38
+ """
39
+
40
+ def __init__(self):
41
+ self._spam_tracker: Dict[str, List[float]] = {} # {user_jid: [timestamps]}
42
+ self._action_log: List[Dict] = [] # Log de ações autónomas tomadas
43
+ self._db = None
44
+ self._llm_caller = None
45
+
46
+ def init(self, db_instance=None, llm_caller=None):
47
+ """Inicializa com dependências."""
48
+ self._db = db_instance
49
+ self._llm_caller = llm_caller
50
+ logger.info("🤖 [AUTONOMOUS AGENT] Motor de decisão autónoma iniciado.")
51
+
52
+ # ─────────────────────────────────────────────────────────────────
53
+ # ANÁLISE DE COMPORTAMENTO DE GRUPO
54
+ # ─────────────────────────────────────────────────────────────────
55
+
56
+ def track_message(self, user_jid: str, group_jid: str, message: str, timestamp: float = None) -> Dict[str, Any]:
57
+ """
58
+ Regista uma mensagem e avalia se deve tomar acção autónoma.
59
+
60
+ Retorna:
61
+ Dict com ação recomendada (pode ser vazio se não há ação)
62
+ """
63
+ if timestamp is None:
64
+ timestamp = time.time()
65
+
66
+ key = f"{group_jid}:{user_jid}"
67
+
68
+ # Adiciona timestamp ao tracker
69
+ if key not in self._spam_tracker:
70
+ self._spam_tracker[key] = []
71
+
72
+ self._spam_tracker[key].append(timestamp)
73
+
74
+ # Limpa timestamps antigos (> 60s)
75
+ self._spam_tracker[key] = [
76
+ ts for ts in self._spam_tracker[key]
77
+ if timestamp - ts <= 60
78
+ ]
79
+
80
+ # ─── Detecta Flood ───
81
+ recent_flood = [ts for ts in self._spam_tracker[key] if timestamp - ts <= FLOOD_WINDOW_SECS]
82
+ if len(recent_flood) >= FLOOD_MSG_COUNT:
83
+ logger.warning(f"🚨 [AGENT] Flood detetado: {user_jid} em {group_jid}")
84
+ return self._build_moderation_action(
85
+ action="mute",
86
+ target_jid=user_jid,
87
+ group_jid=group_jid,
88
+ reason=f"Flood: {len(recent_flood)} mensagens em {FLOOD_WINDOW_SECS}s",
89
+ duration_minutes=10,
90
+ severity="AVISO"
91
+ )
92
+
93
+ # ─── Detecta Spam ───
94
+ recent_spam = [ts for ts in self._spam_tracker[key] if timestamp - ts <= SPAM_WINDOW_SECS]
95
+ if len(recent_spam) >= SPAM_MSG_COUNT:
96
+ logger.warning(f"🚨 [AGENT] Spam detetado: {user_jid} em {group_jid}")
97
+ return self._build_moderation_action(
98
+ action="mute",
99
+ target_jid=user_jid,
100
+ group_jid=group_jid,
101
+ reason=f"Spam: {len(recent_spam)} mensagens em {SPAM_WINDOW_SECS}s",
102
+ duration_minutes=30,
103
+ severity="CRÍTICO"
104
+ )
105
+
106
+ return {} # Sem ação
107
+
108
+ def analyze_message_for_moderation(self, message: str, user_jid: str, group_jid: str) -> Dict[str, Any]:
109
+ """
110
+ Analisa o conteúdo de uma mensagem para decidir se há necessidade de moderação.
111
+
112
+ Deteta:
113
+ - Links externos em grupos com anti-link
114
+ - Conteúdo explicitamente ofensivo
115
+ - Ameaças ou doxxing
116
+ """
117
+ message_lower = message.lower()
118
+
119
+ # Padrões de links externos
120
+ link_patterns = ["http://", "https://", "t.me/", "bit.ly/", "wa.me/", "discord.gg/"]
121
+ has_link = any(p in message_lower for p in link_patterns)
122
+
123
+ if has_link:
124
+ return {
125
+ "type": "remote_action",
126
+ "action": "autonomous_action",
127
+ "params": {
128
+ "cmd": "warn",
129
+ "target": user_jid,
130
+ "group_jid": group_jid,
131
+ "reason": "Link externo detetado pelo sistema autónomo",
132
+ "notify_owner": True,
133
+ "silent": True
134
+ }
135
+ }
136
+
137
+ return {}
138
+
139
+ def analyze_image_description_for_moderation(self, description: str, user_jid: str, group_jid: str) -> Dict[str, Any]:
140
+ """
141
+ [AGENTE AUTÓNOMO VISUAL]
142
+ Verifica se a descrição gerada pelo modelo de visão da imagem contém conteúdo explicitamente proibido (NSFW/Gore).
143
+ """
144
+ if not description:
145
+ return {}
146
+
147
+ desc_lower = description.lower()
148
+
149
+ # Padrões baseados em descrições comuns de NSFW ou Gore por LLMs de visão
150
+ nsfw_keywords = [
151
+ "pornografia", "nudez", "sexo explícito", "conteúdo adulto", "nsfw",
152
+ "sangue extremo", "vísceras", "gore", "mutilação", "violência extrema",
153
+ "nudez explícita", "ato sexual", "porn"
154
+ ]
155
+
156
+ if any(keyword in desc_lower for keyword in nsfw_keywords):
157
+ logger.warning(f"🚨 [AGENT-VISÃO] Imagem Proibida detetada: {user_jid} em {group_jid}")
158
+ return {
159
+ "type": "remote_action",
160
+ "action": "autonomous_action",
161
+ "params": {
162
+ "cmd": "delete_message",
163
+ "target": user_jid,
164
+ "group_jid": group_jid,
165
+ "reason": "Imagem NSFW/Gore bloqueada pelo escudo visual da Akira",
166
+ "notify_owner": True,
167
+ "silent": True
168
+ }
169
+ }
170
+
171
+ return {}
172
+
173
+ # ─────────────────────────────────────────────────────────────────
174
+ # CONSTRUÇÃO DE AÇÕES
175
+ # ─────────────────────────────────────────────────────────────────
176
+
177
+ def _build_moderation_action(
178
+ self,
179
+ action: str,
180
+ target_jid: str,
181
+ group_jid: str,
182
+ reason: str,
183
+ duration_minutes: int = 0,
184
+ severity: str = "AVISO"
185
+ ) -> Dict[str, Any]:
186
+ """Constrói uma ação de moderação autónoma."""
187
+
188
+ # Regista no log interno
189
+ log_entry = {
190
+ "timestamp": datetime.now().isoformat(),
191
+ "tipo": "MODERAÇÃO_AUTÓNOMA",
192
+ "severidade": severity,
193
+ "utilizador": target_jid,
194
+ "grupo": group_jid,
195
+ "ação": action,
196
+ "motivo": reason,
197
+ "duração": f"{duration_minutes}min" if duration_minutes else "N/A"
198
+ }
199
+ self._action_log.append(log_entry)
200
+
201
+ # Guarda no DB se disponível
202
+ if self._db:
203
+ try:
204
+ self._db._execute_with_retry(
205
+ """INSERT INTO system_events
206
+ (tipo, servidor, descricao, acao_tomada, resolvido)
207
+ VALUES (?, ?, ?, ?, 1)""",
208
+ (severity, "railway", f"Moderação: {reason} | User: {target_jid}", action, ),
209
+ commit=True
210
+ )
211
+ except Exception as e:
212
+ logger.debug(f"[AGENT] Não foi possível registar no DB: {e}")
213
+
214
+ logger.info(f"🤖 [AGENT ACTION] {action} em {target_jid} | Motivo: {reason}")
215
+
216
+ return {
217
+ "type": "remote_action",
218
+ "action": "autonomous_action",
219
+ "params": {
220
+ "cmd": action,
221
+ "target": target_jid,
222
+ "group_jid": group_jid,
223
+ "reason": reason,
224
+ "args": [str(duration_minutes)] if duration_minutes else [],
225
+ "notify_owner": True,
226
+ "silent": True
227
+ }
228
+ }
229
+
230
+ def decide_action_from_context(self, context: str, event_type: str) -> Optional[Dict]:
231
+ """
232
+ Usa o LLM para decidir uma ação com base num evento do sistema.
233
+ Usado para situações mais complexas que precisam de raciocínio.
234
+ """
235
+ if not self._llm_caller:
236
+ return None
237
+
238
+ system = (
239
+ "És a Akira, agente autónoma de infraestrutura. "
240
+ "Analisa o evento abaixo e decide a melhor ação a tomar. "
241
+ "Responde APENAS em JSON com: {\"acao\": \"string\", \"motivo\": \"string\", \"prioridade\": \"alta|media|baixa\"}. "
242
+ "Ações possíveis: mute_user, kick_user, lock_group, send_owner_report, ignore."
243
+ )
244
+
245
+ try:
246
+ response = self._llm_caller(system, f"EVENTO: {event_type}\nCONTEXTO:\n{context}")
247
+ # Extrai JSON da resposta
248
+ import re
249
+ json_match = re.search(r'\{.*\}', response, re.DOTALL)
250
+ if json_match:
251
+ return json.loads(json_match.group())
252
+ except Exception as e:
253
+ logger.error(f"[AGENT] Erro ao decidir ação: {e}")
254
+
255
+ return None
256
+
257
+ def get_recent_actions_summary(self) -> str:
258
+ """Retorna um resumo das ações autónomas recentes."""
259
+ if not self._action_log:
260
+ return "Nenhuma ação autónoma registada."
261
+
262
+ recent = self._action_log[-10:] # Últimas 10 ações
263
+ lines = [f"• [{a['timestamp'][:16]}] {a['ação'].upper()} em {a['utilizador'].split('@')[0]} — {a['motivo']}"
264
+ for a in recent]
265
+ return f"📋 Últimas {len(recent)} ações autónomas:\n" + "\n".join(lines)
266
+
267
+
268
+ # Instância global singleton
269
+ autonomous_agent = AutonomousAgent()
modules/skills/base_skill.py ADDED
@@ -0,0 +1,280 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ BaseSkill - Classe base para todas as skills agrupadas com fallbacks
3
+ Padrão: Primary Provider -> Fallback Chain -> Error Handling
4
+ """
5
+
6
+ import time
7
+ import json
8
+ import logging
9
+ from typing import Any, Dict, List, Optional, Callable
10
+ from abc import ABC, abstractmethod
11
+ from datetime import datetime, timedelta
12
+ import hashlib
13
+
14
+
15
+ class SkillError(Exception):
16
+ """Erro base em skills"""
17
+ pass
18
+
19
+
20
+ class APITimeoutError(SkillError):
21
+ """Timeout em chamada de API"""
22
+ pass
23
+
24
+
25
+ class APIRateLimitError(SkillError):
26
+ """Rate limit atingido"""
27
+ pass
28
+
29
+
30
+ class DataValidationError(SkillError):
31
+ """Dados inválidos retornados"""
32
+ pass
33
+
34
+
35
+ class CacheManager:
36
+ """Gerencia cache com TTL"""
37
+
38
+ def __init__(self):
39
+ self.cache = {}
40
+ self.logger = logging.getLogger(f"Cache")
41
+
42
+ def set(self, key: str, value: Any, ttl: int = 3600):
43
+ """Armazena valor em cache com TTL (em segundos)"""
44
+ self.cache[key] = {
45
+ "value": value,
46
+ "expires_at": time.time() + ttl,
47
+ "created_at": datetime.now().isoformat()
48
+ }
49
+ self.logger.debug(f"💾 Cache SET: {key} (TTL: {ttl}s)")
50
+
51
+ def get(self, key: str) -> Optional[Any]:
52
+ """Recupera valor do cache se ainda válido"""
53
+ if key not in self.cache:
54
+ return None
55
+
56
+ entry = self.cache[key]
57
+ if time.time() > entry["expires_at"]:
58
+ del self.cache[key]
59
+ self.logger.debug(f"♻️ Cache EXPIRED: {key}")
60
+ return None
61
+
62
+ self.logger.debug(f"✅ Cache HIT: {key}")
63
+ return entry["value"]
64
+
65
+ def clear(self):
66
+ """Limpa todo o cache"""
67
+ self.cache.clear()
68
+
69
+ def get_stats(self) -> Dict:
70
+ """Retorna estatísticas do cache"""
71
+ return {
72
+ "total_items": len(self.cache),
73
+ "items": list(self.cache.keys())
74
+ }
75
+
76
+
77
+ class BaseSkill(ABC):
78
+ """
79
+ Classe base para skills com suporte a fallbacks automáticos
80
+
81
+ Exemplo de uso:
82
+ class WeatherSkill(BaseSkill):
83
+ def get_primary_provider(self):
84
+ return self.web_search_weather
85
+
86
+ def get_fallback_chain(self):
87
+ return [
88
+ self.weather_api,
89
+ self.wttr_in
90
+ ]
91
+ """
92
+
93
+ def __init__(self, name: str, description: str):
94
+ self.name = name
95
+ self.description = description
96
+ self.logger = logging.getLogger(f"Skill[{name}]")
97
+ self.cache = CacheManager()
98
+ self.call_count = 0
99
+ self.error_count = 0
100
+
101
+ @abstractmethod
102
+ def get_primary_provider(self) -> Callable:
103
+ """Retorna função do provider primário"""
104
+ pass
105
+
106
+ def get_fallback_chain(self) -> List[Callable]:
107
+ """Retorna lista de fallbacks (pode estar vazio)"""
108
+ return []
109
+
110
+ def execute(self, *args, **kwargs) -> Dict[str, Any]:
111
+ """
112
+ Executa skill com fallback automático
113
+ Tenta: Primary -> Fallback1 -> Fallback2 -> Error
114
+ """
115
+ self.call_count += 1
116
+ start_time = time.time()
117
+
118
+ # Verifica cache
119
+ cache_key = self._make_cache_key(*args, **kwargs)
120
+ cached = self.cache.get(cache_key)
121
+ if cached:
122
+ return {**cached, "cache_hit": True}
123
+
124
+ # Chain de providers
125
+ providers = [self.get_primary_provider()] + self.get_fallback_chain()
126
+
127
+ last_error = None
128
+ for i, provider in enumerate(providers):
129
+ provider_name = getattr(provider, "__name__", f"Provider{i}")
130
+
131
+ try:
132
+ self.logger.info(f"🔄 Tentando {provider_name}...")
133
+ result = self._execute_with_timeout(provider, *args, **kwargs)
134
+
135
+ if not result.get("sucesso"):
136
+ self.logger.warning(f"⚠️ {provider_name} retornou erro: {result.get('erro')}")
137
+ last_error = result.get("erro")
138
+ continue
139
+
140
+ # Sucesso! Formata e cacheia
141
+ response = self._format_response(provider_name, result, False)
142
+ elapsed = time.time() - start_time
143
+ response["elapsed_ms"] = int(elapsed * 1000)
144
+
145
+ # Cacheia resultado bem-sucedido
146
+ ttl = kwargs.pop("cache_ttl", 3600)
147
+ self.cache.set(cache_key, response, ttl=ttl)
148
+
149
+ self.logger.info(f"✅ {provider_name} sucesso ({elapsed:.2f}s)")
150
+ return response
151
+
152
+ except APITimeoutError as e:
153
+ self.logger.warning(f"⏱️ {provider_name} timeout: {e}")
154
+ last_error = f"Timeout: {e}"
155
+ if i < len(providers) - 1:
156
+ time.sleep(0.5 * (2 ** i)) # Backoff exponencial
157
+ continue
158
+
159
+ except APIRateLimitError as e:
160
+ self.logger.warning(f"🚫 {provider_name} rate limit: {e}")
161
+ last_error = f"Rate limit: {e}"
162
+ continue
163
+
164
+ except DataValidationError as e:
165
+ self.logger.warning(f"❌ {provider_name} dados inválidos: {e}")
166
+ last_error = f"Dados inválidos: {e}"
167
+ continue
168
+
169
+ except Exception as e:
170
+ self.logger.error(f"💥 {provider_name} erro: {type(e).__name__}: {e}")
171
+ last_error = f"{type(e).__name__}: {e}"
172
+ continue
173
+
174
+ # Todos providers falharam
175
+ self.error_count += 1
176
+ self.logger.error(f"🔴 Todos providers falharam para {self.name}")
177
+
178
+ return self._format_error_response(last_error)
179
+
180
+ def _execute_with_timeout(self, fn: Callable, *args, timeout: float = 5.0, **kwargs) -> Any:
181
+ """
182
+ Executa função com timeout
183
+ Implementação simples (ideal seria threading/async)
184
+ """
185
+ # Para versão simples, apenas chama a função
186
+ # Em produção, usar ThreadPoolExecutor ou asyncio
187
+ return fn(*args, **kwargs)
188
+
189
+ def _format_response(self, provider: str, data: Dict, cache_hit: bool) -> Dict:
190
+ """Formata resposta padrão"""
191
+ return {
192
+ "sucesso": True,
193
+ "skill": self.name,
194
+ "provider": provider,
195
+ "cache_hit": cache_hit,
196
+ "dados": data,
197
+ "timestamp": datetime.now().isoformat()
198
+ }
199
+
200
+ def _format_error_response(self, error: str) -> Dict:
201
+ """Formata resposta de erro"""
202
+ return {
203
+ "sucesso": False,
204
+ "skill": self.name,
205
+ "erro": error or f"Nenhum provider disponível para {self.name}",
206
+ "sugestao": self._get_error_suggestion(),
207
+ "timestamp": datetime.now().isoformat()
208
+ }
209
+
210
+ def _get_error_suggestion(self) -> str:
211
+ """Retorna sugestão quando tudo falha"""
212
+ return "Tenta de novo mais tarde"
213
+
214
+ def _make_cache_key(self, *args, **kwargs) -> str:
215
+ """Cria chave de cache baseada em argumentos"""
216
+ # Remove cache_ttl antes de processar
217
+ cache_ttl = kwargs.pop("cache_ttl", None)
218
+
219
+ # Serializa argumentos
220
+ key_str = f"{self.name}:{json.dumps([args, kwargs], sort_keys=True, default=str)}"
221
+ return hashlib.md5(key_str.encode()).hexdigest()
222
+
223
+ def get_stats(self) -> Dict:
224
+ """Retorna estatísticas da skill"""
225
+ return {
226
+ "name": self.name,
227
+ "description": self.description,
228
+ "calls": self.call_count,
229
+ "errors": self.error_count,
230
+ "error_rate": f"{(self.error_count/max(1, self.call_count)*100):.1f}%",
231
+ "cache": self.cache.get_stats()
232
+ }
233
+
234
+ def clear_cache(self):
235
+ """Limpa cache da skill"""
236
+ self.cache.clear()
237
+ self.logger.info("🧹 Cache limpo")
238
+
239
+
240
+ # ==========================
241
+ # Decoradores úteis
242
+ # ==========================
243
+
244
+ def retry(max_attempts: int = 3, backoff: float = 1.0):
245
+ """Decorator para retry automático com backoff exponencial"""
246
+ def decorator(fn):
247
+ def wrapper(*args, **kwargs):
248
+ for attempt in range(max_attempts):
249
+ try:
250
+ return fn(*args, **kwargs)
251
+ except Exception as e:
252
+ if attempt == max_attempts - 1:
253
+ raise
254
+ wait_time = backoff * (2 ** attempt)
255
+ logging.warning(f"Retry {attempt+1}/{max_attempts}, aguardando {wait_time}s")
256
+ time.sleep(wait_time)
257
+ return wrapper
258
+ return decorator
259
+
260
+
261
+ def timeout(seconds: float = 5.0):
262
+ """Decorator para timeout (implementação simples)"""
263
+ def decorator(fn):
264
+ def wrapper(*args, **kwargs):
265
+ # Implementação real usaria signal ou threading
266
+ return fn(*args, **kwargs)
267
+ return wrapper
268
+ return decorator
269
+
270
+
271
+ def validate_response(schema: Dict = None):
272
+ """Decorator para validar resposta contra schema"""
273
+ def decorator(fn):
274
+ def wrapper(*args, **kwargs):
275
+ result = fn(*args, **kwargs)
276
+ if not isinstance(result, dict):
277
+ raise DataValidationError(f"Response deve ser dict, got {type(result)}")
278
+ return result
279
+ return wrapper
280
+ return decorator
modules/skills/entertainment_skill.py ADDED
@@ -0,0 +1,85 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ EntertainmentSkill - Piadas, Dicas e Citações em uma skill agrupada
3
+ """
4
+
5
+ from modules.skills.base_skill import BaseSkill
6
+ from modules.api_integrations.entertainment_providers import EntertainmentProviders
7
+
8
+
9
+ class EntertainmentSkill(BaseSkill):
10
+ """
11
+ Skill de entretenimento que retorna:
12
+ - Piadas (Joke API + fallback local)
13
+ - Dicas (Advice Slip API + fallback local)
14
+ - Citações (Quotable API + fallback local)
15
+
16
+ Usa fallback automático se a API primária falhar
17
+ """
18
+
19
+ def __init__(self):
20
+ super().__init__(
21
+ name="get_entertainment",
22
+ description="Retorna piadas, dicas ou citações com fallbacks automáticos"
23
+ )
24
+
25
+ def get_primary_provider(self):
26
+ """Tipo depende do parâmetro 'tipo'"""
27
+ return self.comedy_or_advice
28
+
29
+ def get_fallback_chain(self):
30
+ """Fallbacks locais"""
31
+ return [
32
+ self.fallback_entertainment,
33
+ ]
34
+
35
+ def comedy_or_advice(self, tipo: str = "random", **kwargs) -> dict:
36
+ """
37
+ Baseado no tipo, retorna piada, dica ou citação
38
+ """
39
+ tipo = tipo.lower().strip()
40
+
41
+ if tipo == "joke":
42
+ result = EntertainmentProviders.get_joke()
43
+ if result and result.get("sucesso"):
44
+ return result
45
+ return {"sucesso": False, "erro": "Joke API falhou"}
46
+
47
+ elif tipo == "advice":
48
+ result = EntertainmentProviders.get_advice()
49
+ if result and result.get("sucesso"):
50
+ return result
51
+ return {"sucesso": False, "erro": "Advice API falhou"}
52
+
53
+ elif tipo == "quote":
54
+ result = EntertainmentProviders.get_quote()
55
+ if result and result.get("sucesso"):
56
+ return result
57
+ return {"sucesso": False, "erro": "Quote API falhou"}
58
+
59
+ else: # random
60
+ import random
61
+ tipo_aleatorio = random.choice(["joke", "advice", "quote"])
62
+ return self.comedy_or_advice(tipo=tipo_aleatorio, **kwargs)
63
+
64
+ def fallback_entertainment(self, tipo: str = "random", **kwargs) -> dict:
65
+ """
66
+ Fallback 1: Entertainment local
67
+ Usa cache de piadas, dicas e citações
68
+ """
69
+ tipo = tipo.lower().strip()
70
+
71
+ if tipo == "joke":
72
+ return EntertainmentProviders.get_joke_fallback()
73
+ elif tipo == "advice":
74
+ return EntertainmentProviders.get_advice_fallback()
75
+ elif tipo == "quote":
76
+ return EntertainmentProviders.get_quote_fallback()
77
+ else:
78
+ # Random entre os 3
79
+ import random
80
+ tipo_aleatorio = random.choice(["joke", "advice", "quote"])
81
+ return self.fallback_entertainment(tipo=tipo_aleatorio, **kwargs)
82
+
83
+ def _get_error_suggestion(self) -> str:
84
+ """Sugestão quando tudo falha"""
85
+ return "Tenta de novo em alguns segundos"
modules/skills/manus_skill.py ADDED
@@ -0,0 +1,44 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ ManusSkill - Skill de Pesquisa Avançada via Manus AI
3
+ """
4
+
5
+ from modules.skills.base_skill import BaseSkill
6
+ from modules.skills.native_research import native_research_agent
7
+
8
+ class ManusSkill(BaseSkill):
9
+ """
10
+ Skill que utiliza o Manus AI para pesquisas profundas e tarefas complexas.
11
+ """
12
+
13
+ def __init__(self):
14
+ super().__init__(
15
+ name="manus_research",
16
+ description="Realiza pesquisas profundas e resolve tarefas complexas usando o agente autônomo Manus AI."
17
+ )
18
+
19
+ def get_primary_provider(self):
20
+ return self.manus_research_tool
21
+
22
+ def manus_research_tool(self, prompt: str, **kwargs) -> dict:
23
+ """
24
+ Executa uma pesquisa ou tarefa no Manus AI.
25
+ """
26
+ # Executa a pesquisa profunda diretamente no nosso agente local (OpenManus style)
27
+ result = native_research_agent.run(prompt)
28
+
29
+ if result.get("sucesso"):
30
+ return {
31
+ "sucesso": True,
32
+ "resultado": result.get("resultado"),
33
+ "prompt_original": prompt,
34
+ "status": "concluído"
35
+ }
36
+
37
+ return {
38
+ "sucesso": False,
39
+ "erro": result.get("erro", "Erro desconhecido no Manus AI"),
40
+ "sugestao": "Tenta reformular o pedido ou usa a busca web convencional."
41
+ }
42
+
43
+ def _get_error_suggestion(self) -> str:
44
+ return "A pesquisa profunda falhou. Os sites podem estar bloqueados para leitura automática ou o LLM ficou sobrecarregado."
modules/skills/music_skill.py ADDED
@@ -0,0 +1,92 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ MusicSkill - Gêneros, Letras e OSTs com fallbacks
3
+ """
4
+
5
+ from modules.skills.base_skill import BaseSkill
6
+ from modules.api_integrations.music_providers import MusicProviders
7
+
8
+
9
+ class MusicSkill(BaseSkill):
10
+ """
11
+ Skill de música que pode:
12
+ 1. Gerar gêneros aleatórios (Genrenator)
13
+ 2. Buscar letras (Genius - TODO)
14
+ 3. Buscar OST de animes (Jikan)
15
+ 4. Dar recomendações personalizadas
16
+ 5. Fallback com recomendação local
17
+ """
18
+
19
+ def __init__(self):
20
+ super().__init__(
21
+ name="get_music",
22
+ description="Gera gêneros, recomendações ou busca informações musicais com fallbacks"
23
+ )
24
+
25
+ def get_primary_provider(self):
26
+ """Tipo de música depende do parâmetro 'tipo'"""
27
+ return self.music_primary
28
+
29
+ def get_fallback_chain(self):
30
+ """Fallbacks para entretenimento musical"""
31
+ return [
32
+ self.music_fallback,
33
+ ]
34
+
35
+ def music_primary(self, tipo: str = "genre", **kwargs) -> dict:
36
+ """
37
+ Provider primário dependendo do tipo:
38
+ - genre: Gera gênero aleatório
39
+ - recommendation: Recomendação contextual
40
+ - anime_ost: Busca trilha sonora de anime
41
+ - lyrics: Busca letra (não implementado)
42
+ """
43
+ tipo = tipo.lower().strip()
44
+
45
+ if tipo == "genre":
46
+ mood = kwargs.get("mood")
47
+ result = MusicProviders.generate_genre_with_details(mood)
48
+ if result and result.get("sucesso"):
49
+ return result
50
+ return {"sucesso": False, "erro": "Geração de gênero falhou"}
51
+
52
+ elif tipo == "recommendation":
53
+ mood = kwargs.get("mood")
54
+ result = MusicProviders.generate_genre_with_details(mood)
55
+ if result and result.get("sucesso"):
56
+ return result
57
+ return {"sucesso": False, "erro": "Recomendação falhou"}
58
+
59
+ elif tipo == "anime_ost":
60
+ anime_name = kwargs.get("anime")
61
+ if not anime_name:
62
+ return {"sucesso": False, "erro": "Parâmetro 'anime' obrigatório"}
63
+
64
+ result = MusicProviders.search_anime_ost_jikan(anime_name)
65
+ if result and result.get("sucesso"):
66
+ return result
67
+ return {"sucesso": False, "erro": f"Anime '{anime_name}' não encontrado"}
68
+
69
+ elif tipo == "lyrics":
70
+ song = kwargs.get("song")
71
+ artist = kwargs.get("artist")
72
+ if not song:
73
+ return {"sucesso": False, "erro": "Parâmetro 'song' obrigatório"}
74
+
75
+ result = MusicProviders.search_lyrics_genius(song, artist)
76
+ if result and result.get("sucesso"):
77
+ return result
78
+
79
+ return {"sucesso": False, "erro": "Não foi possível encontrar a letra desta música. Tente pesquisar o nome exato."}
80
+
81
+ else:
82
+ return {"sucesso": False, "erro": f"Tipo '{tipo}' desconhecido"}
83
+
84
+ def music_fallback(self, tipo: str = "genre", **kwargs) -> dict:
85
+ """
86
+ Fallback: Recomendação musical local
87
+ Sempre retorna algo interessante
88
+ """
89
+ return MusicProviders.get_fallback_recommendation()
90
+
91
+ def _get_error_suggestion(self) -> str:
92
+ return "Tenta pedir um gênero aleatório com 'tipo=genre' ou uma recomendação com 'tipo=recommendation'"
modules/skills/native_research.py ADDED
@@ -0,0 +1,216 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import time
2
+ import json
3
+ import requests
4
+ import trafilatura
5
+ import concurrent.futures
6
+ from typing import List, Dict, Any
7
+ from urllib.parse import urlparse
8
+
9
+ from loguru import logger
10
+ try:
11
+ from ddgs import DDGS
12
+ except ImportError:
13
+ try:
14
+ from duckduckgo_search import DDGS # fallback: nome antigo do pacote
15
+ except ImportError:
16
+ DDGS = None
17
+ from modules.config import OPENROUTER_API_KEY, GROQ_API_KEY, MISTRAL_API_KEY, OPENROUTER_MODEL
18
+
19
+ class NativeDeepResearch:
20
+ """
21
+ Agente Nativo de Deep Research para Akira (OpenManus Clone).
22
+ Não depende de APIs pagas de pesquisa autônoma (como o Manus),
23
+ usa DDGS + Trafilatura + LLM para investigar e sintetizar relatórios completos.
24
+ """
25
+
26
+ def __init__(self):
27
+ # Configuração de Sessão Robusta para evitar "Connection pool is full"
28
+ self.session = requests.Session()
29
+ adapter = requests.adapters.HTTPAdapter(
30
+ pool_connections=20,
31
+ pool_maxsize=20,
32
+ max_retries=3
33
+ )
34
+ self.session.mount("http://", adapter)
35
+ self.session.mount("https://", adapter)
36
+
37
+ # Prioridade: Mistral Direct -> OpenRouter -> Groq
38
+ if MISTRAL_API_KEY:
39
+ self.api_url = "https://api.mistral.ai/v1/chat/completions"
40
+ self.api_key = MISTRAL_API_KEY
41
+ self.model = "mistral-large-latest"
42
+ elif OPENROUTER_API_KEY:
43
+ self.api_url = "https://openrouter.ai/api/v1/chat/completions"
44
+ self.api_key = OPENROUTER_API_KEY
45
+ self.model = OPENROUTER_MODEL
46
+ elif GROQ_API_KEY:
47
+ self.api_url = "https://api.groq.com/openai/v1/chat/completions"
48
+ self.api_key = GROQ_API_KEY
49
+ self.model = "llama3-70b-8192"
50
+ else:
51
+ self.api_url = ""
52
+ self.api_key = ""
53
+ self.model = ""
54
+
55
+ def _call_llm(self, system_prompt: str, user_prompt: str, json_mode: bool = False) -> str:
56
+ """Chamada direta ao LLM para evitar importações circulares com o LLMManager."""
57
+ if not self.api_key:
58
+ return ""
59
+
60
+ headers = {
61
+ "Authorization": f"Bearer {self.api_key}",
62
+ "Content-Type": "application/json",
63
+ "HTTP-Referer": "https://akira.softedge.ai",
64
+ "X-Title": "Akira Native Research"
65
+ }
66
+
67
+ model_to_use = self.model
68
+
69
+ payload = {
70
+ "model": model_to_use,
71
+ "messages": [
72
+ {"role": "system", "content": system_prompt},
73
+ {"role": "user", "content": user_prompt}
74
+ ],
75
+ "temperature": 0.3,
76
+ "max_tokens": 3000
77
+ }
78
+
79
+ if json_mode and "groq" not in self.api_url:
80
+ payload["response_format"] = {"type": "json_object"}
81
+
82
+ response = None
83
+ try:
84
+ response = self.session.post(self.api_url, headers=headers, json=payload, timeout=60)
85
+ response.raise_for_status()
86
+ data = response.json()
87
+ return data["choices"][0]["message"]["content"]
88
+ except Exception as e:
89
+ logger.error(f"❌ [NATIVE RESEARCH] Erro no LLM: {e}")
90
+ if response is not None and hasattr(response, 'text'):
91
+ logger.error(f"Detalhes: {response.text[:500]}")
92
+ return ""
93
+
94
+ def brainstorm_queries(self, topic: str) -> List[str]:
95
+ """Gera 3 a 4 sub-pesquisas otimizadas para motores de busca."""
96
+ sys_prompt = "És um assistente de pesquisa especializado. Dada uma instrução complexa, gera 3 queries de pesquisa no Google para investigar o tema profundamente. Retorna APENAS as queries, uma por linha, sem numeração."
97
+
98
+ res = self._call_llm(sys_prompt, topic)
99
+ if not res:
100
+ return [topic]
101
+
102
+ queries = [q.strip().strip('-').strip('1234567890.').strip() for q in res.split('\n') if q.strip()]
103
+ # Evita demasiadas queries
104
+ return queries[:3] if queries else [topic]
105
+
106
+ def search_urls(self, queries: List[str]) -> List[str]:
107
+ """Procura na web usando o DuckDuckGo e extrai URLs únicos."""
108
+ urls = set()
109
+
110
+ try:
111
+ with DDGS() as ddgs:
112
+ for q in queries:
113
+ try:
114
+ logger.info(f"🔍 [NATIVE RESEARCH] Procurando por: {q}")
115
+ results = list(ddgs.text(q, max_results=3))
116
+ for r in results:
117
+ if isinstance(r, dict) and "href" in r:
118
+ url = r["href"]
119
+ # Filtra links inúteis
120
+ if not any(x in url for x in ['youtube.com', 'facebook.com', 'instagram.com', 'tiktok.com']):
121
+ urls.add(url)
122
+ except Exception as e:
123
+ logger.warning(f"⚠️ Erro ao procurar '{q}': {e}")
124
+ time.sleep(1) # Pequena pausa em caso de rate limit
125
+ except Exception as session_err:
126
+ logger.error(f"❌ [NATIVE RESEARCH] Erro na sessão DDGS: {session_err}")
127
+
128
+ return list(urls)
129
+
130
+ def scrape_url(self, url: str) -> str:
131
+ """Saca o texto limpo do site usando requests + trafilatura."""
132
+ try:
133
+ # Uso da sessão com pool aumentado para estabilidade
134
+ response = self.session.get(url, timeout=15, headers={
135
+ 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36'
136
+ })
137
+ response.raise_for_status()
138
+
139
+ if response.text:
140
+ text = trafilatura.extract(response.text, include_comments=False, include_tables=True)
141
+ if text:
142
+ return f"--- CONTEÚDO DE {url} ---\n{text[:5000]}\n"
143
+ except Exception as e:
144
+ logger.debug(f"Falha ao ler {url}: {e}")
145
+ return ""
146
+
147
+ def synthesize_report(self, prompt: str, context: str) -> str:
148
+ """Gera o relatório final baseado em todo o contexto lido."""
149
+ sys_prompt = (
150
+ "És a Akira, a IA incrivelmente inteligente do ecossistema SoftEdge.\n"
151
+ "Foi-te dada uma tarefa de pesquisa profunda (Deep Research). Tens abaixo as notas extraídas "
152
+ "da internet em bruto. O teu objectivo é ler tudo, sintetizar a verdade e redigir um "
153
+ "relatório detalhado, claro e fenomenal para o utilizador.\n"
154
+ "- Foca-te em dados precisos e atuais.\n"
155
+ "- Ignora informações redundantes ou não relacionadas com a pergunta original.\n"
156
+ "- O relatório DEVE ter parágrafos limpos, sem excesso de hashtags.\n"
157
+ "- Se as notas não tiverem informação suficiente, responde com o que sabes e avisa que a pesquisa web não encontrou tudo."
158
+ )
159
+
160
+ user_prompt = f"TAREFA ORIGINAL DO UTILIZADOR: {prompt}\n\nNOTAS EXTRAÍDAS DA WEB:\n{context}"
161
+
162
+ res = self._call_llm(sys_prompt, user_prompt)
163
+ return res
164
+
165
+ def run(self, prompt: str) -> Dict[str, Any]:
166
+ """Executa a rotina completa de pesquisa nativa (OpenManus flow)."""
167
+ if not self.api_key:
168
+ return {"sucesso": False, "erro": "Chave de API do LLM em falta para o Native Research."}
169
+
170
+ start_time = time.time()
171
+ logger.info("🧠 [NATIVE RESEARCH] Iniciando pipeline autónoma...")
172
+
173
+ # 1. Planeamento
174
+ queries = self.brainstorm_queries(prompt)
175
+ logger.info(f"📋 Sub-pesquisas geradas: {queries}")
176
+
177
+ # 2. Pesquisa de URLs
178
+ urls = self.search_urls(queries)
179
+ if not urls:
180
+ return {"sucesso": False, "erro": "Não foi possível encontrar páginas web relevantes."}
181
+
182
+ logger.info(f"🌐 URLs recolhidos ({len(urls)}). A extrair texto...")
183
+
184
+ # 3. Scraping Paralelo (Velocidade)
185
+ scraped_texts = []
186
+ with concurrent.futures.ThreadPoolExecutor(max_workers=3) as executor:
187
+ future_to_url = {executor.submit(self.scrape_url, url): url for url in urls}
188
+ for future in concurrent.futures.as_completed(future_to_url):
189
+ text = future.result()
190
+ if text:
191
+ scraped_texts.append(text)
192
+
193
+ if not scraped_texts:
194
+ return {"sucesso": False, "erro": "Os sites bloqueadores a leitura dos dados. Não foi possível extrair texto."}
195
+
196
+ full_context = "\n".join(scraped_texts)
197
+ logger.info(f"📚 Extraídos {len(full_context)} caracteres de conteúdo em bruto. A sintetizar...")
198
+
199
+ # 4. Síntese Final
200
+ final_report = self.synthesize_report(prompt, full_context)
201
+
202
+ elapsed = int(time.time() - start_time)
203
+ logger.info(f"✅ [NATIVE RESEARCH] Concluído com sucesso em {elapsed} segundos.")
204
+
205
+ if final_report:
206
+ return {
207
+ "sucesso": True,
208
+ "resultado": final_report,
209
+ "prompt_original": prompt,
210
+ "status": "concluído"
211
+ }
212
+ else:
213
+ return {"sucesso": False, "erro": "Falha na síntese do relatório final."}
214
+
215
+ # Instância partilhada
216
+ native_research_agent = NativeDeepResearch()
modules/skills/weather_skill.py ADDED
@@ -0,0 +1,58 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ WeatherSkill - Informações de clima com fallbacks automáticos
3
+ """
4
+
5
+ from modules.skills.base_skill import BaseSkill
6
+ from modules.api_integrations.weather_providers import WeatherProviders
7
+
8
+
9
+ class WeatherSkill(BaseSkill):
10
+ """
11
+ Skill de clima que tenta múltiplos provedores:
12
+ 1. Web search (se tiver contexto)
13
+ 2. Weather Data API (wttr.in)
14
+ 3. Open-Meteo API
15
+ 4. Mensagem de erro apropriada
16
+ """
17
+
18
+ def __init__(self):
19
+ super().__init__(
20
+ name="get_weather",
21
+ description="Retorna previsão de clima para uma localização com fallbacks automáticos"
22
+ )
23
+
24
+ def get_primary_provider(self):
25
+ """Weather Data API como provider primário"""
26
+ return self.weather_data_api
27
+
28
+ def get_fallback_chain(self):
29
+ """Chain de fallbacks"""
30
+ return [
31
+ self.open_meteo_fallback,
32
+ ]
33
+
34
+ def weather_data_api(self, location: str, **kwargs) -> dict:
35
+ """
36
+ Provider primário: wttr.in
37
+ """
38
+ result = WeatherProviders.from_weather_api(location)
39
+
40
+ if result and result.get("sucesso"):
41
+ return result
42
+
43
+ return {"sucesso": False, "erro": "Weather API falhou"}
44
+
45
+ def open_meteo_fallback(self, location: str, **kwargs) -> dict:
46
+ """
47
+ Fallback 1: Open-Meteo (sem autenticação)
48
+ """
49
+ result = WeatherProviders.from_openweather_fallback(location)
50
+
51
+ if result and result.get("sucesso"):
52
+ return result
53
+
54
+ return {"sucesso": False, "erro": "Open-Meteo falhou"}
55
+
56
+ def _get_error_suggestion(self) -> str:
57
+ """Sugestão customizada quando tudo falha"""
58
+ return "Tenta com o nome de uma cidade maior ou tenta de novo mais tarde"