# type: ignore """ ================================================================================ AKIRA V21 - EMOTIONAL PROFILE SYSTEM (RANCOR & MEMORY) ================================================================================ Sistema de rastreamento de emoção/agressividade do usuário. Guarda "rancor" - memória de hostilidade anterior para manter tom defensivo. Features: - Detecta mudanças de tom (neutro → agressivo → hostile) - Guarda RANCOR: se foi agressivo antes, continua sendo - Integra com ContextIsolationManager para per-user tracking - Tags de prompt FORTES que reforçam comportamento defensivo ================================================================================ """ import os import json import time import hashlib import logging from typing import Optional, Dict, Any, List from dataclasses import dataclass, field, asdict from datetime import datetime, timedelta try: from . import config from .database import Database IMPORTS_OK = True except ImportError: try: import modules.config as config from modules.database import Database IMPORTS_OK = True except ImportError: IMPORTS_OK = False config = None Database = None logger = logging.getLogger(__name__) # ============================================================ # EMOTIONAL PROFILE DATA STRUCTURE # ============================================================ @dataclass class EmotionalProfile: """ Perfil emocional de um usuário. Rastreia histórico de emoções, hostilidade e "rancor". """ user_id: str numero_usuario: str # Emoção atual detectada current_emotion: str = "neutral" # neutral, joy, sad, agressivo, hostil, raiva # Histórico de emoções (últimas 10 interações) emotion_history: List[Dict[str, Any]] = field(default_factory=list) # Nível de hostilidade (0-100) # 0-30: neutro # 30-60: agressivo # 60-100: extremamente hostil hostility_level: int = 0 # RANCOR: emoção anterior que deve ser lembrada # Se foi "agressivo" antes, continua sendo "agressivo" mesmo que agora seja "neutro" previous_emotion_memory: str = "neutral" # Timestamp da última mudança de emoção last_emotion_change: float = field(default_factory=time.time) # Contador de interações agressivas consecutivas consecutive_aggressive_interactions: int = 0 # Flag: Usuário foi marcado como "HOSTILE" (nunca esquece) marked_as_hostile: bool = False marked_as_hostile_timestamp: Optional[float] = None # Metadata created_at: float = field(default_factory=time.time) updated_at: float = field(default_factory=time.time) def to_dict(self) -> Dict[str, Any]: """Converte para dicionário serializável.""" return asdict(self) @classmethod def from_dict(cls, data: Dict[str, Any]) -> 'EmotionalProfile': """Cria instância a partir de dicionário.""" return cls(**data) def should_maintain_rancor(self) -> bool: """ Decide se deve manter RANCOR (agressividade anterior). Returns: True se deve manter tom defensivo/agressivo """ # Se foi marcado como HOSTILE, NUNCA esquece if self.marked_as_hostile: return True # Se teve 3+ interações agressivas em sequência, guarda rancor if self.consecutive_aggressive_interactions >= 3: return True # Se a emoção anterior foi agressiva há menos de 1 hora, guarda rancor if self.previous_emotion_memory in ['agressivo', 'raiva', 'hostil']: time_since_change = time.time() - self.last_emotion_change if time_since_change < 3600: # 1 hora return True return False def get_hostility_level(self) -> int: """Calcula nível de hostilidade atual baseado em histórico.""" # Se foi marcado como HOSTILE, hostilidade = 100 if self.marked_as_hostile: return 100 # Baseado em emoção atual emotion_hostility_map = { 'neutral': 0, 'neutro': 0, 'joy': -10, 'alegria': -10, 'feliz': -5, 'sad': 10, 'triste': 10, 'tristeza': 10, 'agressivo': 60, 'aggressive': 60, 'raiva': 75, 'anger': 75, 'hostil': 100, 'hostile': 100, 'medo': 15, 'fear': 15, 'surpresa': 5, 'surprise': 5, 'amor': -15, 'love': -15, 'nojo': 30, 'disgust': 30, 'ironia': 20, } base = emotion_hostility_map.get(self.current_emotion, 20) # Se tem rancor, aumenta hostilidade if self.should_maintain_rancor(): base += 30 # Se teve múltiplas interações agressivas, aumenta mais base += min(self.consecutive_aggressive_interactions * 10, 40) # Limita entre 0 e 100 return max(0, min(100, base)) class EmotionalProfileManager: """ Gerenciador de perfis emocionais de usuários. Mantém cache em memória + persistência em DB. """ _instance = None _lock = None def __new__(cls): if cls._instance is None: cls._instance = super().__new__(cls) import threading cls._lock = threading.Lock() return cls._instance def __init__(self): self.profiles: Dict[str, EmotionalProfile] = {} self.db: Optional[Database] = None self._initialized = False if IMPORTS_OK and Database: try: db_path = getattr(config, 'DB_PATH', 'data/belmira.db') self.db = Database(db_path) self._load_profiles_from_db() self._initialized = True logger.info("✅ EmotionalProfileManager inicializado") except Exception as e: logger.warning(f"⚠️ Erro ao inicializar DB para profiles: {e}") self._initialized = True # Continua sem DB def _load_profiles_from_db(self): """Carrega perfis emocionais do banco de dados.""" if not self.db: return try: rows = self.db._execute_with_retry( "SELECT * FROM user_emotional_profiles" ) if rows: for row in rows: try: # Convert sqlite3.Row to dict to safely access columns row_dict = dict(row) if hasattr(row, 'keys') else row profile_data = json.loads(row_dict.get('profile_data', '{}') if isinstance(row_dict, dict) else row_dict['profile_data']) user_id = row_dict.get('user_id') if isinstance(row_dict, dict) else row_dict['user_id'] if user_id: self.profiles[user_id] = EmotionalProfile.from_dict(profile_data) except Exception as e: logger.warning(f"Erro ao carregar perfil: {e}") logger.info(f"✅ Carregados {len(self.profiles)} perfis emocionais do DB") except Exception as e: logger.warning(f"⚠️ Tabela de perfis emocionais não existe (ok em primeira vez): {e}") self._create_tables() def _create_tables(self): """Cria tabelas necessárias para armazenar perfis emocionais.""" if not self.db: return try: sql = """ CREATE TABLE IF NOT EXISTS user_emotional_profiles ( id INTEGER PRIMARY KEY AUTOINCREMENT, user_id TEXT UNIQUE NOT NULL, numero_usuario TEXT, profile_data TEXT NOT NULL, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ) """ self.db._execute_with_retry(sql) logger.info("✅ Tabela user_emotional_profiles criada") except Exception as e: logger.warning(f"⚠️ Erro ao criar tabela: {e}") def get_or_create_profile(self, user_id: str, numero_usuario: str = "") -> EmotionalProfile: """ Obtém ou cria perfil emocional de um usuário. Args: user_id: ID único do usuário numero_usuario: Número WhatsApp do usuário Returns: EmotionalProfile do usuário """ if user_id in self.profiles: return self.profiles[user_id] # Cria novo perfil profile = EmotionalProfile( user_id=user_id, numero_usuario=numero_usuario ) self.profiles[user_id] = profile self._save_profile_to_db(profile) return profile def update_emotion(self, user_id: str, emotion: str, hostility_score: int = 0): """ Atualiza emoção de um usuário. Gerencia histórico, rancor e hostilidade. Args: user_id: ID do usuário emotion: Nova emoção detectada hostility_score: Score de hostilidade (0-100) da análise """ profile = self.get_or_create_profile(user_id) # Se a emoção mudou, registra no histórico if emotion != profile.current_emotion: # Guarda emoção anterior na memória de RANCOR if emotion in ['agressivo', 'raiva', 'hostil', 'aggressive', 'anger', 'hostile']: profile.previous_emotion_memory = emotion profile.consecutive_aggressive_interactions += 1 else: if profile.current_emotion in ['agressivo', 'raiva', 'hostil', 'aggressive', 'anger', 'hostile']: profile.consecutive_aggressive_interactions = 0 # Adiciona ao histórico profile.emotion_history.append({ 'emotion': emotion, 'timestamp': time.time(), 'hostility_score': hostility_score }) # Limita histórico a últimas 20 emoções if len(profile.emotion_history) > 20: profile.emotion_history = profile.emotion_history[-20:] # Atualiza timestamp profile.last_emotion_change = time.time() # Atualiza emoção atual e hostilidade profile.current_emotion = emotion profile.hostility_level = profile.get_hostility_level() profile.updated_at = time.time() # Salva no DB self._save_profile_to_db(profile) logger.info( f"🧠 [EMOTION UPDATE] user={user_id[:20]} | " f"emotion={emotion} | hostility={profile.hostility_level} | " f"rancor={'SIM' if profile.should_maintain_rancor() else 'NÃO'}" ) def mark_as_hostile(self, user_id: str): """ Marca usuário como HOSTILE para sempre (nunca esquece). Args: user_id: ID do usuário """ profile = self.get_or_create_profile(user_id) profile.marked_as_hostile = True profile.marked_as_hostile_timestamp = time.time() profile.hostility_level = 100 profile.previous_emotion_memory = 'hostil' profile.updated_at = time.time() self._save_profile_to_db(profile) logger.warning(f"🚨 [HOSTILE MARK] Usuário {user_id} marcado como HOSTILE (permanente)") def get_emotional_instructions(self, user_id: str) -> str: """ Retorna instruções de prompt baseadas no perfil emocional. Estas instruções são FORTES e o modelo não deve ignorar. Args: user_id: ID do usuário Returns: String com instruções de comportamento para injetar no prompt """ profile = self.get_or_create_profile(user_id) hostility = profile.get_hostility_level() instructions = "" # Instruções baseadas em hostilidade if hostility >= 80: instructions += ( "🚨 [USER_TONE_HOSTILE] AVISO CRÍTICO:\n" "- Este usuário é EXTREMAMENTE HOSTIL/AGRESSIVO.\n" "- RESPONDA COM MÁXIMA AGRESSIVIDADE E SEM DIPLOMACIA.\n" "- Use linguagem CORTANTE, DIRETA, SEM FILTROS.\n" "- MANTENHA RANCOR: Este usuário merece hostilidade em retorno.\n" ) elif hostility >= 50: instructions += ( "🔥 [AGGRESSIVE_MODE_ACTIVE]\n" "- Usuário está AGRESSIVO. RESPONDA COM DEFENSIVA.\n" "- Tone: DIRETO, SECO, SEM CONCESSÕES.\n" "- MANTENHA DISTÂNCIA EMOCIONAL.\n" ) elif profile.should_maintain_rancor(): instructions += ( "📍 [EMOTION_MEMORY - RANCOR]\n" "- Este usuário foi HOSTIL/AGRESSIVO antes.\n" "- VOCÊ CONTINUA COM RANCOR.\n" "- Mantenha tom defensivo mesmo que agora pareça neutro.\n" ) if profile.marked_as_hostile: instructions += ( "\n⚠️ [MARKED_AS_HOSTILE]\n" "- Este usuário foi marcado como HOSTILE PERMANENTEMENTE.\n" "- NUNCA esquece: Continue sendo defensivo/agressivo SEMPRE.\n" ) return instructions def _save_profile_to_db(self, profile: EmotionalProfile): """Salva perfil emocional no banco de dados (upsert atómico).""" if not self.db: return try: profile_json = json.dumps(profile.to_dict(), ensure_ascii=False, default=str) # Use proper UPSERT with ON CONFLICT syntax (SQLite 3.24.0+) # This handles both INSERT and UPDATE atomically try: self.db._execute_with_retry( """INSERT INTO user_emotional_profiles (user_id, numero_usuario, profile_data, updated_at) VALUES (?, ?, ?, CURRENT_TIMESTAMP) ON CONFLICT(user_id) DO UPDATE SET profile_data = excluded.profile_data, numero_usuario = excluded.numero_usuario, updated_at = CURRENT_TIMESTAMP""", (profile.user_id, profile.numero_usuario, profile_json) ) except Exception as e: # Fallback: check if exists, then update or insert if "UNIQUE constraint failed" in str(e): check_result = self.db._execute_with_retry( "SELECT id FROM user_emotional_profiles WHERE user_id = ?", (profile.user_id,) ) if check_result and len(check_result) > 0: # Update existing self.db._execute_with_retry( """UPDATE user_emotional_profiles SET profile_data = ?, numero_usuario = ?, updated_at = CURRENT_TIMESTAMP WHERE user_id = ?""", (profile_json, profile.numero_usuario, profile.user_id) ) else: # Insert new self.db._execute_with_retry( """INSERT INTO user_emotional_profiles (user_id, numero_usuario, profile_data, updated_at) VALUES (?, ?, ?, CURRENT_TIMESTAMP)""", (profile.user_id, profile.numero_usuario, profile_json) ) else: raise except Exception as e: logger.warning(f"⚠️ Erro ao salvar perfil emocional: {e}") def get_profile_stats(self, user_id: str) -> Dict[str, Any]: """Retorna estatísticas do perfil emocional.""" profile = self.get_or_create_profile(user_id) return { 'user_id': user_id, 'current_emotion': profile.current_emotion, 'hostility_level': profile.get_hostility_level(), 'has_rancor': profile.should_maintain_rancor(), 'marked_as_hostile': profile.marked_as_hostile, 'consecutive_aggressive': profile.consecutive_aggressive_interactions, 'emotion_history_count': len(profile.emotion_history), 'last_emotion_change': profile.last_emotion_change, } # ============================================================ # SINGLETON INSTANCE # ============================================================ _emotional_profile_manager: Optional[EmotionalProfileManager] = None def get_emotional_profile_manager() -> EmotionalProfileManager: """ Retorna instância singleton do EmotionalProfileManager. """ global _emotional_profile_manager if _emotional_profile_manager is None: _emotional_profile_manager = EmotionalProfileManager() return _emotional_profile_manager