akra35567 commited on
Commit
9bddcf0
·
1 Parent(s): b2fd145

fix: mega-sweep - crash fixes, autonomous_agent TS, memory leaks, DB bugs

Browse files

CRITICAL 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 CHANGED
@@ -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
- # 0. Track flood/spam (detecção temporal de mensagens rápidas)
3018
- _track_result = _autonomous_agent.track_message(
3019
- user_jid=_user_jid,
3020
- group_jid=_group_jid,
3021
- message=mensagem
3022
- )
3023
- if _track_result and _track_result.get("type") == "remote_action":
3024
- autonomous_actions.append(_track_result)
3025
- self.logger.info(f"🤖 [AUTONOMOUS TRACK] Flood/spam detetado")
 
 
 
 
 
 
 
 
 
3026
 
3027
- # 1. Análise de hostilidade para ações (mutar/banir/prevenir)
3028
- action_analysis = _autonomous_agent.analyze_hostility_for_action(
3029
- float(hostility_score), _user_jid, _group_jid
3030
- )
3031
- if action_analysis:
3032
- autonomous_actions.append(action_analysis)
3033
- _cmd = action_analysis.get('params', {}).get('cmd', '?')
3034
- self.logger.info(f"🤖 [AUTONOMOUS] hostilidade → {_cmd}")
3035
-
3036
- # 2. Detecção de toxicidade (proatividade, sem mensagem hostil)
3037
- toxicity_actions = _autonomous_agent.analyze_toxic_language(mensagem, _user_jid, _group_jid)
3038
- if toxicity_actions:
3039
- autonomous_actions.append(toxicity_actions)
3040
- _cmd = toxicity_actions.get('params', {}).get('cmd', '?')
3041
- self.logger.info(f"🤖 [AUTONOMOUS TOXIC] toxicidade → {_cmd}")
3042
-
3043
- # 3. Abuso de menção em massa
3044
- mass_mention_actions = _autonomous_agent.analyze_mass_mention_abuse(mensagem, _user_jid, _group_jid)
3045
- if mass_mention_actions:
3046
- autonomous_actions.append(mass_mention_actions)
3047
- _cmd = mass_mention_actions.get('params', {}).get('cmd', '?')
3048
- self.logger.info(f"🤖 [AUTONOMOUS SPAM] menções → {_cmd}")
3049
-
3050
- # 4. Análise de conteúdo (links proibidos, ameaças, conteúdo ofensivo)
3051
- content_actions = _autonomous_agent.analyze_message_for_moderation(mensagem, _user_jid, _group_jid)
3052
- if content_actions:
3053
- autonomous_actions.append(content_actions)
3054
- self.logger.info(f"🤖 [AUTONOMOUS CONTENT] Violação de conteúdo detetada")
3055
-
3056
- # 5. Análise de imagem para NSFW/Gore (se houve análise visual)
3057
- if analise_visao and isinstance(analise_visao, dict):
3058
- img_desc = analise_visao.get('description', '')
3059
- if img_desc:
3060
- img_actions = _autonomous_agent.analyze_image_description_for_moderation(img_desc, _user_jid, _group_jid)
3061
- if img_actions:
3062
- autonomous_actions.append(img_actions)
3063
- self.logger.info(f"🤖 [AUTONOMOUS VISUAL] Conteúdo proibido detetado na imagem")
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.update(obs_data)
 
 
 
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."
modules/database_pg.py CHANGED
@@ -769,12 +769,12 @@ class DatabasePG:
769
  def recuperar_humor(self, numero_usuario):
770
  try:
771
  rows = self._execute_with_retry(
772
- "SELECT humor FROM contexto WHERE user_key = %s",
773
  (numero_usuario,)
774
  )
775
  if rows:
776
  r = rows[0]
777
- return r['humor'] if isinstance(r, dict) else r[0]
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
- try:
1140
- fcntl.flock(lock_fd, fcntl.LOCK_EX | fcntl.LOCK_NB)
1141
- except BlockingIOError:
1142
- logger.info("⏳ Backup já em execução por outro worker — pulando")
1143
- lock_fd.close()
1144
- return True
 
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
- fcntl.flock(lock_fd, fcntl.LOCK_UN)
 
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}")
modules/skills/autonomous_agent.py CHANGED
@@ -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
- "cu", "buceta", "piranha", "vagabunda", "vagabundo"
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 Akira, agente autónoma de infraestrutura. "
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: mute_user, kick_user, lock_group, send_owner_report, ignore."
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'\{.*\}', response, re.DOTALL)
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:
modules/skills/base_skill.py CHANGED
@@ -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
- # 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
 
 
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