Spaces:
Running
fix: mega-sweep - crash fixes, autonomous_agent TS, memory leaks, DB bugs
Browse filesCRITICAL CRASH FIXES:
- api.py: raw_str NameError on doc/image messages (line 1964)
- api.py: media_response AttributeError when skill returns non-dict
AUTONOMOUS AGENT:
- LLM prompt said 'Akira' instead of 'Belmira' + wrong action names
- 'cu' substring matched 'cuidar','curta' (word boundary for short patterns)
- Belmira was moderating herself (self-check guard added)
- _action_log memory leak (capped at 500 entries)
- _spam_tracker memory leak (prune empty keys)
- JSON regex greedy match (use non-greedy [^{}]*)
DATABASE_PG:
- recuperar_humor queried 'humor' but column is 'humor_atual'
- isinstance(memoryview) missing second argument
- import fcntl crashed on Windows (graceful fallback)
BASE_SKILL:
- cache_ttl always ignored (_make_cache_key was popping it)
- modules/api.py +60 -49
- modules/database_pg.py +17 -11
- modules/skills/autonomous_agent.py +15 -4
- modules/skills/base_skill.py +1 -4
|
@@ -1961,7 +1961,6 @@ class AkiraAPI:
|
|
| 1961 |
_img_check = 'imagem' in data or 'imagem_dados' in data
|
| 1962 |
if _doc_check or _img_check:
|
| 1963 |
self.logger.info(f"[API] Campos recebidos: documento={_doc_check} | imagem={_img_check} | keys={list(data.keys())}")
|
| 1964 |
-
self.logger.warning(f"[API] Payload resultou em dicionário vazio. Bruto (latin-1): {raw_str[:200]}")
|
| 1965 |
|
| 1966 |
usuario = data.get('usuario', 'anonimo')
|
| 1967 |
numero = data.get('numero', '')
|
|
@@ -3014,53 +3013,62 @@ class AkiraAPI:
|
|
| 3014 |
_user_jid = numero or usuario
|
| 3015 |
_group_jid = grupo_id or ''
|
| 3016 |
|
| 3017 |
-
#
|
| 3018 |
-
|
| 3019 |
-
|
| 3020 |
-
|
| 3021 |
-
|
| 3022 |
-
|
| 3023 |
-
|
| 3024 |
-
|
| 3025 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 3026 |
|
| 3027 |
-
|
| 3028 |
-
|
| 3029 |
-
|
| 3030 |
-
|
| 3031 |
-
|
| 3032 |
-
|
| 3033 |
-
|
| 3034 |
-
|
| 3035 |
-
|
| 3036 |
-
|
| 3037 |
-
|
| 3038 |
-
|
| 3039 |
-
|
| 3040 |
-
|
| 3041 |
-
|
| 3042 |
-
|
| 3043 |
-
|
| 3044 |
-
|
| 3045 |
-
|
| 3046 |
-
|
| 3047 |
-
|
| 3048 |
-
|
| 3049 |
-
|
| 3050 |
-
|
| 3051 |
-
|
| 3052 |
-
|
| 3053 |
-
|
| 3054 |
-
|
| 3055 |
-
|
| 3056 |
-
|
| 3057 |
-
|
| 3058 |
-
|
| 3059 |
-
|
| 3060 |
-
|
| 3061 |
-
|
| 3062 |
-
|
| 3063 |
-
|
| 3064 |
except Exception as e:
|
| 3065 |
self.logger.warning(f"⚠️ [AUTONOMOUS] Falha na análise: {e}")
|
| 3066 |
|
|
@@ -4464,7 +4472,7 @@ class AkiraAPI:
|
|
| 4464 |
self.logger.debug(f" ℹ️ observation não é dict nem JSON string")
|
| 4465 |
|
| 4466 |
# ✅ NOVO: Captura media_response se houver (para imagens geradas)
|
| 4467 |
-
if obs_data.get("media_response"):
|
| 4468 |
media_response = obs_data.get("media_response")
|
| 4469 |
self.logger.info(f"📸 [MEDIA] Capturado media_response: tipo={media_response.get('tipo')}")
|
| 4470 |
|
|
@@ -4476,7 +4484,10 @@ class AkiraAPI:
|
|
| 4476 |
remote_actions.append(obs_data)
|
| 4477 |
observation = f"Ação remota '{obs_data.get('action')}' será executada pelo bot."
|
| 4478 |
elif obs_data.get("type") == "media_response":
|
| 4479 |
-
media_response
|
|
|
|
|
|
|
|
|
|
| 4480 |
observation = f"Mídia gerada com sucesso."
|
| 4481 |
else:
|
| 4482 |
observation = f"Resultado obtido com sucesso."
|
|
|
|
| 1961 |
_img_check = 'imagem' in data or 'imagem_dados' in data
|
| 1962 |
if _doc_check or _img_check:
|
| 1963 |
self.logger.info(f"[API] Campos recebidos: documento={_doc_check} | imagem={_img_check} | keys={list(data.keys())}")
|
|
|
|
| 1964 |
|
| 1965 |
usuario = data.get('usuario', 'anonimo')
|
| 1966 |
numero = data.get('numero', '')
|
|
|
|
| 3013 |
_user_jid = numero or usuario
|
| 3014 |
_group_jid = grupo_id or ''
|
| 3015 |
|
| 3016 |
+
# SKIP: Belmira não modera a ela mesma
|
| 3017 |
+
_bot_numero = str(getattr(self.config, 'BOT_NUMERO', '37839265886398'))
|
| 3018 |
+
_sender_pure = re.sub(r'\D', '', str(_user_jid))
|
| 3019 |
+
_bot_pure = re.sub(r'\D', '', _bot_numero)
|
| 3020 |
+
if _sender_pure and _bot_pure and _sender_pure == _bot_pure:
|
| 3021 |
+
pass # Não moderar a própria Belmira
|
| 3022 |
+
elif str(_user_jid).startswith('BOT:'):
|
| 3023 |
+
pass # Não moderar outros bots
|
| 3024 |
+
else:
|
| 3025 |
+
# 0. Track flood/spam (detecção temporal de mensagens rápidas)
|
| 3026 |
+
_track_result = _autonomous_agent.track_message(
|
| 3027 |
+
user_jid=_user_jid,
|
| 3028 |
+
group_jid=_group_jid,
|
| 3029 |
+
message=mensagem
|
| 3030 |
+
)
|
| 3031 |
+
if _track_result and _track_result.get("type") == "remote_action":
|
| 3032 |
+
autonomous_actions.append(_track_result)
|
| 3033 |
+
self.logger.info(f"🤖 [AUTONOMOUS TRACK] Flood/spam detetado")
|
| 3034 |
|
| 3035 |
+
# 1. Análise de hostilidade para ações (mutar/banir/prevenir)
|
| 3036 |
+
action_analysis = _autonomous_agent.analyze_hostility_for_action(
|
| 3037 |
+
float(hostility_score), _user_jid, _group_jid
|
| 3038 |
+
)
|
| 3039 |
+
if action_analysis:
|
| 3040 |
+
autonomous_actions.append(action_analysis)
|
| 3041 |
+
_cmd = action_analysis.get('params', {}).get('cmd', '?')
|
| 3042 |
+
self.logger.info(f"🤖 [AUTONOMOUS] hostilidade → {_cmd}")
|
| 3043 |
+
|
| 3044 |
+
# 2. Detecção de toxicidade (proatividade, sem mensagem hostil)
|
| 3045 |
+
toxicity_actions = _autonomous_agent.analyze_toxic_language(mensagem, _user_jid, _group_jid)
|
| 3046 |
+
if toxicity_actions:
|
| 3047 |
+
autonomous_actions.append(toxicity_actions)
|
| 3048 |
+
_cmd = toxicity_actions.get('params', {}).get('cmd', '?')
|
| 3049 |
+
self.logger.info(f"🤖 [AUTONOMOUS TOXIC] toxicidade → {_cmd}")
|
| 3050 |
+
|
| 3051 |
+
# 3. Abuso de menção em massa
|
| 3052 |
+
mass_mention_actions = _autonomous_agent.analyze_mass_mention_abuse(mensagem, _user_jid, _group_jid)
|
| 3053 |
+
if mass_mention_actions:
|
| 3054 |
+
autonomous_actions.append(mass_mention_actions)
|
| 3055 |
+
_cmd = mass_mention_actions.get('params', {}).get('cmd', '?')
|
| 3056 |
+
self.logger.info(f"🤖 [AUTONOMOUS SPAM] menções → {_cmd}")
|
| 3057 |
+
|
| 3058 |
+
# 4. Análise de conteúdo (links proibidos, ameaças, conteúdo ofensivo)
|
| 3059 |
+
content_actions = _autonomous_agent.analyze_message_for_moderation(mensagem, _user_jid, _group_jid)
|
| 3060 |
+
if content_actions:
|
| 3061 |
+
autonomous_actions.append(content_actions)
|
| 3062 |
+
self.logger.info(f"🤖 [AUTONOMOUS CONTENT] Violação de conteúdo detetada")
|
| 3063 |
+
|
| 3064 |
+
# 5. Análise de imagem para NSFW/Gore (se houve análise visual)
|
| 3065 |
+
if analise_visao and isinstance(analise_visao, dict):
|
| 3066 |
+
img_desc = analise_visao.get('description', '')
|
| 3067 |
+
if img_desc:
|
| 3068 |
+
img_actions = _autonomous_agent.analyze_image_description_for_moderation(img_desc, _user_jid, _group_jid)
|
| 3069 |
+
if img_actions:
|
| 3070 |
+
autonomous_actions.append(img_actions)
|
| 3071 |
+
self.logger.info(f"🤖 [AUTONOMOUS VISUAL] Conteúdo proibido detetado na imagem")
|
| 3072 |
except Exception as e:
|
| 3073 |
self.logger.warning(f"⚠️ [AUTONOMOUS] Falha na análise: {e}")
|
| 3074 |
|
|
|
|
| 4472 |
self.logger.debug(f" ℹ️ observation não é dict nem JSON string")
|
| 4473 |
|
| 4474 |
# ✅ NOVO: Captura media_response se houver (para imagens geradas)
|
| 4475 |
+
if obs_data.get("media_response") and isinstance(obs_data.get("media_response"), dict):
|
| 4476 |
media_response = obs_data.get("media_response")
|
| 4477 |
self.logger.info(f"📸 [MEDIA] Capturado media_response: tipo={media_response.get('tipo')}")
|
| 4478 |
|
|
|
|
| 4484 |
remote_actions.append(obs_data)
|
| 4485 |
observation = f"Ação remota '{obs_data.get('action')}' será executada pelo bot."
|
| 4486 |
elif obs_data.get("type") == "media_response":
|
| 4487 |
+
if media_response and isinstance(media_response, dict):
|
| 4488 |
+
media_response.update(obs_data)
|
| 4489 |
+
else:
|
| 4490 |
+
media_response = obs_data
|
| 4491 |
observation = f"Mídia gerada com sucesso."
|
| 4492 |
else:
|
| 4493 |
observation = f"Resultado obtido com sucesso."
|
|
@@ -769,12 +769,12 @@ class DatabasePG:
|
|
| 769 |
def recuperar_humor(self, numero_usuario):
|
| 770 |
try:
|
| 771 |
rows = self._execute_with_retry(
|
| 772 |
-
"SELECT
|
| 773 |
(numero_usuario,)
|
| 774 |
)
|
| 775 |
if rows:
|
| 776 |
r = rows[0]
|
| 777 |
-
return r['
|
| 778 |
return "neutro"
|
| 779 |
except:
|
| 780 |
return "neutro"
|
|
@@ -915,7 +915,7 @@ class DatabasePG:
|
|
| 915 |
results = []
|
| 916 |
for r in rows:
|
| 917 |
emb = r['embedding']
|
| 918 |
-
if isinstance(memoryview):
|
| 919 |
emb = bytes(emb)
|
| 920 |
results.append({
|
| 921 |
'source_type': r['source_type'],
|
|
@@ -1126,8 +1126,12 @@ class DatabasePG:
|
|
| 1126 |
def fazer_checkpoint_hf_sync(self):
|
| 1127 |
"""Backup PostgreSQL via pg_dump com lock para evitar dupla execução entre workers."""
|
| 1128 |
import subprocess
|
| 1129 |
-
import fcntl
|
| 1130 |
from pathlib import Path
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1131 |
try:
|
| 1132 |
cloud_sync_dir = Path("/akira/data/cloud_sync")
|
| 1133 |
cloud_sync_dir.mkdir(parents=True, exist_ok=True)
|
|
@@ -1136,12 +1140,13 @@ class DatabasePG:
|
|
| 1136 |
|
| 1137 |
# Lock file para garantir que apenas 1 worker executa pg_dump
|
| 1138 |
lock_fd = open(lock_path, 'w')
|
| 1139 |
-
|
| 1140 |
-
|
| 1141 |
-
|
| 1142 |
-
|
| 1143 |
-
|
| 1144 |
-
|
|
|
|
| 1145 |
|
| 1146 |
try:
|
| 1147 |
params = self._conn_params
|
|
@@ -1160,7 +1165,8 @@ class DatabasePG:
|
|
| 1160 |
logger.error(f"pg_dump falhou: {result.stderr}")
|
| 1161 |
return False
|
| 1162 |
finally:
|
| 1163 |
-
|
|
|
|
| 1164 |
lock_fd.close()
|
| 1165 |
except Exception as e:
|
| 1166 |
logger.error(f"Erro no checkpoint: {e}")
|
|
|
|
| 769 |
def recuperar_humor(self, numero_usuario):
|
| 770 |
try:
|
| 771 |
rows = self._execute_with_retry(
|
| 772 |
+
"SELECT humor_atual FROM contexto WHERE user_key = %s",
|
| 773 |
(numero_usuario,)
|
| 774 |
)
|
| 775 |
if rows:
|
| 776 |
r = rows[0]
|
| 777 |
+
return r['humor_atual'] if isinstance(r, dict) else r[0]
|
| 778 |
return "neutro"
|
| 779 |
except:
|
| 780 |
return "neutro"
|
|
|
|
| 915 |
results = []
|
| 916 |
for r in rows:
|
| 917 |
emb = r['embedding']
|
| 918 |
+
if isinstance(emb, memoryview):
|
| 919 |
emb = bytes(emb)
|
| 920 |
results.append({
|
| 921 |
'source_type': r['source_type'],
|
|
|
|
| 1126 |
def fazer_checkpoint_hf_sync(self):
|
| 1127 |
"""Backup PostgreSQL via pg_dump com lock para evitar dupla execução entre workers."""
|
| 1128 |
import subprocess
|
|
|
|
| 1129 |
from pathlib import Path
|
| 1130 |
+
try:
|
| 1131 |
+
import fcntl
|
| 1132 |
+
except ImportError:
|
| 1133 |
+
logger.warning("⚠️ fcntl não disponível (Windows) — backup sem lock entre workers")
|
| 1134 |
+
fcntl = None
|
| 1135 |
try:
|
| 1136 |
cloud_sync_dir = Path("/akira/data/cloud_sync")
|
| 1137 |
cloud_sync_dir.mkdir(parents=True, exist_ok=True)
|
|
|
|
| 1140 |
|
| 1141 |
# Lock file para garantir que apenas 1 worker executa pg_dump
|
| 1142 |
lock_fd = open(lock_path, 'w')
|
| 1143 |
+
if fcntl:
|
| 1144 |
+
try:
|
| 1145 |
+
fcntl.flock(lock_fd, fcntl.LOCK_EX | fcntl.LOCK_NB)
|
| 1146 |
+
except BlockingIOError:
|
| 1147 |
+
logger.info("⏳ Backup já em execução por outro worker — pulando")
|
| 1148 |
+
lock_fd.close()
|
| 1149 |
+
return True
|
| 1150 |
|
| 1151 |
try:
|
| 1152 |
params = self._conn_params
|
|
|
|
| 1165 |
logger.error(f"pg_dump falhou: {result.stderr}")
|
| 1166 |
return False
|
| 1167 |
finally:
|
| 1168 |
+
if fcntl:
|
| 1169 |
+
fcntl.flock(lock_fd, fcntl.LOCK_UN)
|
| 1170 |
lock_fd.close()
|
| 1171 |
except Exception as e:
|
| 1172 |
logger.error(f"Erro no checkpoint: {e}")
|
|
@@ -74,6 +74,8 @@ class AutonomousAgent:
|
|
| 74 |
ts for ts in self._spam_tracker[key]
|
| 75 |
if timestamp - ts <= 60
|
| 76 |
]
|
|
|
|
|
|
|
| 77 |
|
| 78 |
# ─── Detecta Flood ───
|
| 79 |
recent_flood = [ts for ts in self._spam_tracker[key] if timestamp - ts <= FLOOD_WINDOW_SECS]
|
|
@@ -238,8 +240,10 @@ class AutonomousAgent:
|
|
| 238 |
"filho da mãe", "piriquito", "bosta", "merda", "fodasse",
|
| 239 |
"vai te foder", "come merda", "peste", "lixo", "nojento", "nojenta",
|
| 240 |
"desgraçado", "desgraçada", "maldito", "maldita", "cornos",
|
| 241 |
-
"
|
| 242 |
]
|
|
|
|
|
|
|
| 243 |
|
| 244 |
# Padrões de ameaças de violência
|
| 245 |
threat_patterns = [
|
|
@@ -252,7 +256,12 @@ class AutonomousAgent:
|
|
| 252 |
]
|
| 253 |
|
| 254 |
# Conta insultos encontrados
|
|
|
|
| 255 |
insults_found = [p for p in insult_patterns if p in msg_lower]
|
|
|
|
|
|
|
|
|
|
|
|
|
| 256 |
threats_found = [p for p in threat_patterns if p in msg_lower]
|
| 257 |
|
| 258 |
if threats_found:
|
|
@@ -389,6 +398,8 @@ class AutonomousAgent:
|
|
| 389 |
if blacklist:
|
| 390 |
log_entry["blacklist"] = True
|
| 391 |
self._action_log.append(log_entry)
|
|
|
|
|
|
|
| 392 |
|
| 393 |
# Guarda no DB se disponível
|
| 394 |
if self._db:
|
|
@@ -442,17 +453,17 @@ class AutonomousAgent:
|
|
| 442 |
return None
|
| 443 |
|
| 444 |
system = (
|
| 445 |
-
"És a
|
| 446 |
"Analisa o evento abaixo e decide a melhor ação a tomar. "
|
| 447 |
"Responde APENAS em JSON com: {\"acao\": \"string\", \"motivo\": \"string\", \"prioridade\": \"alta|media|baixa\"}. "
|
| 448 |
-
"Ações possíveis:
|
| 449 |
)
|
| 450 |
|
| 451 |
try:
|
| 452 |
response = self._llm_caller(system, f"EVENTO: {event_type}\nCONTEXTO:\n{context}")
|
| 453 |
# Extrai JSON da resposta
|
| 454 |
import re
|
| 455 |
-
json_match = re.search(r'\{
|
| 456 |
if json_match:
|
| 457 |
return json.loads(json_match.group())
|
| 458 |
except Exception as e:
|
|
|
|
| 74 |
ts for ts in self._spam_tracker[key]
|
| 75 |
if timestamp - ts <= 60
|
| 76 |
]
|
| 77 |
+
if not self._spam_tracker[key]:
|
| 78 |
+
del self._spam_tracker[key]
|
| 79 |
|
| 80 |
# ─── Detecta Flood ───
|
| 81 |
recent_flood = [ts for ts in self._spam_tracker[key] if timestamp - ts <= FLOOD_WINDOW_SECS]
|
|
|
|
| 240 |
"filho da mãe", "piriquito", "bosta", "merda", "fodasse",
|
| 241 |
"vai te foder", "come merda", "peste", "lixo", "nojento", "nojenta",
|
| 242 |
"desgraçado", "desgraçada", "maldito", "maldita", "cornos",
|
| 243 |
+
"buceta", "piranha", "vagabunda", "vagabundo"
|
| 244 |
]
|
| 245 |
+
# Padrões curtos que precisam de word boundary para evitar falsos positivos
|
| 246 |
+
insult_patterns_boundary = ["cu"]
|
| 247 |
|
| 248 |
# Padrões de ameaças de violência
|
| 249 |
threat_patterns = [
|
|
|
|
| 256 |
]
|
| 257 |
|
| 258 |
# Conta insultos encontrados
|
| 259 |
+
import re
|
| 260 |
insults_found = [p for p in insult_patterns if p in msg_lower]
|
| 261 |
+
# Padrões curtos: word boundary para evitar "cu" em "cuidar"
|
| 262 |
+
for p in insult_patterns_boundary:
|
| 263 |
+
if re.search(r'\b' + re.escape(p) + r'\b', msg_lower):
|
| 264 |
+
insults_found.append(p)
|
| 265 |
threats_found = [p for p in threat_patterns if p in msg_lower]
|
| 266 |
|
| 267 |
if threats_found:
|
|
|
|
| 398 |
if blacklist:
|
| 399 |
log_entry["blacklist"] = True
|
| 400 |
self._action_log.append(log_entry)
|
| 401 |
+
if len(self._action_log) > 500:
|
| 402 |
+
self._action_log = self._action_log[-500:]
|
| 403 |
|
| 404 |
# Guarda no DB se disponível
|
| 405 |
if self._db:
|
|
|
|
| 453 |
return None
|
| 454 |
|
| 455 |
system = (
|
| 456 |
+
"És a Belmira, agente autónoma de infraestrutura da Softedge. "
|
| 457 |
"Analisa o evento abaixo e decide a melhor ação a tomar. "
|
| 458 |
"Responde APENAS em JSON com: {\"acao\": \"string\", \"motivo\": \"string\", \"prioridade\": \"alta|media|baixa\"}. "
|
| 459 |
+
"Ações possíveis: mute, ban, warn, blacklist, ignore."
|
| 460 |
)
|
| 461 |
|
| 462 |
try:
|
| 463 |
response = self._llm_caller(system, f"EVENTO: {event_type}\nCONTEXTO:\n{context}")
|
| 464 |
# Extrai JSON da resposta
|
| 465 |
import re
|
| 466 |
+
json_match = re.search(r'\{[^{}]*\}', response, re.DOTALL)
|
| 467 |
if json_match:
|
| 468 |
return json.loads(json_match.group())
|
| 469 |
except Exception as e:
|
|
@@ -213,10 +213,7 @@ class BaseSkill(ABC):
|
|
| 213 |
|
| 214 |
def _make_cache_key(self, *args, **kwargs) -> str:
|
| 215 |
"""Cria chave de cache baseada em argumentos"""
|
| 216 |
-
#
|
| 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 |
|
|
|
|
| 213 |
|
| 214 |
def _make_cache_key(self, *args, **kwargs) -> str:
|
| 215 |
"""Cria chave de cache baseada em argumentos"""
|
| 216 |
+
# Não remove cache_ttl — é preservado para uso externo
|
|
|
|
|
|
|
|
|
|
| 217 |
key_str = f"{self.name}:{json.dumps([args, kwargs], sort_keys=True, default=str)}"
|
| 218 |
return hashlib.md5(key_str.encode()).hexdigest()
|
| 219 |
|