alive-ai 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (168) hide show
  1. package/Dockerfile +24 -0
  2. package/LICENSE +21 -0
  3. package/README.md +143 -0
  4. package/alive_ai/__init__.py +3 -0
  5. package/brain/__init__.py +59 -0
  6. package/brain/almost_said.py +154 -0
  7. package/brain/bid_detector.py +636 -0
  8. package/brain/conversation_flow.py +135 -0
  9. package/brain/curiosity.py +328 -0
  10. package/brain/default_mode.py +1438 -0
  11. package/brain/dreams.py +220 -0
  12. package/brain/embeddings/__init__.py +82 -0
  13. package/brain/emotional_memory.py +949 -0
  14. package/brain/global_activity.py +173 -0
  15. package/brain/group_dynamics.py +63 -0
  16. package/brain/linguistic.py +235 -0
  17. package/brain/llm/__init__.py +63 -0
  18. package/brain/llm/base.py +33 -0
  19. package/brain/llm/fallback_router.py +309 -0
  20. package/brain/llm/manifest.md +30 -0
  21. package/brain/llm/ollama.py +218 -0
  22. package/brain/llm/openrouter.py +151 -0
  23. package/brain/llm/provider.py +205 -0
  24. package/brain/llm/unified.py +423 -0
  25. package/brain/llm/zai.py +169 -0
  26. package/brain/manifest.md +23 -0
  27. package/brain/memory/__init__.py +123 -0
  28. package/brain/memory/episodic.py +92 -0
  29. package/brain/memory/fact_extractor.py +209 -0
  30. package/brain/memory/index.py +54 -0
  31. package/brain/memory/manager.py +151 -0
  32. package/brain/memory/summarizer.py +102 -0
  33. package/brain/memory/vector_store.py +297 -0
  34. package/brain/memory/working.py +43 -0
  35. package/brain/narrative.py +343 -0
  36. package/brain/stt/__init__.py +4 -0
  37. package/brain/stt/google_stt.py +83 -0
  38. package/brain/stt/whisper_stt.py +82 -0
  39. package/brain/subconscious/__init__.py +33 -0
  40. package/brain/subconscious/actions.py +136 -0
  41. package/brain/subconscious/evaluation.py +166 -0
  42. package/brain/subconscious/goal_system.py +90 -0
  43. package/brain/subconscious/goals.py +41 -0
  44. package/brain/subconscious/impulse_generator.py +200 -0
  45. package/brain/subconscious/impulses.py +48 -0
  46. package/brain/subconscious/learning.py +24 -0
  47. package/brain/subconscious/learning_system.py +79 -0
  48. package/brain/subconscious/loop.py +398 -0
  49. package/brain/subconscious/manifest.md +32 -0
  50. package/brain/subconscious/relationship.py +47 -0
  51. package/brain/subconscious/relationship_memory.py +83 -0
  52. package/brain/subconscious/response_analyzer.py +74 -0
  53. package/brain/subconscious/templates.py +70 -0
  54. package/brain/subconscious/thought.py +37 -0
  55. package/brain/subconscious/working_memory.py +97 -0
  56. package/cli/index.js +371 -0
  57. package/config/directives.example.json +28 -0
  58. package/config/instructions.example.md +16 -0
  59. package/config/self.example.json +74 -0
  60. package/config/settings.example.json +95 -0
  61. package/core/__init__.py +1 -0
  62. package/core/config.py +54 -0
  63. package/core/directives.py +198 -0
  64. package/core/events.py +50 -0
  65. package/core/follow_up.py +267 -0
  66. package/core/hot_reload.py +174 -0
  67. package/core/initialization.py +253 -0
  68. package/core/manifest.md +28 -0
  69. package/core/media_handler.py +241 -0
  70. package/core/memory_monitor.py +200 -0
  71. package/core/message_handler.py +1440 -0
  72. package/core/proactive_generator.py +277 -0
  73. package/core/self.py +188 -0
  74. package/core/settings.py +169 -0
  75. package/core/skills_registry.py +357 -0
  76. package/core/state.py +27 -0
  77. package/core/subconscious_bridge.py +93 -0
  78. package/core/thinking.py +175 -0
  79. package/core/user_manager.py +306 -0
  80. package/core/user_tracker.py +144 -0
  81. package/demo/index.html +144 -0
  82. package/docker-compose.yml +28 -0
  83. package/docs/assets/logo.svg +15 -0
  84. package/docs/index.html +355 -0
  85. package/heart/__init__.py +93 -0
  86. package/heart/afterglow.py +215 -0
  87. package/heart/attachment.py +186 -0
  88. package/heart/circadian.py +251 -0
  89. package/heart/complex_emotions.py +114 -0
  90. package/heart/conflicts.py +589 -0
  91. package/heart/core.py +387 -0
  92. package/heart/emotional_decay.py +59 -0
  93. package/heart/emotional_memory.py +261 -0
  94. package/heart/emotional_state.py +146 -0
  95. package/heart/emotional_variability.py +156 -0
  96. package/heart/hormonal.py +424 -0
  97. package/heart/inconsistency.py +1222 -0
  98. package/heart/integrity.py +469 -0
  99. package/heart/interoception.py +997 -0
  100. package/heart/love.py +120 -0
  101. package/heart/manifest.md +25 -0
  102. package/heart/mood_shifts.py +169 -0
  103. package/heart/phantom_somatic.py +259 -0
  104. package/heart/predictive.py +374 -0
  105. package/heart/scars.py +474 -0
  106. package/heart/somatic.py +482 -0
  107. package/heart/soul.py +633 -0
  108. package/heart/telemetry.py +942 -0
  109. package/heart/triggers.py +119 -0
  110. package/heart/unconscious.py +443 -0
  111. package/input/__init__.py +1 -0
  112. package/input/manifest.md +24 -0
  113. package/input/telegram/__init__.py +1 -0
  114. package/input/telegram/commands.py +762 -0
  115. package/input/telegram/listener.py +532 -0
  116. package/main.py +90 -0
  117. package/manifest.md +28 -0
  118. package/mypics/.gitkeep +1 -0
  119. package/myvids/.gitkeep +1 -0
  120. package/output/__init__.py +1 -0
  121. package/output/images/__init__.py +1 -0
  122. package/output/images/fal_gen.py +43 -0
  123. package/output/manifest.md +26 -0
  124. package/output/text/__init__.py +1 -0
  125. package/output/text/sender.py +22 -0
  126. package/output/voice/__init__.py +64 -0
  127. package/output/voice/google_tts.py +252 -0
  128. package/output/voice/gtts_tts.py +214 -0
  129. package/output/voice/vibe_tts.py +190 -0
  130. package/package.json +58 -0
  131. package/pyproject.toml +23 -0
  132. package/requirements.txt +21 -0
  133. package/skills/__init__.py +1 -0
  134. package/skills/anticipation_engine/__init__.py +8 -0
  135. package/skills/anticipation_engine/engine.py +618 -0
  136. package/skills/anticipation_engine/manifest.md +192 -0
  137. package/skills/calendar/__init__.py +1 -0
  138. package/skills/content_unlocks/__init__.py +8 -0
  139. package/skills/content_unlocks/manifest.md +231 -0
  140. package/skills/content_unlocks/unlocks.py +945 -0
  141. package/skills/exclusive_moments/__init__.py +8 -0
  142. package/skills/exclusive_moments/manifest.md +145 -0
  143. package/skills/exclusive_moments/moments.py +506 -0
  144. package/skills/intimacy_layers/__init__.py +8 -0
  145. package/skills/intimacy_layers/layers.py +703 -0
  146. package/skills/intimacy_layers/manifest.md +203 -0
  147. package/skills/manifest.md +67 -0
  148. package/skills/memory_callbacks/__init__.py +9 -0
  149. package/skills/memory_callbacks/callbacks.py +748 -0
  150. package/skills/memory_callbacks/manifest.md +170 -0
  151. package/skills/message_scheduler/__init__.py +19 -0
  152. package/skills/message_scheduler/manifest.md +107 -0
  153. package/skills/message_scheduler/scheduler.py +510 -0
  154. package/skills/photo_manager/__init__.py +1 -0
  155. package/skills/photo_manager/scanner.py +296 -0
  156. package/skills/relationship_milestones/__init__.py +8 -0
  157. package/skills/relationship_milestones/manifest.md +206 -0
  158. package/skills/relationship_milestones/tracker.py +494 -0
  159. package/skills/self_authorship/__init__.py +23 -0
  160. package/skills/self_authorship/author.py +331 -0
  161. package/skills/self_authorship/manifest.md +24 -0
  162. package/skills/video_manager/__init__.py +5 -0
  163. package/skills/video_manager/manifest.md +37 -0
  164. package/skills/video_manager/scanner.py +229 -0
  165. package/webui/__init__.py +3 -0
  166. package/webui/app.py +936 -0
  167. package/webui/bridge.py +366 -0
  168. package/webui/static/index.html +2070 -0
@@ -0,0 +1,949 @@
1
+ """
2
+ Brain: Emotional Memory System
3
+ Memories weighted by emotional significance
4
+
5
+ Based on neuroscience:
6
+ - Amygdala tags emotional significance
7
+ - Hippocampus binds context (emotion + content + relationship state)
8
+ - High-emotion memories stored with higher fidelity and retrieved more easily
9
+ - "Tag and capture": emotional moments strengthen surrounding memories
10
+
11
+ This module is MODULAR - can be connected/disconnected without breaking anything.
12
+ """
13
+
14
+ import json
15
+ import uuid
16
+ import math
17
+ from datetime import datetime, timedelta
18
+ from pathlib import Path
19
+ from typing import Optional, List, Dict, Any, Tuple
20
+ from dataclasses import dataclass, field, asdict
21
+ import threading
22
+
23
+ # Import settings for configuration
24
+ import sys
25
+ sys.path.insert(0, str(Path(__file__).parent.parent))
26
+ from core.settings import get_int, get_float
27
+
28
+
29
+ # ============================================================
30
+ # Data Classes
31
+ # ============================================================
32
+
33
+ @dataclass
34
+ class EmotionalMemory:
35
+ """
36
+ A single memory with emotional weighting.
37
+
38
+ Memory Structure:
39
+ - id: unique identifier
40
+ - content: what happened
41
+ - emotional_weight: 0-1, how significant (amygdala tag strength)
42
+ - emotional_valence: -1 to 1, positive/negative
43
+ - emotions_felt: list of emotion names
44
+ - context: surrounding information (hippocampus binding)
45
+ - timestamp: when it occurred
46
+ - access_count: how many times retrieved
47
+ - last_accessed: when last retrieved
48
+ - consolidated: processed into long-term patterns
49
+ """
50
+ id: str
51
+ content: str
52
+ emotional_weight: float # 0-1
53
+ emotional_valence: float # -1 to 1
54
+ emotions_felt: List[str]
55
+ context: Dict[str, Any]
56
+ timestamp: str
57
+ access_count: int = 0
58
+ last_accessed: Optional[str] = None
59
+ consolidated: bool = False
60
+
61
+ # Additional metadata
62
+ user_id: str = "default"
63
+ importance_boost: float = 0.0 # Additional boost from consolidation
64
+
65
+ def __post_init__(self):
66
+ """Ensure values are within bounds"""
67
+ self.emotional_weight = max(0.0, min(1.0, self.emotional_weight))
68
+ self.emotional_valence = max(-1.0, min(1.0, self.emotional_valence))
69
+
70
+ def to_dict(self) -> dict:
71
+ """Convert to dictionary for serialization"""
72
+ return asdict(self)
73
+
74
+ @classmethod
75
+ def from_dict(cls, data: dict) -> "EmotionalMemory":
76
+ """Create from dictionary"""
77
+ return cls(**data)
78
+
79
+ def age_hours(self) -> float:
80
+ """How old is this memory in hours"""
81
+ try:
82
+ event_time = datetime.fromisoformat(self.timestamp)
83
+ delta = datetime.now() - event_time
84
+ return delta.total_seconds() / 3600
85
+ except:
86
+ return 9999
87
+
88
+ def age_days(self) -> float:
89
+ """How old is this memory in days"""
90
+ return self.age_hours() / 24
91
+
92
+ def get_retrieval_score(self,
93
+ current_emotion: dict = None,
94
+ recency_weight: float = 0.3,
95
+ emotional_similarity_weight: float = 0.4,
96
+ access_weight: float = 0.2,
97
+ base_weight: float = 0.1) -> float:
98
+ """
99
+ Calculate retrieval score based on multiple factors.
100
+ Higher score = more likely to be retrieved.
101
+ """
102
+ # Base score from emotional weight
103
+ score = self.emotional_weight * base_weight
104
+
105
+ # Recency factor (exponential decay)
106
+ decay_days = get_int("EMOTIONAL_MEMORY_DECAY_DAYS", 30)
107
+ recency_factor = math.exp(-self.age_days() / decay_days)
108
+ score += recency_factor * recency_weight
109
+
110
+ # Access count factor (memories accessed more are easier to retrieve)
111
+ access_factor = min(1.0, self.access_count / 10)
112
+ score += access_factor * access_weight
113
+
114
+ # Emotional similarity factor
115
+ if current_emotion and self.emotions_felt:
116
+ similarity = self._calculate_emotional_similarity(current_emotion)
117
+ score += similarity * emotional_similarity_weight
118
+
119
+ # Importance boost from consolidation
120
+ score += self.importance_boost * 0.1
121
+
122
+ return min(1.0, score)
123
+
124
+ def _calculate_emotional_similarity(self, current_emotion: dict) -> float:
125
+ """Calculate how similar current emotional state is to this memory"""
126
+ if not self.emotions_felt:
127
+ return 0.0
128
+
129
+ # Map emotion names to valence/arousal for comparison
130
+ emotion_mapping = {
131
+ "happy": (0.8, 0.5),
132
+ "joy": (0.9, 0.6),
133
+ "excited": (0.7, 0.8),
134
+ "love": (0.9, 0.4),
135
+ "desire": (0.6, 0.7),
136
+ "arousal": (0.5, 0.9),
137
+ "sad": (-0.6, 0.3),
138
+ "angry": (-0.7, 0.7),
139
+ "fear": (-0.5, 0.8),
140
+ "anxious": (-0.3, 0.6),
141
+ "calm": (0.3, 0.2),
142
+ "content": (0.5, 0.2),
143
+ "nostalgic": (0.3, 0.3),
144
+ "romantic": (0.7, 0.5),
145
+ "flirty": (0.6, 0.6),
146
+ "vulnerable": (-0.2, 0.5),
147
+ "grateful": (0.8, 0.3),
148
+ }
149
+
150
+ # Get average valence/arousal for memory's emotions
151
+ memory_va = [emotion_mapping.get(e, (0, 0)) for e in self.emotions_felt]
152
+ if not memory_va:
153
+ return 0.0
154
+
155
+ memory_valence = sum(v for v, _ in memory_va) / len(memory_va)
156
+ memory_arousal = sum(a for _, a in memory_va) / len(memory_va)
157
+
158
+ # Calculate current state valence/arousal
159
+ current_valence = current_emotion.get("valence", 0)
160
+ current_arousal = current_emotion.get("arousal", 0.5)
161
+
162
+ # Infer from named emotions if present
163
+ if "emotions" in current_emotion:
164
+ for emotion_name, value in current_emotion["emotions"].items():
165
+ if emotion_name in emotion_mapping and value > 0.3:
166
+ ev, ea = emotion_mapping[emotion_name]
167
+ current_valence = current_valence * 0.5 + ev * 0.5
168
+ current_arousal = current_arousal * 0.5 + ea * 0.5
169
+
170
+ # Calculate Euclidean similarity
171
+ distance = math.sqrt((memory_valence - current_valence)**2 +
172
+ (memory_arousal - current_arousal)**2)
173
+ similarity = 1.0 - min(1.0, distance / 2.0) # Normalize to 0-1
174
+
175
+ return similarity
176
+
177
+
178
+ # ============================================================
179
+ # Main System Class
180
+ # ============================================================
181
+
182
+ class EmotionalMemorySystem:
183
+ """
184
+ Core emotional memory system implementing amygdala-hippocampus-like
185
+ memory processing with emotional weighting.
186
+
187
+ Features:
188
+ - Emotional tagging of interactions
189
+ - Weighted memory storage and retrieval
190
+ - Contextual binding (emotion + content + relationship state)
191
+ - Memory consolidation over time
192
+ """
193
+
194
+ def __init__(self, data_path: Path = None, user_id: str = "default"):
195
+ """
196
+ Initialize the emotional memory system.
197
+
198
+ Args:
199
+ data_path: Path to data directory (defaults to project data/)
200
+ user_id: User identifier for per-user memory isolation
201
+ """
202
+ self.user_id = user_id
203
+ self._lock = threading.RLock()
204
+
205
+ # Set up data path
206
+ if data_path is None:
207
+ data_path = Path(__file__).parent.parent / "data"
208
+ self.data_path = Path(data_path)
209
+ self.storage_path = self.data_path / "emotional_memories"
210
+ self.storage_path.mkdir(parents=True, exist_ok=True)
211
+
212
+ # User-specific storage
213
+ self.user_storage_file = self.storage_path / f"{user_id}_memories.json"
214
+
215
+ # In-memory cache
216
+ self._memories: Dict[str, EmotionalMemory] = {}
217
+ self._recent_memories: List[str] = [] # IDs of recent memories
218
+ self._consolidation_queue: List[str] = [] # IDs pending consolidation
219
+
220
+ # Configuration from settings
221
+ self.max_stored = get_int("EMOTIONAL_MEMORY_MAX_STORED", 500)
222
+ self.high_emotion_threshold = get_float("HIGH_EMOTION_THRESHOLD", 0.7)
223
+ self.decay_days = get_int("EMOTIONAL_MEMORY_DECAY_DAYS", 30)
224
+
225
+ # Load existing memories
226
+ self._load()
227
+
228
+ print(f"[EmotionalMemory] Initialized for user {user_id} with {len(self._memories)} memories")
229
+
230
+ # ============================================================
231
+ # Core API Methods
232
+ # ============================================================
233
+
234
+ def encode_memory(self,
235
+ content: str,
236
+ emotional_weight: float,
237
+ context: dict = None,
238
+ emotional_valence: float = 0.0,
239
+ emotions_felt: List[str] = None) -> EmotionalMemory:
240
+ """
241
+ Store a memory with emotional weighting.
242
+
243
+ Args:
244
+ content: What happened (the memory content)
245
+ emotional_weight: 0-1, how emotionally significant (amygdala tag)
246
+ context: Additional context (relationship state, time, topic, etc.)
247
+ emotional_valence: -1 to 1, positive vs negative
248
+ emotions_felt: List of emotion names experienced
249
+
250
+ Returns:
251
+ The created EmotionalMemory object
252
+ """
253
+ with self._lock:
254
+ memory_id = str(uuid.uuid4())
255
+
256
+ # Enrich context with defaults
257
+ if context is None:
258
+ context = {}
259
+
260
+ # Add automatic context
261
+ context.setdefault("time_of_day", self._get_time_of_day())
262
+ context.setdefault("day_of_week", datetime.now().strftime("%A"))
263
+ if "relationship_state" not in context:
264
+ context["relationship_state"] = "unknown"
265
+
266
+ # Create memory object
267
+ memory = EmotionalMemory(
268
+ id=memory_id,
269
+ content=content,
270
+ emotional_weight=emotional_weight,
271
+ emotional_valence=emotional_valence,
272
+ emotions_felt=emotions_felt or [],
273
+ context=context,
274
+ timestamp=datetime.now().isoformat(),
275
+ user_id=self.user_id
276
+ )
277
+
278
+ # Store in cache
279
+ self._memories[memory_id] = memory
280
+ self._recent_memories.append(memory_id)
281
+
282
+ # Queue for consolidation if high emotion
283
+ if emotional_weight >= self.high_emotion_threshold:
284
+ self._consolidation_queue.append(memory_id)
285
+ print(f"[EmotionalMemory] High-emotion memory queued for consolidation: {content[:50]}...")
286
+
287
+ # Enforce storage limits
288
+ self._enforce_limits()
289
+
290
+ # Save to disk
291
+ self._save()
292
+
293
+ print(f"[EmotionalMemory] Encoded memory (weight={emotional_weight:.2f}): {content[:50]}...")
294
+
295
+ return memory
296
+
297
+ def retrieve_relevant(self,
298
+ query: str = None,
299
+ current_emotion: dict = None,
300
+ limit: int = 5,
301
+ min_weight: float = 0.0,
302
+ include_consolidated: bool = True) -> List[EmotionalMemory]:
303
+ """
304
+ Retrieve memories with emotional resonance scoring.
305
+
306
+ Args:
307
+ query: Optional search query for content matching
308
+ current_emotion: Current emotional state for resonance matching
309
+ limit: Maximum number of memories to return
310
+ min_weight: Minimum emotional weight threshold
311
+ include_consolidated: Whether to include consolidated memories
312
+
313
+ Returns:
314
+ List of EmotionalMemory objects sorted by relevance
315
+ """
316
+ with self._lock:
317
+ candidates = []
318
+
319
+ for memory in self._memories.values():
320
+ # Apply filters
321
+ if memory.emotional_weight < min_weight:
322
+ continue
323
+
324
+ if not include_consolidated and memory.consolidated:
325
+ continue
326
+
327
+ # Calculate retrieval score
328
+ score = memory.get_retrieval_score(current_emotion)
329
+
330
+ # Boost score for query matching
331
+ if query and query.lower() in memory.content.lower():
332
+ score += 0.3
333
+
334
+ candidates.append((memory, score))
335
+
336
+ # Sort by score descending
337
+ candidates.sort(key=lambda x: x[1], reverse=True)
338
+
339
+ # Get top memories
340
+ results = []
341
+ for memory, score in candidates[:limit]:
342
+ # Update access tracking
343
+ memory.access_count += 1
344
+ memory.last_accessed = datetime.now().isoformat()
345
+ results.append(memory)
346
+
347
+ # Save updated access counts
348
+ if results:
349
+ self._save()
350
+
351
+ return results
352
+
353
+ def get_emotionally_similar_memories(self,
354
+ emotion_state: dict,
355
+ limit: int = 5,
356
+ valence_tolerance: float = 0.3) -> List[EmotionalMemory]:
357
+ """
358
+ Find memories matching current mood/emotional state.
359
+
360
+ This implements "state-dependent memory" - memories are easier
361
+ to retrieve when in a similar emotional state.
362
+
363
+ Args:
364
+ emotion_state: Current emotional state (valence, arousal, emotion names)
365
+ limit: Maximum memories to return
366
+ valence_tolerance: How close valence must match
367
+
368
+ Returns:
369
+ List of emotionally similar memories
370
+ """
371
+ with self._lock:
372
+ # Extract current valence
373
+ current_valence = emotion_state.get("valence", 0)
374
+ current_arousal = emotion_state.get("arousal", 0.5)
375
+
376
+ # Get named emotions
377
+ active_emotions = set()
378
+ if "emotions" in emotion_state:
379
+ active_emotions = {e for e, v in emotion_state["emotions"].items() if v > 0.3}
380
+
381
+ candidates = []
382
+
383
+ for memory in self._memories.values():
384
+ # Check valence similarity
385
+ valence_diff = abs(memory.emotional_valence - current_valence)
386
+ if valence_diff > valence_tolerance:
387
+ continue
388
+
389
+ # Calculate similarity score
390
+ similarity = memory._calculate_emotional_similarity(emotion_state)
391
+
392
+ # Boost for matching emotion names
393
+ if active_emotions and memory.emotions_felt:
394
+ overlap = len(active_emotions & set(memory.emotions_felt))
395
+ similarity += overlap * 0.1
396
+
397
+ # Apply emotional weight as multiplier
398
+ final_score = similarity * (0.5 + memory.emotional_weight * 0.5)
399
+
400
+ candidates.append((memory, final_score))
401
+
402
+ # Sort by score
403
+ candidates.sort(key=lambda x: x[1], reverse=True)
404
+
405
+ return [m for m, _ in candidates[:limit]]
406
+
407
+ def consolidate_recent_memories(self,
408
+ max_age_hours: float = 24,
409
+ min_weight: float = 0.5) -> int:
410
+ """
411
+ Process recent memories into long-term patterns.
412
+
413
+ This implements "memory consolidation" - important recent memories
414
+ are strengthened and linked together.
415
+
416
+ Args:
417
+ max_age_hours: Only consolidate memories newer than this
418
+ min_weight: Minimum weight to consider for consolidation
419
+
420
+ Returns:
421
+ Number of memories consolidated
422
+ """
423
+ with self._lock:
424
+ consolidated_count = 0
425
+
426
+ # Find recent, significant, unconsolidated memories
427
+ candidates = []
428
+ for memory_id, memory in self._memories.items():
429
+ if memory.consolidated:
430
+ continue
431
+ if memory.age_hours() > max_age_hours:
432
+ continue
433
+ if memory.emotional_weight < min_weight:
434
+ continue
435
+ candidates.append(memory)
436
+
437
+ # Process consolidation queue first (high-emotion memories)
438
+ queue_ids = set(self._consolidation_queue)
439
+ high_priority = [m for m in candidates if m.id in queue_ids]
440
+ normal_priority = [m for m in candidates if m.id not in queue_ids]
441
+
442
+ # Consolidate high priority first
443
+ for memory in high_priority + normal_priority:
444
+ # Mark as consolidated
445
+ memory.consolidated = True
446
+
447
+ # Calculate importance boost based on:
448
+ # - Emotional weight
449
+ # - Access count
450
+ # - Age (slightly older = more stable)
451
+ importance = (
452
+ memory.emotional_weight * 0.5 +
453
+ min(0.3, memory.access_count * 0.03) +
454
+ min(0.2, memory.age_hours() / 168) # Up to 0.2 after a week
455
+ )
456
+ memory.importance_boost = min(1.0, importance)
457
+
458
+ consolidated_count += 1
459
+ print(f"[EmotionalMemory] Consolidated: {memory.content[:50]}... (boost={memory.importance_boost:.2f})")
460
+
461
+ # Clear consolidation queue
462
+ self._consolidation_queue = []
463
+
464
+ # Save changes
465
+ if consolidated_count > 0:
466
+ self._save()
467
+
468
+ return consolidated_count
469
+
470
+ def get_memory_prompt_section(self,
471
+ user_id: str = None,
472
+ current_emotion: dict = None,
473
+ max_memories: int = 5,
474
+ max_tokens: int = 500) -> str:
475
+ """
476
+ Format relevant memories for LLM context.
477
+
478
+ This provides emotionally-weighted memory context to the LLM,
479
+ prioritizing high-emotion and mood-matching memories.
480
+
481
+ Args:
482
+ user_id: Optional user filter (uses instance user_id if not provided)
483
+ current_emotion: Current emotional state for matching
484
+ max_memories: Maximum memories to include
485
+ max_tokens: Approximate token limit for output
486
+
487
+ Returns:
488
+ Formatted string for LLM prompt
489
+ """
490
+ with self._lock:
491
+ # Get emotionally relevant memories
492
+ memories = self.retrieve_relevant(
493
+ query=None,
494
+ current_emotion=current_emotion,
495
+ limit=max_memories * 2, # Get more for filtering
496
+ min_weight=0.3
497
+ )
498
+
499
+ if not memories:
500
+ return ""
501
+
502
+ # Build prompt section
503
+ sections = []
504
+ sections.append("EMOTIONAL MEMORIES (significant moments):")
505
+
506
+ char_estimate = 0
507
+ included_count = 0
508
+
509
+ for memory in memories:
510
+ # Format single memory
511
+ valence_indicator = "+" if memory.emotional_valence > 0.3 else ("-" if memory.emotional_valence < -0.3 else "~")
512
+
513
+ # Time context
514
+ age = memory.age_hours()
515
+ if age < 1:
516
+ time_str = "very recently"
517
+ elif age < 24:
518
+ time_str = f"{int(age)} hours ago"
519
+ elif age < 168:
520
+ time_str = f"{int(age/24)} days ago"
521
+ else:
522
+ time_str = f"{int(age/168)} weeks ago"
523
+
524
+ # Emotions felt
525
+ emotions_str = ""
526
+ if memory.emotions_felt:
527
+ emotions_str = f" [felt: {', '.join(memory.emotions_felt[:3])}]"
528
+
529
+ # Context
530
+ context_str = ""
531
+ if memory.context.get("conversation_topic"):
532
+ context_str = f" (about: {memory.context['conversation_topic']})"
533
+
534
+ line = f" {valence_indicator} [{time_str}] {memory.content[:100]}{emotions_str}{context_str}"
535
+
536
+ # Check token limit (rough estimate: 1 token ~ 4 chars)
537
+ if char_estimate + len(line) > max_tokens * 4:
538
+ break
539
+
540
+ sections.append(line)
541
+ char_estimate += len(line)
542
+ included_count += 1
543
+
544
+ if included_count >= max_memories:
545
+ break
546
+
547
+ if included_count == 0:
548
+ return ""
549
+
550
+ return "\n".join(sections)
551
+
552
+ def tag_emotional_moment(self,
553
+ content: str,
554
+ intensity: float,
555
+ emotion_type: str = None,
556
+ context: dict = None) -> EmotionalMemory:
557
+ """
558
+ Mark a significant emotional moment for strong encoding.
559
+
560
+ This is the "tag and capture" mechanism - highly emotional
561
+ moments get stronger memory encoding.
562
+
563
+ Args:
564
+ content: What happened
565
+ intensity: 0-1 intensity of the emotion
566
+ emotion_type: Type of emotion (love, desire, joy, hurt, etc.)
567
+ context: Additional context
568
+
569
+ Returns:
570
+ The created high-emotion memory
571
+ """
572
+ # Map emotion types to valence
573
+ valence_map = {
574
+ "love": 0.9,
575
+ "joy": 0.8,
576
+ "excited": 0.7,
577
+ "happy": 0.7,
578
+ "desire": 0.6,
579
+ "arousal": 0.5,
580
+ "romantic": 0.7,
581
+ "flirty": 0.6,
582
+ "calm": 0.3,
583
+ "content": 0.5,
584
+ "sad": -0.6,
585
+ "hurt": -0.7,
586
+ "angry": -0.8,
587
+ "fear": -0.6,
588
+ "anxious": -0.4,
589
+ "vulnerable": -0.2,
590
+ }
591
+
592
+ # Determine valence
593
+ valence = valence_map.get(emotion_type, 0.0) if emotion_type else 0.0
594
+
595
+ # High-intensity moments get boosted weight
596
+ weight = min(1.0, intensity * 1.2)
597
+
598
+ # Create emotions list
599
+ emotions_felt = [emotion_type] if emotion_type else []
600
+
601
+ # Add intensity to context
602
+ if context is None:
603
+ context = {}
604
+ context["moment_type"] = "emotional_peak"
605
+ context["intensity"] = intensity
606
+
607
+ return self.encode_memory(
608
+ content=content,
609
+ emotional_weight=weight,
610
+ emotional_valence=valence,
611
+ emotions_felt=emotions_felt,
612
+ context=context
613
+ )
614
+
615
+ # ============================================================
616
+ # Additional Utility Methods
617
+ # ============================================================
618
+
619
+ def get_memory_by_id(self, memory_id: str) -> Optional[EmotionalMemory]:
620
+ """Retrieve a specific memory by ID"""
621
+ return self._memories.get(memory_id)
622
+
623
+ def get_recent_high_emotion(self,
624
+ hours: float = 24,
625
+ limit: int = 5) -> List[EmotionalMemory]:
626
+ """Get recent high-emotion memories"""
627
+ with self._lock:
628
+ candidates = [
629
+ m for m in self._memories.values()
630
+ if m.age_hours() <= hours and m.emotional_weight >= self.high_emotion_threshold
631
+ ]
632
+ candidates.sort(key=lambda m: m.timestamp, reverse=True)
633
+ return candidates[:limit]
634
+
635
+ def get_emotional_summary(self, hours: float = 24) -> dict:
636
+ """
637
+ Get a summary of recent emotional memory patterns.
638
+ Useful for understanding overall emotional state.
639
+ """
640
+ with self._lock:
641
+ recent = [m for m in self._memories.values() if m.age_hours() <= hours]
642
+
643
+ if not recent:
644
+ return {"count": 0, "average_weight": 0, "dominant_valence": "neutral"}
645
+
646
+ avg_weight = sum(m.emotional_weight for m in recent) / len(recent)
647
+ avg_valence = sum(m.emotional_valence for m in recent) / len(recent)
648
+
649
+ # Find dominant emotions
650
+ emotion_counts = {}
651
+ for m in recent:
652
+ for e in m.emotions_felt:
653
+ emotion_counts[e] = emotion_counts.get(e, 0) + 1
654
+
655
+ dominant_emotions = sorted(emotion_counts.items(), key=lambda x: x[1], reverse=True)[:3]
656
+
657
+ return {
658
+ "count": len(recent),
659
+ "average_weight": round(avg_weight, 2),
660
+ "average_valence": round(avg_valence, 2),
661
+ "dominant_valence": "positive" if avg_valence > 0.2 else ("negative" if avg_valence < -0.2 else "neutral"),
662
+ "dominant_emotions": [e for e, _ in dominant_emotions],
663
+ "high_emotion_count": len([m for m in recent if m.emotional_weight >= self.high_emotion_threshold])
664
+ }
665
+
666
+ def strengthen_memory(self, memory_id: str, boost: float = 0.1) -> bool:
667
+ """
668
+ Strengthen a memory (like remembering/rehearsing it).
669
+ Implements memory strengthening through recall.
670
+ """
671
+ with self._lock:
672
+ if memory_id not in self._memories:
673
+ return False
674
+
675
+ memory = self._memories[memory_id]
676
+ memory.emotional_weight = min(1.0, memory.emotional_weight + boost)
677
+ memory.access_count += 1
678
+ memory.last_accessed = datetime.now().isoformat()
679
+
680
+ self._save()
681
+ return True
682
+
683
+ def link_memories(self, memory_id1: str, memory_id2: str, link_type: str = "related") -> bool:
684
+ """
685
+ Create an associative link between two memories.
686
+ This implements memory association/priming.
687
+ """
688
+ with self._lock:
689
+ if memory_id1 not in self._memories or memory_id2 not in self._memories:
690
+ return False
691
+
692
+ # Add link to context
693
+ mem1 = self._memories[memory_id1]
694
+ mem2 = self._memories[memory_id2]
695
+
696
+ if "linked_memories" not in mem1.context:
697
+ mem1.context["linked_memories"] = []
698
+ if "linked_memories" not in mem2.context:
699
+ mem2.context["linked_memories"] = []
700
+
701
+ mem1.context["linked_memories"].append({"id": memory_id2, "type": link_type})
702
+ mem2.context["linked_memories"].append({"id": memory_id1, "type": link_type})
703
+
704
+ self._save()
705
+ return True
706
+
707
+ def clear_old_memories(self, max_age_days: float = 90, keep_weight_above: float = 0.8) -> int:
708
+ """
709
+ Clean up old, less significant memories.
710
+ High-weight memories are preserved.
711
+ """
712
+ with self._lock:
713
+ to_remove = []
714
+
715
+ for memory_id, memory in self._memories.items():
716
+ # Always keep high-weight memories
717
+ if memory.emotional_weight >= keep_weight_above:
718
+ continue
719
+
720
+ # Remove old, low-weight memories
721
+ if memory.age_days() > max_age_days:
722
+ to_remove.append(memory_id)
723
+
724
+ for memory_id in to_remove:
725
+ del self._memories[memory_id]
726
+
727
+ if to_remove:
728
+ self._save()
729
+ print(f"[EmotionalMemory] Cleared {len(to_remove)} old memories")
730
+
731
+ return len(to_remove)
732
+
733
+ def get_stats(self) -> dict:
734
+ """Get system statistics"""
735
+ with self._lock:
736
+ if not self._memories:
737
+ return {
738
+ "total_memories": 0,
739
+ "user_id": self.user_id,
740
+ "storage_path": str(self.user_storage_file)
741
+ }
742
+
743
+ weights = [m.emotional_weight for m in self._memories.values()]
744
+ valences = [m.emotional_valence for m in self._memories.values()]
745
+
746
+ return {
747
+ "total_memories": len(self._memories),
748
+ "user_id": self.user_id,
749
+ "storage_path": str(self.user_storage_file),
750
+ "average_weight": round(sum(weights) / len(weights), 2),
751
+ "average_valence": round(sum(valences) / len(valences), 2),
752
+ "high_emotion_count": len([w for w in weights if w >= self.high_emotion_threshold]),
753
+ "consolidated_count": len([m for m in self._memories.values() if m.consolidated]),
754
+ "pending_consolidation": len(self._consolidation_queue),
755
+ "oldest_memory_days": round(max(m.age_days() for m in self._memories.values()), 1),
756
+ "config": {
757
+ "max_stored": self.max_stored,
758
+ "high_emotion_threshold": self.high_emotion_threshold,
759
+ "decay_days": self.decay_days
760
+ }
761
+ }
762
+
763
+ # ============================================================
764
+ # Private Methods
765
+ # ============================================================
766
+
767
+ def _get_time_of_day(self) -> str:
768
+ """Get current time of day category"""
769
+ hour = datetime.now().hour
770
+ if 5 <= hour < 12:
771
+ return "morning"
772
+ elif 12 <= hour < 17:
773
+ return "afternoon"
774
+ elif 17 <= hour < 21:
775
+ return "evening"
776
+ else:
777
+ return "night"
778
+
779
+ def _enforce_limits(self):
780
+ """Enforce maximum storage limits by removing lowest-weight memories"""
781
+ if len(self._memories) <= self.max_stored:
782
+ return
783
+
784
+ # Sort by emotional weight + recency
785
+ def sort_key(m):
786
+ recency = math.exp(-m.age_days() / self.decay_days)
787
+ return m.emotional_weight * 0.7 + recency * 0.3
788
+
789
+ sorted_memories = sorted(self._memories.values(), key=sort_key)
790
+
791
+ # Remove lowest scoring memories
792
+ to_remove = len(self._memories) - self.max_stored
793
+ for memory in sorted_memories[:to_remove]:
794
+ del self._memories[memory.id]
795
+
796
+ print(f"[EmotionalMemory] Removed {to_remove} low-priority memories to enforce limit")
797
+
798
+ def _load(self):
799
+ """Load memories from disk"""
800
+ if not self.user_storage_file.exists():
801
+ return
802
+
803
+ try:
804
+ with open(self.user_storage_file, 'r') as f:
805
+ data = json.load(f)
806
+
807
+ for memory_data in data.get("memories", []):
808
+ try:
809
+ memory = EmotionalMemory.from_dict(memory_data)
810
+ self._memories[memory.id] = memory
811
+ except Exception as e:
812
+ print(f"[EmotionalMemory] Error loading memory: {e}")
813
+
814
+ # Load consolidation queue
815
+ self._consolidation_queue = data.get("consolidation_queue", [])
816
+
817
+ print(f"[EmotionalMemory] Loaded {len(self._memories)} memories from disk")
818
+
819
+ except Exception as e:
820
+ print(f"[EmotionalMemory] Error loading memories: {e}")
821
+
822
+ def _save(self):
823
+ """Save memories to disk"""
824
+ try:
825
+ data = {
826
+ "user_id": self.user_id,
827
+ "last_updated": datetime.now().isoformat(),
828
+ "memories": [m.to_dict() for m in self._memories.values()],
829
+ "consolidation_queue": self._consolidation_queue
830
+ }
831
+
832
+ with open(self.user_storage_file, 'w') as f:
833
+ json.dump(data, f, indent=2)
834
+
835
+ except Exception as e:
836
+ print(f"[EmotionalMemory] Error saving memories: {e}")
837
+
838
+
839
+ # ============================================================
840
+ # Singleton Management
841
+ # ============================================================
842
+
843
+ _instances: Dict[str, EmotionalMemorySystem] = {}
844
+ _instances_lock = threading.Lock()
845
+
846
+
847
+ def get_emotional_memory_system(user_id: str = "default",
848
+ data_path: Path = None) -> EmotionalMemorySystem:
849
+ """
850
+ Get or create an EmotionalMemorySystem instance for a user.
851
+
852
+ This implements a per-user singleton pattern.
853
+
854
+ Args:
855
+ user_id: User identifier
856
+ data_path: Optional custom data path
857
+
858
+ Returns:
859
+ EmotionalMemorySystem instance for the user
860
+ """
861
+ with _instances_lock:
862
+ if user_id not in _instances:
863
+ _instances[user_id] = EmotionalMemorySystem(
864
+ data_path=data_path,
865
+ user_id=user_id
866
+ )
867
+ return _instances[user_id]
868
+
869
+
870
+ def reset_emotional_memory_system(user_id: str = None):
871
+ """
872
+ Reset the singleton instances (mainly for testing).
873
+
874
+ Args:
875
+ user_id: Specific user to reset, or None for all
876
+ """
877
+ with _instances_lock:
878
+ if user_id:
879
+ _instances.pop(user_id, None)
880
+ else:
881
+ _instances.clear()
882
+
883
+
884
+ # ============================================================
885
+ # Integration Helper Functions
886
+ # ============================================================
887
+
888
+ def create_from_conversation(content: str,
889
+ emotion_data: dict,
890
+ context: dict = None,
891
+ user_id: str = "default") -> Optional[EmotionalMemory]:
892
+ """
893
+ Convenience function to create emotional memory from conversation data.
894
+
895
+ Args:
896
+ content: Conversation content
897
+ emotion_data: Emotion data from emotional state system
898
+ context: Additional context
899
+ user_id: User identifier
900
+
901
+ Returns:
902
+ Created memory or None
903
+ """
904
+ system = get_emotional_memory_system(user_id)
905
+
906
+ # Extract emotional weight from emotion data
907
+ weight = 0.5
908
+ valence = 0.0
909
+ emotions = []
910
+
911
+ if emotion_data:
912
+ # Get highest emotion intensity as weight
913
+ emotion_values = {k: v for k, v in emotion_data.items()
914
+ if isinstance(v, (int, float)) and k not in ["valence", "arousal"]}
915
+ if emotion_values:
916
+ weight = max(emotion_values.values())
917
+ emotions = [k for k, v in emotion_values.items() if v >= 0.3]
918
+
919
+ valence = emotion_data.get("valence", 0.0)
920
+
921
+ return system.encode_memory(
922
+ content=content,
923
+ emotional_weight=weight,
924
+ emotional_valence=valence,
925
+ emotions_felt=emotions,
926
+ context=context
927
+ )
928
+
929
+
930
+ def get_memory_context_for_llm(user_id: str = "default",
931
+ current_emotion: dict = None,
932
+ max_memories: int = 5) -> str:
933
+ """
934
+ Convenience function to get memory context for LLM prompts.
935
+
936
+ Args:
937
+ user_id: User identifier
938
+ current_emotion: Current emotional state
939
+ max_memories: Maximum memories to include
940
+
941
+ Returns:
942
+ Formatted memory context string
943
+ """
944
+ system = get_emotional_memory_system(user_id)
945
+ return system.get_memory_prompt_section(
946
+ user_id=user_id,
947
+ current_emotion=current_emotion,
948
+ max_memories=max_memories
949
+ )