# type: ignore """ ================================================================================ AKIRA V21 ULTIMATE - SHORT-TERM MEMORY MODULE ================================================================================ Sistema de memória de curto prazo com sliding window de 100 mensagens. Prioriza contexto de replies e ajusta importância dinamicamente. Features: - Sliding window de 100 mensagens por usuário - Priorização automática de replies (importancia > 1.0) - Perguntas curtas com reply ganham prioridade ainda maior - Serialização JSON para persistência - Peso adaptativo baseado em análise de conteúdo ================================================================================ """ import os import sys import time import json import re import logging from pathlib import Path from typing import Optional, Dict, Any, List, Tuple from dataclasses import dataclass, field from collections import deque from datetime import datetime # Imports robustos com fallback - CORRIGIDO para usar modules. try: import modules.config as config SHORT_TERM_MEMORY_AVAILABLE = True except ImportError: try: from . import config SHORT_TERM_MEMORY_AVAILABLE = True except ImportError: SHORT_TERM_MEMORY_AVAILABLE = False config = None logger = logging.getLogger(__name__) # ============================================================ # CONFIGURAÇÃO # ============================================================ # Máximo de mensagens na memória de curto prazo (100 conforme usuário) MAX_SHORT_TERM_MESSAGES: int = 100 # Multiplicadores de importância IMPORTANCIA_NORMAL: float = 1.0 IMPORTANCIA_REPLY: float = 1.3 IMPORTANCIA_REPLY_TO_BOT: float = 1.5 IMPORTANCIA_PERGUNTA_CURTA_REPLY: float = 1.7 # Prioridade máxima # Limite de palavras para considerar "pergunta curta" PERGUNTA_CURTA_LIMITE: int = 5 @dataclass class MessageWithContext: """ Mensagem com metadados de contexto completo. Attributes: role: "user" ou "assistant" content: Texto da mensagem timestamp: Timestamp da mensagem importancia: Peso de importância (1.0 = normal, >1.0 = replies) emocao: Emoção detectada reply_info: Info sobre reply (se aplicável) conversation_id: ID da conversa isolada token_count: Contagem aproximada de tokens """ role: str content: str timestamp: float = field(default_factory=time.time) importancia: float = 1.0 emocao: str = "neutral" reply_info: Dict[str, Any] = field(default_factory=dict) conversation_id: str = "" token_count: int = 0 def to_dict(self) -> Dict[str, Any]: """Converte para dicionário.""" return { "role": self.role, "content": self.content, "timestamp": self.timestamp, "importancia": self.importancia, "emocao": self.emocao, "reply_info": self.reply_info, "conversation_id": self.conversation_id, "token_count": self.token_count } @classmethod def from_dict(cls, data: Dict[str, Any]) -> 'MessageWithContext': """Cria instância a partir de dicionário.""" return cls( role=data.get("role", "user"), content=data.get("content", ""), timestamp=data.get("timestamp", time.time()), importancia=data.get("importancia", 1.0), emocao=data.get("emocao", "neutral"), reply_info=data.get("reply_info", {}), conversation_id=data.get("conversation_id", ""), token_count=data.get("token_count", 0) ) @property def is_reply(self) -> bool: """Verifica se é um reply.""" return bool(self.reply_info) and self.reply_info.get("is_reply", False) @property def is_reply_to_bot(self) -> bool: """Verifica se é reply direcionado ao bot.""" return self.reply_info.get("reply_to_bot", False) # ============================================================ # FUNÇÕES AUXILIARES # ============================================================ def contar_palavras(texto: str) -> int: """Conta palavras em um texto.""" if not texto: return 0 return len(texto.split()) def estimar_tokens(texto: str) -> int: """ Estima número de tokens (aproximação粗糙). Média de 4 caracteres por token em português. """ if not texto: return 0 return max(1, len(texto) // 4) def is_pergunta_curta(texto: str) -> bool: """ Verifica se o texto é uma pergunta curta. Args: texto: Texto a verificar Returns: True se for pergunta com poucas palavras """ if not texto: return False texto_lower = texto.strip().lower() # Deve ter marcador de pergunta ou palavras interrogativas has_question_marker = '?' in texto or '?' in texto has_interrogative = any(w in texto_lower for w in [ 'qual', 'quais', 'quem', 'como', 'onde', 'quando', 'por que', 'porque', 'para que', 'o que', 'que', 'é o que' ]) word_count = contar_palavras(texto) # Pergunta curta: até N palavras E (marcador ? OU palavra interrogativa) return word_count <= PERGUNTA_CURTA_LIMITE and (has_question_marker or has_interrogative) def calcular_importancia( is_reply: bool = False, reply_to_bot: bool = False, mensagem: str = "", emocao: str = "neutral" ) -> float: """ Calcula importância da mensagem baseada em múltiplos fatores. Args: is_reply: Se é um reply reply_to_bot: Se é reply para o bot mensagem: Texto da mensagem emocao: Emoção detectada Returns: Float de importância (1.0 = normal, >1.0 = prioritário) """ importancia = IMPORTANCIA_NORMAL # Reply para o bot tem maior prioridade if is_reply and reply_to_bot: importancia = IMPORTANCIA_REPLY_TO_BOT # Pergunta curta com reply ao bot = prioridade máxima if is_pergunta_curta(mensagem): importancia = IMPORTANCIA_PERGUNTA_CURTA_REPLY # Reply normal elif is_reply: importancia = IMPORTANCIA_REPLY # Emoção intensa pode aumentar importância emocoes_intensas = ['joy', 'love', 'anger', 'fear'] if emocao in emocoes_intensas: importancia *= 1.1 return importancia # ============================================================ # CLASSE PRINCIPAL DE MEMÓRIA DE CURTO PRAZO # ============================================================ class ShortTermMemory: """ Sistema de memória de curto prazo com sliding window. Características: - Mantém últimas N mensagens (100 por padrão) - Auto-reorganização por importância - Persistência JSON - Integração com ReplyContextHandler - Token budgeting para contexto LLM """ def __init__( self, conversation_id: str = "", max_messages: int = MAX_SHORT_TERM_MESSAGES, context_data: Optional[Dict[str, Any]] = None ): """ Inicializa memória de curto prazo. Args: conversation_id: ID da conversa isolada max_messages: Máximo de mensagens (padrão 100) context_data: Dados para restauração (opcional) """ self.conversation_id = conversation_id self.max_messages = max_messages # Deque para O(1) em operações de borda self._messages: deque = deque(maxlen=max_messages) # Cache para rápido acesso self._replies_cache: List[MessageWithContext] = [] self._last_update: float = time.time() # Carrega dados se fornecidos if context_data and isinstance(context_data, dict): self._from_dict(context_data) else: self._initialize_empty() logger.debug(f"🧠 ShortTermMemory initialized: {conversation_id or 'temp'} | {len(self._messages)} msgs") def _initialize_empty(self): """Inicializa estrutura vazia.""" self._messages = deque(maxlen=self.max_messages) self._replies_cache = [] self._last_update = time.time() # ============================================================ # ADIÇÃO DE MENSAGENS # ============================================================ def add_message( self, role: str, content: str, importancia: float = IMPORTANCIA_NORMAL, emocao: str = "neutral", reply_info: Optional[Dict[str, Any]] = None, metadata: Optional[Dict[str, Any]] = None ) -> MessageWithContext: """ Adiciona mensagem à memória. Args: role: "user" ou "assistant" content: Texto da mensagem importancia: Peso de importância emocao: Emoção detectada reply_info: Info de reply (se aplicável) metadata: Metadados adicionais Returns: MessageWithContext criada """ # Cria mensagem com contexto msg = MessageWithContext( role=role, content=content, importancia=importancia, emocao=emocao, reply_info=reply_info or {}, conversation_id=self.conversation_id, token_count=estimar_tokens(content) ) # Adiciona metadados extras if metadata: msg_data = msg.to_dict() msg_data.update(metadata) msg = MessageWithContext.from_dict(msg_data) # Adiciona ao deque self._messages.append(msg) self._last_update = time.time() # Atualiza cache de replies if msg.is_reply: self._replies_cache.append(msg) # Limita cache de replies if len(self._replies_cache) > 20: self._replies_cache = self._replies_cache[-20:] return msg def add_user_message( self, content: str, emocao: str = "neutral", reply_info: Optional[Dict[str, Any]] = None, importancia: float = None ) -> MessageWithContext: """ Adiciona mensagem do usuário. Args: content: Texto da mensagem emocao: Emoção detectada reply_info: Info de reply importancia: Importância customizada (calculada automaticamente se None) Returns: MessageWithContext criada """ if importancia is None: importancia = calcular_importancia( is_reply=bool(reply_info and reply_info.get("is_reply")), reply_to_bot=bool(reply_info and reply_info.get("reply_to_bot")), mensagem=content, emocao=emocao ) return self.add_message( role="user", content=content, importancia=importancia, emocao=emocao, reply_info=reply_info ) def add_assistant_message( self, content: str, emocao: str = "neutral", importancia: float = IMPORTANCIA_NORMAL ) -> MessageWithContext: """ Adiciona mensagem do assistente (bot). Args: content: Texto da resposta emocao: Emoção da resposta importancia: Importância Returns: MessageWithContext criada """ return self.add_message( role="assistant", content=content, importancia=importancia, emocao=emocao ) # ============================================================ # RECUPERAÇÃO DE CONTEXTO # ============================================================ def get_context_window( self, include_replies: bool = True, prioritize_replies: bool = True, max_messages: Optional[int] = None, max_tokens: int = 8000 ) -> List[MessageWithContext]: """ Obtém janela de contexto otimizada para LLM. Args: include_replies: Se deve incluir replies prioritize_replies: Se deve priorizar replies max_messages: Máximo de mensagens (usa config se None) max_tokens: Limite de tokens Returns: Lista de mensagens ordenadas """ messages = list(self._messages) if not messages: return [] # Filtra replies se necessário if not include_replies: messages = [m for m in messages if not m.is_reply] # Reorganiza por importância se solicitado if prioritize_replies: messages.sort(key=lambda m: m.importancia, reverse=True) # Aplica limite de mensagens if max_messages and len(messages) > max_messages: messages = messages[:max_messages] # Aplica limite de tokens if max_tokens > 0: tokens_accumulated = 0 result = [] for msg in messages: if tokens_accumulated + msg.token_count <= max_tokens: result.append(msg) tokens_accumulated += msg.token_count else: break messages = result return messages def get_messages(self, conversation_id: str = "", limit: int = 10) -> List[MessageWithContext]: """Alias para get_last_n_messages (compatibilidade PersonaTracker e UnifiedContext).""" return self.get_last_n_messages(limit) def get_context(self, **kwargs) -> List[MessageWithContext]: """Alias para get_context_window.""" return self.get_context_window(**kwargs) def get_last_n_messages(self, n: int) -> List[MessageWithContext]: """ Obtém últimas N mensagens (ordem cronológica). Args: n: Número de mensagens Returns: Lista das últimas N mensagens """ return list(self._messages)[-n:] def get_recent_replies( self, n: int = 5, include_reply_to_bot: bool = True ) -> List[MessageWithContext]: """ Obtém replies mais recentes. Args: n: Número de replies a retornar include_reply_to_bot: Se inclui replies ao bot Returns: Lista de replies ordenados por timestamp """ replies = [m for m in self._messages if m.is_reply] if not include_reply_to_bot: replies = [m for m in replies if not m.is_reply_to_bot] # Retorna mais recentes primeiro return replies[-n:][::-1] def get_all_messages(self) -> List[MessageWithContext]: """Retorna todas as mensagens.""" return list(self._messages) def get_messages_for_llm( self, reply_context: Optional[MessageWithContext] = None, max_tokens: int = 6000 ) -> List[Dict[str, str]]: """ Obtém mensagens formatadas para LLM. Args: reply_context: Contexto de reply atual (terá prioridade) max_tokens: Limite de tokens Returns: Lista de dicts com role e content """ messages = self.get_context_window( include_replies=True, prioritize_replies=True, max_tokens=max_tokens ) # Se há reply_context, coloca no início if reply_context: # Garante que reply_context está na lista ou adiciona reply_msg = MessageWithContext( role="user", content=f"[REPLY CONTEXT] {reply_context.content}", importancia=IMPORTANCIA_PERGUNTA_CURTA_REPLY, reply_info=reply_context.reply_info ) # Remove duplicata se existir messages = [m for m in messages if not ( m.is_reply and m.reply_info.get("quoted_text_original") == reply_context.reply_info.get("quoted_text_original") )] # Adiciona reply no início messages.insert(0, reply_msg) # Formata para LLM return [ {"role": msg.role, "content": msg.content} for msg in messages ] # ============================================================ # ANÁLISE DE CONTEXTO # ============================================================ def get_conversation_summary(self) -> Dict[str, Any]: """ Gera resumo estatístico da conversa. Returns: Dicionário com estatísticas """ messages = list(self._messages) if not messages: return { "total_messages": 0, "user_messages": 0, "assistant_messages": 0, "replies_count": 0, "emocoes": {}, "avg_importancia": 1.0, "token_count": 0, "duration_seconds": 0 } user_msgs = [m for m in messages if m.role == "user"] assistant_msgs = [m for m in messages if m.role == "assistant"] replies = [m for m in messages if m.is_reply] # Contagem de emoções emocoes = {} for m in messages: emocao = m.emocao or "neutral" emocoes[emocao] = emocoes.get(emocao, 0) + 1 # Duração timestamps = [m.timestamp for m in messages] duration = max(timestamps) - min(timestamps) if len(timestamps) > 1 else 0 return { "total_messages": len(messages), "user_messages": len(user_msgs), "assistant_messages": len(assistant_msgs), "replies_count": len(replies), "emocoes": emocoes, "avg_importancia": sum(m.importancia for m in messages) / max(1, len(messages)), "token_count": sum(m.token_count for m in messages), "duration_seconds": duration, "is_full": len(messages) >= self.max_messages } def get_emotional_trend(self) -> str: """Retorna tendência emocional da conversa.""" messages = list(self._messages) if not messages: return "neutral" # Pesos mais recentes têm mais importância emocoes = {} total_weight = 0 for i, msg in enumerate(reversed(messages)): weight = 1.0 + (i * 0.05) #_msgs recentes pesam mais emocao = msg.emocao or "neutral" emocoes[emocao] = emocoes.get(emocao, 0) + weight total_weight += weight # Normaliza for e in emocoes: emocoes[e] /= total_weight return max(emocoes, key=emocoes.get) if emocoes else "neutral" # type: ignore # ============================================================ # PERSISTÊNCIA # ============================================================ def to_dict(self) -> Dict[str, Any]: """Serializa para dicionário.""" return { "conversation_id": self.conversation_id, "max_messages": self.max_messages, "messages": [m.to_dict() for m in self._messages], "last_update": self._last_update } def _from_dict(self, data: Dict[str, Any]): """Desserializa de dicionário.""" self.conversation_id = data.get("conversation_id", "") self.max_messages = data.get("max_messages", MAX_SHORT_TERM_MESSAGES) self._last_update = data.get("last_update", time.time()) messages_data = data.get("messages", []) self._messages = deque(maxlen=self.max_messages) self._replies_cache = [] for msg_data in messages_data: msg = MessageWithContext.from_dict(msg_data) self._messages.append(msg) if msg.is_reply: self._replies_cache.append(msg) def save_to_file(self, filepath: str) -> bool: """Salva memória em arquivo JSON.""" try: with open(filepath, 'w', encoding='utf-8') as f: json.dump(self.to_dict(), f, ensure_ascii=False, indent=2) return True except Exception as e: logger.warning(f"Erro ao salvar memória: {e}") return False @classmethod def load_from_file(cls, filepath: str) -> 'ShortTermMemory': """Carrega memória de arquivo JSON.""" try: with open(filepath, 'r', encoding='utf-8') as f: data = json.load(f) return cls(context_data=data) except Exception as e: logger.warning(f"Erro ao carregar memória: {e}") return cls() # ============================================================ # GESTÃO # ============================================================ def clear(self): """Limpa toda a memória.""" self._initialize_empty() logger.debug(f"🧠 ShortTermMemory cleared: {self.conversation_id or 'temp'}") def merge_from(self, other: 'ShortTermMemory') -> None: """ Mescla mensagens de outra memória. Útil para migração de dados. Args: other: Outra ShortTermMemory """ for msg in other.get_all_messages(): # Mantém conversation_id original msg_data = msg.to_dict() msg_data["conversation_id"] = self.conversation_id new_msg = MessageWithContext.from_dict(msg_data) self._messages.append(new_msg) self._last_update = time.time() def __len__(self) -> int: """Retorna número de mensagens.""" return len(self._messages) def __bool__(self) -> bool: """Retorna True se há mensagens.""" return len(self._messages) > 0 def __iter__(self): """Iterador sobre mensagens.""" return iter(self._messages) def __repr__(self) -> str: """Representação textual.""" return f"ShortTermMemory(id={self.conversation_id[:8] if self.conversation_id else 'temp'}, msgs={len(self)})" # ============================================================ # FUNÇÕES DE FÁBRICA # ============================================================ def criar_short_term_memory( conversation_id: str = "", max_messages: int = MAX_SHORT_TERM_MESSAGES ) -> ShortTermMemory: """ Factory function para criar ShortTermMemory. Args: conversation_id: ID da conversa max_messages: Máximo de mensagens Returns: ShortTermMemory instance """ return ShortTermMemory(conversation_id=conversation_id, max_messages=max_messages) def calcular_importancia_automatica( mensagem: str, is_reply: bool = False, reply_to_bot: bool = False, emocao: str = "neutral" ) -> float: """ Wrapper para calcular_importancia com todos os parâmetros. Args: mensagem: Texto da mensagem is_reply: Se é reply reply_to_bot: Se é reply para o bot emocao: Emoção detectada Returns: Float de importância """ return calcular_importancia(is_reply, reply_to_bot, mensagem, emocao) # ============================================================ # COMPATIBILIDADE — aliases para imports legados # ============================================================ ShortTermMemoryManager = ShortTermMemory # type: ignore