Spaces:
Running
Running
| # 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 | |
| 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 | |
| } | |
| 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) | |
| ) | |
| def is_reply(self) -> bool: | |
| """Verifica se é um reply.""" | |
| return bool(self.reply_info) and self.reply_info.get("is_reply", False) | |
| 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 | |
| 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 | |