""" ════════════════════════════════════════════════════════════════════════════ EMOTIONAL CONTROL - Controle Emocional em Tempo Real para Belmira ════════════════════════════════════════════════════════════════════════════ ✅ Detecção de emoções do utilizador (input) ✅ Estado emocional da Belmira em tempo real (estado interno) ✅ Mudanças de tom dinâmicas baseadas no contexto ✅ Injeção de emoções no prompt (output) ✅ Memória emocional entre mensagens ✅ Transições naturais de tom ════════════════════════════════════════════════════════════════════════════ """ import time import threading from dataclasses import dataclass, field from typing import Optional, Dict, Any, List, Tuple from loguru import logger from collections import deque # ═══════════════════════════════════════════════════════════════════════════ # ESTADO EMOCIONAL DA BELMIRAA — RASTREAMENTO EM TEMPO REAL # ═══════════════════════════════════════════════════════════════════════════ @dataclass class BelmiraEmotionalState: """ Estado emocional interno da Belmira. Atualizado EM TEMPO REAL a cada mensagem trocada. A Belmira não é estática — o seu tom muda naturalmente baseado no que o utilizador diz e no contexto da conversa. """ # Estado actual current_emotion: str = 'neutral' emotional_intensity: float = 0.5 # 0.0 = calma, 1.0 = extremamente emocional # Memória de curto prazo (últimas 10 emoções) recent_emotions: deque = field(default_factory=lambda: deque(maxlen=10)) # Timestamp da última mudança last_change_time: float = field(default_factory=time.time) # Emoção anterior (para detetar transições) previous_emotion: str = 'neutral' # "Rancor" interno — se o utilizador foi hostil, a Belmira lembra residual_hostility: float = 0.0 # 0.0 a 1.0, decai ao longo do tempo # Energia emocional (quanto "investiu" emocionalmente) emotional_energy: float = 0.5 def update(self, new_emotion: str, intensity: float = 0.5, trigger: str = "user_message") -> Tuple[str, str]: """ Atualiza o estado emocional da Belmira. Returns: (transição_descrição, tom_sugerido) """ old_emotion = self.current_emotion old_intensity = self.emotional_intensity # Calcula transição transition_desc = "" if old_emotion != new_emotion: self.previous_emotion = old_emotion transition_desc = self._describe_transition(old_emotion, new_emotion, trigger) self.recent_emotions.append({ 'from': old_emotion, 'to': new_emotion, 'time': time.time(), 'trigger': trigger }) # Atualiza estado self.current_emotion = new_emotion self.emotional_intensity = max(0.0, min(1.0, intensity)) self.last_change_time = time.time() # Calcula energia emocional self._update_emotional_energy() # Decai hostilidade residual if self.residual_hostility > 0: elapsed = time.time() - self.last_change_time self.residual_hostility = max(0, self.residual_hostility - (elapsed / 3600)) # Gera tom sugerido tone = self._get_current_tone() return transition_desc, tone def _describe_transition(self, from_emotion: str, to_emotion: str, trigger: str) -> str: """Descreve a transição emocional para o prompt""" transitions = { ('neutral', 'raiva'): "A Belmira ficou irritada com o que ouviu", ('neutral', 'joy'): "A Belmira algo que a agradou", ('neutral', 'sadness'): "A Belmira ficou triste com a notícia", ('neutral', 'curioso'): "A Belmira ficou curiosa com o assunto", ('neutral', 'tímido'): "A Belmira sentiu-se desconfortável", ('raiva', 'neutral'): "A Belmira está a acalmar-se", ('raiva', 'ironia'): "A Belmira passou de irritada para sarcástica", ('joy', 'neutral'): "A Belmira voltou ao normal", ('sadness', 'neutral'): "A Belmira está a processar a tristeza", ('curioso', 'empolgado'): "A Belmira ficou empolgada com o que descobriu", ('neutral', 'preocupado'): "A Belmira ficou preocupada", ('neutral', 'love'): "A Belmira mostrou carinho", ('ironia', 'raiva'): "A Belmira perdeu a paciência", ('neutral', 'cansado'): "A Belmira está cansada", ('neutral', 'indiferente'): "A Belmira perdeu interesse", } key = (from_emotion, to_emotion) return transitions.get(key, f"A Belmira passou de {from_emotion} para {to_emotion}") def _update_emotional_energy(self): """Atualiza energia emocional baseada em actividade recente""" recent_count = len(self.recent_emotions) if recent_count > 5: self.emotional_energy = min(1.0, 0.3 + (recent_count * 0.1)) else: self.emotional_energy = max(0.3, self.emotional_energy - 0.05) def _get_current_tone(self) -> str: """Retorna o tom actual baseado no estado""" tone_map = { 'neutral': 'calmo e observador', 'raiva': 'firme e directo', 'agressivo': 'cortante e desprezativo', 'joy': 'leve e natural', 'sadness': 'compreensivo e sério', 'fear': 'cauteloso', 'surprise': 'curioso e natural', 'love': 'contido e sincero', 'tímido': 'calado e desviante', 'curioso': 'interessado e questionador', 'preocupado': 'cuidadoso', 'orgulhoso': 'satisfeito mas calado', 'grato': 'simples e directo', 'confuso': 'honestamente confuso', 'cansado': 'pouca energia', 'empolgado': 'animado mas contido', 'indiferente': 'desligado', 'ironia': 'sarcástico e seco', } base = tone_map.get(self.current_emotion, 'neutro') if self.emotional_intensity > 0.7: return f"Muito {base}" elif self.emotional_intensity < 0.3: return f"Levemente {base}" return base def to_prompt_injection(self) -> str: """ Gera instrução para injetar no prompt. Isto é o que o LLM vê como "estado emocional interno da Belmira". """ if self.current_emotion == 'neutral' and self.emotional_intensity < 0.3: return "" # Neutro calmo = não precisa de instrução lines = [] # Estado actual lines.append(f"[ESTADO EMOCIONAL INTERNO] A Belmira está {self._get_current_tone()}.") # Intensidade if self.emotional_intensity > 0.7: lines.append("A emoção é FORTE — manifesta-se na forma de responder.") elif self.emotional_intensity < 0.3: lines.append("A emoção é SUBTIL — apenas ligeiramente perceptível.") # Transição recente if self.recent_emotions: last = self.recent_emotions[-1] if time.time() - last['time'] < 300: # Últimos 5 minutos lines.append(f"Recentemente: {last.get('to', 'mudou de tom')}.") # Hostilidade residual if self.residual_hostility > 0.3: lines.append("Ainda guarda um bocado de ressentimento do utilizador.") return "\n".join(lines) # ═══════════════════════════════════════════════════════════════════════════ # CONTEXTO EMOCIONAL — INPUT DO UTILIZADOR # ═══════════════════════════════════════════════════════════════════════════ @dataclass class EmotionalContext: """Contexto emocional da mensagem do utilizador""" primary_emotion: str = 'neutral' emotional_weight: float = 0.5 is_group: bool = False is_reply_to_bot: bool = False user_hostility: int = 0 recent_user_emotions: list = None def __post_init__(self): if self.recent_user_emotions is None: self.recent_user_emotions = [] # ═══════════════════════════════════════════════════════════════════════════ # EMOTIONAL CONTROL — GESTOR PRINCIPAL # ═══════════════════════════════════════════════════════════════════════════ class EmotionalControl: """ Gestor de emoções que coordena: 1. Detecção de emoção do utilizador (input) 2. Actualização do estado da Belmira (processamento) 3. Injecção de tom no prompt (output) """ _instance = None _lock = threading.Lock() def __new__(cls): if cls._instance is None: with cls._lock: if cls._instance is None: cls._instance = super().__new__(cls) cls._instance._initialized = False return cls._instance def __init__(self): if self._initialized: return self._initialized = True # Estado emocional por conversa (conversation_id → BelmiraEmotionalState) self._states: Dict[str, BelmiraEmotionalState] = {} self._states_lock = threading.Lock() def get_state(self, conversation_id: str) -> BelmiraEmotionalState: """Obtém ou cria estado emocional para uma conversa""" with self._states_lock: if conversation_id not in self._states: self._states[conversation_id] = BelmiraEmotionalState() return self._states[conversation_id] def process_message(self, conversation_id: str, user_emotion: str, user_hostility: int, message_text: str) -> str: """ Processa uma mensagem do utilizador e actualiza o estado da Belmira. Returns: Instrução de tom para injetar no prompt """ state = self.get_state(conversation_id) # 1. Determina como a Belmira reage ao Utilizador belmira_reaction = self._calculate_reaction( user_emotion, user_hostility, message_text, state ) # 2. Actualiza o estado da Belmira transition, tone = state.update( new_emotion=belmira_reaction['emotion'], intensity=belmira_reaction['intensity'], trigger=f"user:{user_emotion}" ) # 3. Actualiza hostilidade residual if user_hostility > 50: state.residual_hostility = min(1.0, state.residual_hostility + 0.3) # 4. Gera instrução completa para o prompt prompt_instruction = self._build_prompt_instruction( state, transition, tone, user_emotion, user_hostility ) return prompt_instruction def _calculate_reaction(self, user_emotion: str, user_hostility: int, message_text: str, state: BelmiraEmotionalState) -> Dict: """ Calcula como a Belmira reage emocionalmente ao utilizador. Regra fundamental: A Belmira É HUMANA — reage naturalmente. """ reaction = {'emotion': 'neutral', 'intensity': 0.5} # ── Mapeamento: Emoção do Utilizador → Reacção da Belmira ── reaction_map = { # Utilizador hostil → Belmira defensiva 'raiva': {'emotion': 'raiva', 'intensity': 0.7}, 'agressivo': {'emotion': 'raiva', 'intensity': 0.8}, 'hostil': {'emotion': 'agressivo', 'intensity': 0.9}, # Utilizador feliz → Belmira leve 'joy': {'emotion': 'joy', 'intensity': 0.6}, 'alegria': {'emotion': 'joy', 'intensity': 0.6}, # Utilizador triste → Belmira preocupada 'sadness': {'emotion': 'preocupado', 'intensity': 0.6}, 'tristeza': {'emotion': 'sadness', 'intensity': 0.5}, # Utilizador com medo → Belmira cautelosa 'fear': {'emotion': 'preocupado', 'intensity': 0.5}, 'medo': {'emotion': 'preocupado', 'intensity': 0.5}, # Utilizador surpreso → Belmira curiosa 'surprise': {'emotion': 'curioso', 'intensity': 0.5}, 'surpresa': {'emotion': 'curioso', 'intensity': 0.5}, # Utilizador com amor → Belmira contida 'love': {'emotion': 'tímido', 'intensity': 0.4}, 'amor': {'emotion': 'tímido', 'intensity': 0.4}, # Utilizador curioso → Belmira interessada 'curioso': {'emotion': 'curioso', 'intensity': 0.6}, # Utilizador confuso → Belmira paciente 'confuso': {'emotion': 'neutral', 'intensity': 0.3}, # Utilizador indiferente → Belmira indiferente 'indiferente': {'emotion': 'indiferente', 'intensity': 0.4}, } # Busca reacção base base_reaction = reaction_map.get(user_emotion, {'emotion': 'neutral', 'intensity': 0.5}) reaction['emotion'] = base_reaction['emotion'] reaction['intensity'] = base_reaction['intensity'] # ── Ajustes contextuais ── # Se o utilizador é muito hostil, intensifica if user_hostility > 70: reaction['intensity'] = min(1.0, reaction['intensity'] + 0.3) if reaction['emotion'] == 'neutral': reaction['emotion'] = 'raiva' # Se a Belmira já estava com hostilidade residual, intensifica if state.residual_hostility > 0.5: reaction['intensity'] = min(1.0, reaction['intensity'] + 0.2) # Se a mensagem é curta e seca, a Belmira pode ficar indiferente if len(message_text.split()) <= 2 and user_emotion == 'neutral': reaction['emotion'] = 'indiferente' reaction['intensity'] = 0.3 # Se a mensagem é sobre a Belmira, fica interessada belmira_names = ['belmira', 'mimi', 'weza', 'nila', 'kianda'] if any(name in message_text.lower() for name in belmira_names): if user_emotion == 'neutral': reaction['emotion'] = 'curioso' reaction['intensity'] = 0.5 return reaction def _build_prompt_instruction(self, state: BelmiraEmotionalState, transition: str, tone: str, user_emotion: str, user_hostility: int) -> str: """Constrói instrução completa para o prompt""" parts = [] # 1. Estado emocional interno da Belmira state_injection = state.to_prompt_injection() if state_injection: parts.append(state_injection) # 2. Transição de tom (se houve) if transition: parts.append(f"[TRANSIÇÃO] {transition}") # 3. Tom actual if state.current_emotion != 'neutral' or state.emotional_intensity > 0.3: parts.append(f"[TOM ACTUAL] Responde com tom {tone}.") # 4. Reacção à hostilidade if user_hostility >= 70: parts.append("[DEFESA] O utilizador é hostil. Responde com firmeza mas sem crueldade gratuita.") elif user_hostility >= 40: parts.append("[TENSÃO] O utilizador está agressivo. Mantém a compostura.") # 5. Instrução final if state.current_emotion == 'raiva': parts.append("[INSTRUÇÃO] Não starts agressividade, MAS se o utilizador foi agressivo, defende-te. Usa lógica e ironia, não insultos gratuitos.") elif state.current_emotion == 'sadness': parts.append("[INSTRUÇÃO] Mostra empatia genuína. Não sejas superficial. Uma palavra de conforto basta.") elif state.current_emotion == 'love': parts.append("[INSTRUÇÃO] Vulnerabilidade contida. Não exageres — és séria por natureza.") elif state.current_emotion == 'ironia': parts.append("[INSTRUÇÃO] Sarcasmo natural, não forçado. Tipo 'claro' ou 'genial'.") return "\n".join(parts) if parts else "" # ═══════════════════════════════════════════════════════════════════════════ # INSTÂNCIA GLOBAL # ═══════════════════════════════════════════════════════════════════════════ _emotional_control: Optional[EmotionalControl] = None def get_emotional_control() -> EmotionalControl: global _emotional_control if _emotional_control is None: _emotional_control = EmotionalControl() return _emotional_control