File size: 35,368 Bytes
c715f62
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
c2e2750
 
 
 
c715f62
 
 
 
 
 
 
 
c2e2750
 
c715f62
 
4f90e1d
c2e2750
 
c715f62
 
 
 
 
 
 
 
 
 
 
4f90e1d
 
c715f62
 
 
 
 
 
c2e2750
 
 
 
c715f62
c2e2750
 
 
4f90e1d
c715f62
 
 
c2e2750
c715f62
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
c2e2750
 
c715f62
 
 
 
 
 
 
 
c2e2750
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
c715f62
 
 
 
 
 
 
 
c2e2750
c715f62
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
c2e2750
c715f62
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
c2e2750
c715f62
 
 
 
 
c2e2750
 
 
c715f62
 
c2e2750
c715f62
c2e2750
c715f62
 
c2e2750
 
c715f62
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
# 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
# ============================================================

@dataclass
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)
    
    @classmethod
    def from_dict(cls, data: Dict[str, Any]) -> 'LSTMContextSummary':
        """Cria instância a partir de dicionário."""
        return cls(**data)
    
    @classmethod
    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