Spaces:
Running
Running
Upload 9 files
Browse files- modules/skills/__init__.py +19 -0
- modules/skills/art_skill.py +89 -0
- modules/skills/autonomous_agent.py +269 -0
- modules/skills/base_skill.py +280 -0
- modules/skills/entertainment_skill.py +85 -0
- modules/skills/manus_skill.py +44 -0
- modules/skills/music_skill.py +92 -0
- modules/skills/native_research.py +216 -0
- modules/skills/weather_skill.py +58 -0
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"
|