Spaces:
Running
Running
| # type: ignore | |
| """ | |
| ================================================================================ | |
| AKIRA V21 ULTIMATE - LSTM MEMORY SYSTEM (MENTAL CONTEXT LAYER) | |
| ================================================================================ | |
| Sistema de memória LSTM (Long Short-Term Memory) que funciona completamente | |
| transparente. Resumos mentais ocultos, contexto dual (direto + histórico), | |
| isolamento total por usuário/grupo, integração com PersonaTracker. | |
| Features: | |
| - Resumos mentais ocultos (LSTM Virtual via embeddings + summarization) | |
| - Contexto Dual: Direto (reply atual) + Geral (LSTM histórico) | |
| - Armazenamento em DB com recuperação automática | |
| - Isolamento total por contexto (PV vs Grupos) | |
| - Integração com PersonaTracker para perfil dinâmico | |
| - Sem exposição ao usuário (100% mental) | |
| - Recuperação automática quando modelo precisa | |
| Arquitetura: | |
| 1. Mensagem entra → short_term_memory (100 mensagens) | |
| 2. LSTM processa silenciosamente → creates mental summary | |
| 3. Summary armazenado em DB (lstm_contexto table) | |
| 4. Quando model precisa contexto → recupera automaticamente via SQL | |
| 5. PersonaTracker atualiza perfil baseado em LSTM | |
| Example (User Doesn't See This): | |
| User: "Fale tudo sobre anemia falciforme" | |
| [LSTM MENTAL SUMMARY - HIDDEN]: | |
| topic: "anemia falciforme", | |
| subtopics: ["genética", "hemoglobina", "sangue"], | |
| conversation_path: ["introdução", "definição"], | |
| last_context: "aguardando pergunta sobre cura/tratamento" | |
| User: "cura? tratamento?" | |
| [LSTM SEARCHES CONTEXT]: | |
| ✓ Topic detected: "anemia falciforme" (from mental summary) | |
| ✓ Context understood: Pergunta é sobre a doença anterior | |
| ✓ Model responde naturalmente com contexto correto | |
| ================================================================================ | |
| """ | |
| import os | |
| import sys | |
| import json | |
| import time | |
| import threading | |
| import hashlib | |
| import sqlite3 | |
| import logging | |
| from pathlib import Path | |
| from typing import Optional, Dict, Any, List, Tuple | |
| from dataclasses import dataclass, field, asdict | |
| from datetime import datetime, timedelta | |
| from collections import defaultdict | |
| import re | |
| # Imports robustos com fallback | |
| try: | |
| from . import config | |
| from .database import Database | |
| from .context_isolation import ContextIsolationManager, ConversationContext | |
| from .short_term_memory import ShortTermMemory, MessageWithContext | |
| LSTM_MEMORY_AVAILABLE = True | |
| except ImportError: | |
| try: | |
| import modules.config as config | |
| from modules.database import Database | |
| from modules.context_isolation import ContextIsolationManager, ConversationContext | |
| from modules.short_term_memory import ShortTermMemory, MessageWithContext | |
| LSTM_MEMORY_AVAILABLE = True | |
| except ImportError: | |
| LSTM_MEMORY_AVAILABLE = False | |
| config = None | |
| Database = None | |
| ContextIsolationManager = None | |
| logger = logging.getLogger(__name__) | |
| # ============================================================ | |
| # ESTRUTURA DE DADOS LSTM | |
| # ============================================================ | |
| class LSTMContextSummary: | |
| """ | |
| Resumo mental oculto de uma conversa (não visível para usuário). | |
| Armazenado em DB para recuperação automática. | |
| Attributes: | |
| context_id: ID do contexto (PV ou Grupo) | |
| numero_usuario: Número do usuário | |
| topic_principal: Tópico principal atual | |
| subtopicas: Lista de subtópicos discutidos | |
| conversation_path: Sequência de tópicos (histórico mental) | |
| last_key_message: Última mensagem-chave para retomada | |
| emotional_state: Estado emocional detectado | |
| interaction_pattern: Padrão de interação (perguntador, storyteller, etc) | |
| context_switches: Mudanças de contexto detectadas | |
| unanswered_questions: Perguntas não respondidas pendentes | |
| assumed_knowledge: Conhecimento que o usuário demonstra ter | |
| contradictions: Contradições ou mudanças de opinião | |
| created_at: Quando foi criado | |
| last_updated: Última atualização | |
| metadata: Dados adicionais | |
| """ | |
| context_id: str | |
| numero_usuario: str | |
| topic_principal: Optional[str] = None | |
| subtopicas: List[str] = field(default_factory=list) | |
| conversation_path: List[str] = field(default_factory=list) | |
| last_key_message: Optional[str] = None | |
| emotional_state: str = "neutral" | |
| interaction_pattern: str = "unknown" | |
| context_switches: int = 0 | |
| unanswered_questions: List[Dict[str, str]] = field(default_factory=list) | |
| assumed_knowledge: List[str] = field(default_factory=list) | |
| contradictions: List[Dict[str, Any]] = field(default_factory=list) | |
| created_at: float = field(default_factory=time.time) | |
| last_updated: float = field(default_factory=time.time) | |
| metadata: Dict[str, Any] = field(default_factory=dict) | |
| def to_dict(self) -> Dict[str, Any]: | |
| """Converte para dicionário serializável.""" | |
| return asdict(self) | |
| def to_json(self) -> str: | |
| """Converte para JSON.""" | |
| return json.dumps(self.to_dict(), ensure_ascii=False, default=str) | |
| def from_dict(cls, data: Dict[str, Any]) -> 'LSTMContextSummary': | |
| """Cria instância a partir de dicionário.""" | |
| return cls(**data) | |
| def from_json(cls, json_str: str) -> 'LSTMContextSummary': | |
| """Cria instância a partir de JSON.""" | |
| data = json.loads(json_str) | |
| return cls.from_dict(data) | |
| # ============================================================ | |
| # LSTM MEMORY SYSTEM - CORE | |
| # ============================================================ | |
| class LSTMMemorySystem: | |
| """ | |
| Sistema de memória LSTM que funciona completamente transparente. | |
| Responsabilidades: | |
| 1. Criar resumos mentais de conversas (sem exposição) | |
| 2. Manter contexto dual (direto + histórico) | |
| 3. Detectar tópicos, mudanças de contexto, perguntas pendentes | |
| 4. Armazenar em DB para recuperação automática | |
| 5. Integrar com isolamento de contexto | |
| 6. Permitir busca automática quando modelo precisa | |
| """ | |
| def __init__(self, db: Database, context_isolation: ContextIsolationManager): | |
| """ | |
| Args: | |
| db: Instance da Database | |
| context_isolation: Instance de ContextIsolationManager | |
| """ | |
| self.db = db | |
| self.context_isolation = context_isolation | |
| # Cache em memória de resumos LSTM (para rápido acesso) | |
| self.lstm_cache: Dict[str, LSTMContextSummary] = {} | |
| self.cache_lock = threading.Lock() | |
| # Queue de processamento assíncrono | |
| self.processing_queue: List[Dict[str, Any]] = [] | |
| self.processing_lock = threading.Lock() | |
| # ✅ PROTEÇÃO CONTRA DUPLICAÇÃO: Track mensagens processadas recentemente | |
| self.recently_processed: Dict[str, float] = {} # {hash(context+user+msg): timestamp} | |
| self.dedup_timeout = 5 # Segundos - evita duplicação em 5s | |
| # Inicializar tabelas no DB | |
| self._initialize_database() | |
| logger.info("✅ LSTM Memory System inicializado") | |
| def _initialize_database(self) -> None: | |
| """Cria tabelas necessárias no banco de dados.""" | |
| try: | |
| # As tabelas já são criadas pelo database.py _init_db(). | |
| # Aqui apenas garantimos redundância segura com o esquema oficial. | |
| self.db._execute_with_retry(""" | |
| CREATE TABLE IF NOT EXISTS lstm_contexto ( | |
| context_id VARCHAR(255) NOT NULL, | |
| numero_usuario VARCHAR(50) NOT NULL, | |
| topic_principal VARCHAR(255), | |
| subtopicas TEXT, | |
| conversation_path TEXT, | |
| last_key_message TEXT, | |
| emotional_state TEXT DEFAULT 'neutral', | |
| interaction_pattern TEXT DEFAULT 'unknown', | |
| context_switches INTEGER DEFAULT 0, | |
| unanswered_questions TEXT, | |
| assumed_knowledge TEXT, | |
| contradictions TEXT, | |
| created_at REAL, | |
| last_updated REAL, | |
| metadata TEXT, | |
| PRIMARY KEY (context_id, numero_usuario) | |
| ) | |
| """, commit=True) | |
| self.db._execute_with_retry(""" | |
| CREATE TABLE IF NOT EXISTS lstm_message_links ( | |
| id INTEGER PRIMARY KEY AUTOINCREMENT, | |
| context_id VARCHAR(255) NOT NULL, | |
| message_id VARCHAR(255) NOT NULL, | |
| numero_usuario VARCHAR(50) NOT NULL, | |
| parent_message_id VARCHAR(255), | |
| topic_changed BOOLEAN DEFAULT FALSE, | |
| context_switch_type VARCHAR(50), | |
| relevance_score FLOAT DEFAULT 1.0, | |
| created_at REAL, | |
| FOREIGN KEY (context_id, numero_usuario) REFERENCES lstm_contexto(context_id, numero_usuario) ON DELETE CASCADE | |
| ) | |
| """, commit=True) | |
| logger.info("✅ Tabelas LSTM sincronizadas") | |
| except Exception as e: | |
| logger.error(f"❌ Erro ao inicializar tabelas LSTM: {e}") | |
| # ======================================================== | |
| # CORE LSTM PROCESSING | |
| # ======================================================== | |
| def process_message_async( | |
| self, | |
| context_id: str, | |
| numero_usuario: str, | |
| message: str, | |
| role: str = "user", | |
| parent_message_id: Optional[str] = None, | |
| llm_client: Optional[Any] = None | |
| ) -> None: | |
| """ | |
| Processa mensagem de forma assíncrona para extrair contexto LSTM. | |
| Não bloqueia a resposta. Funciona em background thread. | |
| ✅ Proteção: Evita duplicação em 5 segundos | |
| Args: | |
| context_id: ID do contexto (PV ou Grupo) | |
| numero_usuario: ID do usuário | |
| message: Conteúdo da mensagem | |
| role: "user" ou "assistant" | |
| parent_message_id: ID da mensagem anterior (para linked context) | |
| llm_client: Client LLM para análise (opcional) | |
| """ | |
| # ✅ DEDUPLICATION: Verifica se a mensagem já foi processada recentemente | |
| import hashlib | |
| if message_id: | |
| msg_hash = hashlib.md5(f"msgid:{message_id}".encode()).hexdigest() | |
| else: | |
| msg_hash = hashlib.md5(f"{context_id}:{numero_usuario}:{message[:100]}".encode()).hexdigest() | |
| now = time.time() | |
| # Limpa entries expiradas | |
| expired = [k for k, v in self.recently_processed.items() if now - v > self.dedup_timeout] | |
| for k in expired: | |
| del self.recently_processed[k] | |
| # Verifica se já foi processada recentemente | |
| if msg_hash in self.recently_processed: | |
| logger.debug(f"⚠️ [LSTM DEDUP] Mensagem duplicada ignorada: {message[:50]}...") | |
| return | |
| # Marca como processada | |
| self.recently_processed[msg_hash] = now | |
| # Adiciona à queue para processamento assíncrono | |
| with self.processing_lock: | |
| self.processing_queue.append({ | |
| 'context_id': context_id, | |
| 'numero_usuario': numero_usuario, | |
| 'message': message, | |
| 'role': role, | |
| 'parent_message_id': parent_message_id, | |
| 'timestamp': now | |
| }) | |
| # Dispara thread de processamento se não estiver rodando | |
| if not hasattr(self, '_processing_thread_active'): | |
| self._start_processing_thread(llm_client) | |
| def _start_processing_thread(self, llm_client: Optional[Any] = None) -> None: | |
| """Inicia thread de processamento assíncrono.""" | |
| def process_worker(): | |
| while True: | |
| with self.processing_lock: | |
| if not self.processing_queue: | |
| break | |
| item = self.processing_queue.pop(0) | |
| try: | |
| self._process_message_internal(item, llm_client) | |
| except Exception as e: | |
| logger.warning(f"⚠️ Erro ao processar LSTM: {e}") | |
| thread = threading.Thread(target=process_worker, daemon=True) | |
| thread.start() | |
| def _process_message_internal( | |
| self, | |
| item: Dict[str, Any], | |
| llm_client: Optional[Any] = None | |
| ) -> None: | |
| """ | |
| Processa mensagem internamente. | |
| Extrai tema, contexto, perguntas, etc. | |
| """ | |
| context_id = item['context_id'] | |
| numero_usuario = item['numero_usuario'] | |
| message = item['message'] | |
| role = item['role'] | |
| parent_message_id = item.get('parent_message_id') | |
| # Recuperar ou criar resumo LSTM | |
| lstm_summary = self._get_or_create_lstm_summary(context_id, numero_usuario) | |
| # ✅ ANÁLISE 1: Detectar tópico principal | |
| new_topic = self._extract_topic(message) | |
| # ✅ ANÁLISE 2: Detectar mudança de contexto | |
| if new_topic and lstm_summary.topic_principal != new_topic: | |
| lstm_summary.context_switches += 1 | |
| lstm_summary.conversation_path.append(new_topic) | |
| lstm_summary.topic_principal = new_topic | |
| # Armazenar link entre mensagens | |
| self._record_context_switch(context_id, numero_usuario, parent_message_id, new_topic) | |
| # ✅ ANÁLISE 3: Adicionar subtópicos | |
| subtopics = self._extract_subtopics(message, new_topic) | |
| for sub in subtopics: | |
| if sub not in lstm_summary.subtopicas: | |
| lstm_summary.subtopicas.append(sub) | |
| # ✅ ANÁLISE 4: Detectar perguntas pendentes | |
| if role == "user" and self._is_question(message): | |
| lstm_summary.unanswered_questions.append({ | |
| 'question': message, | |
| 'timestamp': time.time(), | |
| 'parent_message': parent_message_id | |
| }) | |
| # ✅ ANÁLISE 5: Detectar padrão de interação | |
| lstm_summary.interaction_pattern = self._detect_interaction_pattern( | |
| message, role, lstm_summary | |
| ) | |
| # ✅ ANÁLISE 6: Extrair conhecimento observado | |
| knowledge = self._extract_assumed_knowledge(message) | |
| for k in knowledge: | |
| if k not in lstm_summary.assumed_knowledge: | |
| lstm_summary.assumed_knowledge.append(k) | |
| # ✅ ANÁLISE 7: Detectar contradições ou mudanças | |
| contradictions = self._detect_contradictions( | |
| message, lstm_summary.assumed_knowledge | |
| ) | |
| if contradictions: | |
| lstm_summary.contradictions.extend(contradictions) | |
| # ✅ ANÁLISE 8: Guardar mensagem-chave para retomada | |
| if self._is_key_message(message, role): | |
| lstm_summary.last_key_message = f"{role}: {message[:100]}" | |
| # Atualizar timestamp | |
| lstm_summary.last_updated = time.time() | |
| # Salvar no DB | |
| self._save_lstm_summary(lstm_summary) | |
| # Atualizar cache | |
| with self.cache_lock: | |
| self.lstm_cache[context_id] = lstm_summary | |
| # ======================================================== | |
| # ANÁLISE E EXTRAÇÃO | |
| # ======================================================== | |
| def _extract_topic(self, message: str) -> Optional[str]: | |
| """ | |
| Extrai tema principal da mensagem. | |
| Uses simples regex patterns + key phrase detection. | |
| """ | |
| message_lower = message.lower().strip() | |
| # Detects via keywords comuns | |
| keywords_map = { | |
| 'anemia falciforme': ['anemia', 'falciforme', 'hemoglobina', 'sangue'], | |
| 'cura/tratamento': ['cura', 'tratamento', 'medicação', 'terapia'], | |
| 'política': ['presidente', 'eleição', 'política', 'governo', 'ministro'], | |
| 'clima': ['tempo', 'chuva', 'temperatura', 'previsão', 'clima'], | |
| 'saúde': ['doença', 'médico', 'hospital', 'sintomas', 'saúde'], | |
| } | |
| for topic, keywords in keywords_map.items(): | |
| if any(kw in message_lower for kw in keywords): | |
| return topic | |
| # Se não detectar via keywords, tenta extrair primeira entidade nomeada | |
| # (simplificado - em produção usaria NER) | |
| if len(message.split()) >= 3: | |
| # Pega primeiras 3-4 palavras como possível tema | |
| words = message.split()[:4] | |
| if all(w[0].isupper() for w in words if w): | |
| return ' '.join(words) | |
| return None | |
| def _extract_subtopics(self, message: str, main_topic: Optional[str]) -> List[str]: | |
| """Extrai subtópicos mencionados.""" | |
| subtopics = [] | |
| message_lower = message.lower() | |
| # Padrões simples para detecção de subtópicos | |
| patterns = { | |
| 'causas': ['porque', 'causa', 'origem', 'motivo'], | |
| 'sintomas': ['sintoma', 'sinto', 'dor', 'febre', 'crise'], | |
| 'prevenção': ['prevenir', 'prevenção', 'evitar', 'proteção'], | |
| 'complicações': ['complicação', 'risco', 'morte', 'consequência'], | |
| 'história': ['história', 'origem', 'histórico', 'quando começou'], | |
| 'tratamento': ['tratamento', 'medicação', 'remédio', 'terapia'], | |
| } | |
| for subtopic, keywords in patterns.items(): | |
| if any(kw in message_lower for kw in keywords): | |
| subtopics.append(subtopic) | |
| return subtopics | |
| def _is_question(self, message: str) -> bool: | |
| """Detecta se mensagem é uma pergunta.""" | |
| message = message.strip() | |
| # Detecta ? ou gírias de perguntas | |
| return ( | |
| message.endswith('?') or | |
| message.lower().startswith(('qual', 'quem', 'quando', 'onde', 'por que', | |
| 'como', 'quanto', 'cura', 'tratamento')) | |
| ) | |
| def _detect_interaction_pattern( | |
| self, | |
| message: str, | |
| role: str, | |
| lstm_summary: LSTMContextSummary | |
| ) -> str: | |
| """ | |
| Detecta padrão de interação do usuário. | |
| Exemplos: "perguntador", "explicador", "discordante", "concorda", etc. | |
| """ | |
| if role != "user": | |
| return lstm_summary.interaction_pattern | |
| message_lower = message.lower() | |
| # Contadores simples | |
| if self._is_question(message): | |
| return "perguntador" | |
| elif any(w in message_lower for w in ['estou triste', 'deprimido', 'é ruim', 'horrível']): | |
| return "expressivo_negativo" | |
| elif any(w in message_lower for w in ['adorei', 'ótimo', 'perfeito', 'amei']): | |
| return "expressivo_positivo" | |
| elif any(w in message_lower for w in ['discordo', 'não acho', 'errado', 'talvez']): | |
| return "discordante" | |
| elif any(w in message_lower for w in ['concordo', 'é verdade', 'exatamente']): | |
| return "concordante" | |
| elif len(message.split()) > 20: | |
| return "narrativo" | |
| else: | |
| return lstm_summary.interaction_pattern or "casual" | |
| def _extract_assumed_knowledge(self, message: str) -> List[str]: | |
| """ | |
| Extrai conhecimento que o usuário demonstra ter. | |
| Detecta conceitos que ele mencionou como se já soubesse. | |
| """ | |
| knowledge = [] | |
| message_lower = message.lower() | |
| # Conhecimento técnico/científico | |
| if any(w in message_lower for w in ['hemoglobina', 'hemácias', 'globina', 'mutação']): | |
| knowledge.append("conhece_biologia_basica") | |
| if any(w in message_lower for w in ['genético', 'hereditário', 'cromossomo']): | |
| knowledge.append("conhece_genetica") | |
| if any(w in message_lower for w in ['RDC', 'MPLA', 'eleições']): | |
| knowledge.append("conhece_politica_angola") | |
| if any(w in message_lower for w in ['UTC', 'fuso horário', 'timezone']): | |
| knowledge.append("conhece_timezones") | |
| return knowledge | |
| def _detect_contradictions( | |
| self, | |
| message: str, | |
| assumed_knowledge: List[str] | |
| ) -> List[Dict[str, Any]]: | |
| """ | |
| Detecta contradições entre o que o usuário disse antes e agora. | |
| Exemplo: "Anemia falciforme é fácil de tratar" vs "Não há cura" | |
| """ | |
| contradictions = [] | |
| message_lower = message.lower() | |
| # Padrões simples de contradição | |
| if "fácil" in message_lower and any( | |
| w in message_lower for w in ['não há', 'sem cura', 'incurável'] | |
| ): | |
| contradictions.append({ | |
| 'type': 'difficulty_contradiction', | |
| 'current_message': message[:50] | |
| }) | |
| return contradictions | |
| def _is_key_message(self, message: str, role: str) -> bool: | |
| """ | |
| Detecta se é uma "mensagem-chave" para retomada de contexto. | |
| Exemplos: Perguntas importantes, pedidos de esclarecimento, mudança de tema. | |
| """ | |
| if role == "user": | |
| return ( | |
| self._is_question(message) and len(message.split()) <= 10 or | |
| any(w in message.lower() for w in ['cura', 'tratamento', 'então', 'mas', 'porquê']) | |
| ) | |
| return False | |
| # ======================================================== | |
| # ARMAZENAMENTO E RECUPERAÇÃO | |
| # ======================================================== | |
| def _get_or_create_lstm_summary( | |
| self, | |
| context_id: str, | |
| numero_usuario: str | |
| ) -> LSTMContextSummary: | |
| """Recupera ou cria novo resumo LSTM.""" | |
| # Verificar cache primeiro | |
| with self.cache_lock: | |
| if context_id in self.lstm_cache: | |
| return self.lstm_cache[context_id] | |
| # Tentar recuperar do DB | |
| try: | |
| rows = self.db._execute_with_retry( | |
| "SELECT metadata FROM lstm_contexto WHERE context_id = ?", | |
| (context_id,) | |
| ) | |
| if rows and len(rows) > 0: | |
| result = rows[0] | |
| raw = result[0] if isinstance(result, (tuple, list)) else dict(result).get('metadata') | |
| if raw: | |
| data = json.loads(raw) if isinstance(raw, str) else raw | |
| summary = LSTMContextSummary.from_dict(data) | |
| else: | |
| summary = LSTMContextSummary( | |
| context_id=context_id, | |
| numero_usuario=numero_usuario | |
| ) | |
| self._save_lstm_summary(summary) | |
| else: | |
| # Criar novo | |
| summary = LSTMContextSummary( | |
| context_id=context_id, | |
| numero_usuario=numero_usuario | |
| ) | |
| self._save_lstm_summary(summary) | |
| # Cachear | |
| with self.cache_lock: | |
| self.lstm_cache[context_id] = summary | |
| return summary | |
| except Exception as e: | |
| logger.error(f"❌ Erro ao recuperar LSTM summary: {e}") | |
| # Fallback: criar novo | |
| return LSTMContextSummary( | |
| context_id=context_id, | |
| numero_usuario=numero_usuario | |
| ) | |
| def _save_lstm_summary(self, summary: LSTMContextSummary) -> None: | |
| """Salva resumo LSTM no DB.""" | |
| try: | |
| self.db._execute_with_retry(""" | |
| INSERT OR REPLACE INTO lstm_contexto | |
| (context_id, numero_usuario, topic_principal, subtopicas, | |
| conversation_path, last_key_message, emotional_state, | |
| interaction_pattern, context_switches, unanswered_questions, | |
| assumed_knowledge, contradictions, created_at, last_updated, metadata) | |
| VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) | |
| """, ( | |
| summary.context_id, | |
| summary.numero_usuario, | |
| summary.topic_principal, | |
| json.dumps(summary.subtopicas, ensure_ascii=False), | |
| json.dumps(summary.conversation_path, ensure_ascii=False), | |
| summary.last_key_message, | |
| summary.emotional_state, | |
| summary.interaction_pattern, | |
| summary.context_switches, | |
| json.dumps(summary.unanswered_questions, ensure_ascii=False, default=str), | |
| json.dumps(summary.assumed_knowledge, ensure_ascii=False), | |
| json.dumps(summary.contradictions, ensure_ascii=False, default=str), | |
| summary.created_at, | |
| summary.last_updated, | |
| summary.to_json() | |
| ), commit=True) | |
| logger.debug(f"✅ LSTM summary salvo: {summary.context_id}") | |
| except Exception as e: | |
| logger.error(f"❌ Erro ao salvar LSTM summary: {e}") | |
| def _record_context_switch( | |
| self, | |
| context_id: str, | |
| numero_usuario: str, | |
| parent_message_id: Optional[str], | |
| new_topic: str | |
| ) -> None: | |
| """Registra mudança de contexto/tópico.""" | |
| try: | |
| # Gera um ID temporário se não houver | |
| msg_id = f"switch_{int(time.time())}_{hashlib.md5(new_topic.encode()).hexdigest()[:8]}" | |
| self.db._execute_with_retry(""" | |
| INSERT INTO lstm_message_links | |
| (context_id, message_id, numero_usuario, parent_message_id, topic_changed, | |
| context_switch_type, created_at) | |
| VALUES (?, ?, ?, ?, ?, ?, ?) | |
| """, ( | |
| context_id, | |
| msg_id, | |
| numero_usuario, | |
| parent_message_id, | |
| True, | |
| 'topic_change', | |
| time.time() | |
| ), commit=True) | |
| except Exception as e: | |
| logger.warning(f"⚠️ Erro ao registrar context switch: {e}") | |
| # ======================================================== | |
| # RECUPERAÇÃO AUTOMÁTICA PARA MODELO | |
| # ======================================================== | |
| def get_lstm_context_for_model( | |
| self, | |
| context_id: str, | |
| numero_usuario: str, | |
| use_summarization: bool = True | |
| ) -> Dict[str, Any]: | |
| """ | |
| Recupera contexto LSTM para o modelo usar. | |
| Chamado automaticamente pelo modelo quando precisa de contexto. | |
| Retorna contexto mental completo sem exposição ao usuário. | |
| Args: | |
| context_id: ID do contexto | |
| numero_usuario: ID do usuário | |
| use_summarization: Se deve summarizar para embeddings | |
| Returns: | |
| Dicionário com contexto LSTM completo | |
| """ | |
| summary = self._get_or_create_lstm_summary(context_id, numero_usuario) | |
| context_dict = { | |
| 'context_id': context_id, | |
| 'topic_principal': summary.topic_principal, | |
| 'subtopicas': summary.subtopicas, | |
| 'conversation_path': summary.conversation_path, | |
| 'last_key_message': summary.last_key_message, | |
| 'emotional_state': summary.emotional_state, | |
| 'interaction_pattern': summary.interaction_pattern, | |
| 'context_switches': summary.context_switches, | |
| 'unanswered_questions': summary.unanswered_questions, | |
| 'assumed_knowledge': summary.assumed_knowledge, | |
| } | |
| # Se quiser usar para embeddings/similarity | |
| if use_summarization: | |
| context_dict['mental_summary_text'] = self._create_mental_summary_text(summary) | |
| return context_dict | |
| def _create_mental_summary_text(self, summary: LSTMContextSummary) -> str: | |
| """ | |
| Cria texto resumido mental para uso em embeddings/similarity. | |
| Totalmente oculto do usuário. | |
| """ | |
| parts = [] | |
| if summary.topic_principal: | |
| parts.append(f"Topic: {summary.topic_principal}") | |
| if summary.subtopicas: | |
| parts.append(f"Subtopics: {', '.join(summary.subtopicas)}") | |
| if summary.assumed_knowledge: | |
| parts.append(f"User knows about: {', '.join(summary.assumed_knowledge)}") | |
| if summary.unanswered_questions: | |
| questions = [q.get('question', '')[:50] for q in summary.unanswered_questions[-3:]] | |
| parts.append(f"Pending: {'; '.join(questions)}") | |
| if summary.interaction_pattern and summary.interaction_pattern != 'unknown': | |
| parts.append(f"Pattern: {summary.interaction_pattern}") | |
| return " | ".join(parts) | |
| # ======================================================== | |
| # QUERIES E BUSCAS | |
| # ======================================================== | |
| def search_related_contexts( | |
| self, | |
| numero_usuario: str, | |
| query: str, | |
| limit: int = 5 | |
| ) -> List[Dict[str, Any]]: | |
| """ | |
| Busca contextos relacionados ao usuário baseado em query. | |
| Usado quando modelo precisa encontrar conversas relevantes. | |
| Args: | |
| numero_usuario: ID do usuário | |
| query: Query de busca (ex: "anemia falciforme") | |
| limit: Máximo de resultados | |
| Returns: | |
| Lista de contextos relacionados | |
| """ | |
| try: | |
| results = self.db._execute_with_retry(""" | |
| SELECT context_id, topic_principal, subtopicas, | |
| last_key_message, last_updated | |
| FROM lstm_contexto | |
| WHERE numero_usuario = ? | |
| AND (topic_principal LIKE ? OR subtopicas LIKE ? | |
| OR assumed_knowledge LIKE ?) | |
| ORDER BY last_updated DESC | |
| LIMIT ? | |
| """, ( | |
| numero_usuario, | |
| f"%{query}%", | |
| f"%{query}%", | |
| f"%{query}%", | |
| limit | |
| )) | |
| contexts = [] | |
| for row in (results or []): | |
| contexts.append({ | |
| 'context_id': row[0], | |
| 'topic': row[1], | |
| 'subtopics': json.loads(row[2]) if row[2] else [], | |
| 'last_message': row[3], | |
| 'last_interaction': row[4] | |
| }) | |
| return contexts | |
| except Exception as e: | |
| logger.error(f"❌ Erro ao buscar contextos relacionados: {e}") | |
| return [] | |
| def get_conversation_history_with_context( | |
| self, | |
| context_id: str, | |
| last_n_messages: int = 20 | |
| ) -> Dict[str, Any]: | |
| """ | |
| Recupera histórico completo de conversa com contexto LSTM. | |
| Útil para recarregar conversa com máximo contexto. | |
| Args: | |
| context_id: ID do contexto | |
| last_n_messages: Últimas N mensagens a incluir | |
| Returns: | |
| Dicionário com histórico + contexto mental | |
| """ | |
| # Recuperar LSTM | |
| lstm_context = self._get_or_create_lstm_summary( | |
| context_id, | |
| "" # numero_usuario será recuperado do LSTM | |
| ) | |
| # Recuperar mensagens (via short_term_memory ou DB) | |
| try: | |
| rows = self.db._execute_with_retry(""" | |
| SELECT usuario, mensagem, resposta, created_at | |
| FROM mensagens | |
| WHERE conversation_id = ? | |
| ORDER BY id DESC | |
| LIMIT ? | |
| """, (context_id, last_n_messages)) | |
| messages = [] | |
| for m in reversed(rows or []): | |
| if m[1]: # mensagem do user | |
| messages.append({'role': 'user', 'content': m[1], 'timestamp': m[3]}) | |
| if m[2]: # resposta do assistant | |
| messages.append({'role': 'assistant', 'content': m[2], 'timestamp': m[3]}) | |
| except Exception: | |
| messages = [] | |
| return { | |
| 'context_id': context_id, | |
| 'lstm_context': lstm_context.to_dict(), | |
| 'messages': messages, | |
| 'mental_summary': self._create_mental_summary_text(lstm_context) | |
| } | |
| # ============================================================ | |
| # SINGLETON GLOBAL | |
| # ============================================================ | |
| _lstm_memory_instance: Optional[LSTMMemorySystem] = None | |
| _lstm_memory_lock = threading.Lock() | |
| def get_lstm_memory_system( | |
| db: Optional[Database] = None, | |
| context_isolation: Optional[ContextIsolationManager] = None | |
| ) -> Optional[LSTMMemorySystem]: | |
| """ | |
| Obtém instância singleton do LSTM Memory System. | |
| Args: | |
| db: Database instance (opcional, usa global se não fornecido) | |
| context_isolation: ContextIsolationManager instance (opcional) | |
| Returns: | |
| Instância do LSTMMemorySystem ou None se indisponível | |
| """ | |
| global _lstm_memory_instance | |
| if _lstm_memory_instance is not None: | |
| return _lstm_memory_instance | |
| if not LSTM_MEMORY_AVAILABLE: | |
| logger.warning("⚠️ LSTM Memory System não está disponível") | |
| return None | |
| with _lstm_memory_lock: | |
| if _lstm_memory_instance is not None: | |
| return _lstm_memory_instance | |
| try: | |
| if db is None: | |
| db_path = getattr(config, 'DB_PATH', None) or 'data/akira.db' | |
| db = Database(str(db_path)) | |
| if context_isolation is None: | |
| context_isolation = ContextIsolationManager() | |
| _lstm_memory_instance = LSTMMemorySystem(db, context_isolation) | |
| logger.info("✅ LSTM Memory System singleton criado") | |
| return _lstm_memory_instance | |
| except Exception as e: | |
| logger.error(f"❌ Erro ao criar LSTM Memory System: {e}") | |
| return None | |