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.
- package/Dockerfile +24 -0
- package/LICENSE +21 -0
- package/README.md +143 -0
- package/alive_ai/__init__.py +3 -0
- package/brain/__init__.py +59 -0
- package/brain/almost_said.py +154 -0
- package/brain/bid_detector.py +636 -0
- package/brain/conversation_flow.py +135 -0
- package/brain/curiosity.py +328 -0
- package/brain/default_mode.py +1438 -0
- package/brain/dreams.py +220 -0
- package/brain/embeddings/__init__.py +82 -0
- package/brain/emotional_memory.py +949 -0
- package/brain/global_activity.py +173 -0
- package/brain/group_dynamics.py +63 -0
- package/brain/linguistic.py +235 -0
- package/brain/llm/__init__.py +63 -0
- package/brain/llm/base.py +33 -0
- package/brain/llm/fallback_router.py +309 -0
- package/brain/llm/manifest.md +30 -0
- package/brain/llm/ollama.py +218 -0
- package/brain/llm/openrouter.py +151 -0
- package/brain/llm/provider.py +205 -0
- package/brain/llm/unified.py +423 -0
- package/brain/llm/zai.py +169 -0
- package/brain/manifest.md +23 -0
- package/brain/memory/__init__.py +123 -0
- package/brain/memory/episodic.py +92 -0
- package/brain/memory/fact_extractor.py +209 -0
- package/brain/memory/index.py +54 -0
- package/brain/memory/manager.py +151 -0
- package/brain/memory/summarizer.py +102 -0
- package/brain/memory/vector_store.py +297 -0
- package/brain/memory/working.py +43 -0
- package/brain/narrative.py +343 -0
- package/brain/stt/__init__.py +4 -0
- package/brain/stt/google_stt.py +83 -0
- package/brain/stt/whisper_stt.py +82 -0
- package/brain/subconscious/__init__.py +33 -0
- package/brain/subconscious/actions.py +136 -0
- package/brain/subconscious/evaluation.py +166 -0
- package/brain/subconscious/goal_system.py +90 -0
- package/brain/subconscious/goals.py +41 -0
- package/brain/subconscious/impulse_generator.py +200 -0
- package/brain/subconscious/impulses.py +48 -0
- package/brain/subconscious/learning.py +24 -0
- package/brain/subconscious/learning_system.py +79 -0
- package/brain/subconscious/loop.py +398 -0
- package/brain/subconscious/manifest.md +32 -0
- package/brain/subconscious/relationship.py +47 -0
- package/brain/subconscious/relationship_memory.py +83 -0
- package/brain/subconscious/response_analyzer.py +74 -0
- package/brain/subconscious/templates.py +70 -0
- package/brain/subconscious/thought.py +37 -0
- package/brain/subconscious/working_memory.py +97 -0
- package/cli/index.js +371 -0
- package/config/directives.example.json +28 -0
- package/config/instructions.example.md +16 -0
- package/config/self.example.json +74 -0
- package/config/settings.example.json +95 -0
- package/core/__init__.py +1 -0
- package/core/config.py +54 -0
- package/core/directives.py +198 -0
- package/core/events.py +50 -0
- package/core/follow_up.py +267 -0
- package/core/hot_reload.py +174 -0
- package/core/initialization.py +253 -0
- package/core/manifest.md +28 -0
- package/core/media_handler.py +241 -0
- package/core/memory_monitor.py +200 -0
- package/core/message_handler.py +1440 -0
- package/core/proactive_generator.py +277 -0
- package/core/self.py +188 -0
- package/core/settings.py +169 -0
- package/core/skills_registry.py +357 -0
- package/core/state.py +27 -0
- package/core/subconscious_bridge.py +93 -0
- package/core/thinking.py +175 -0
- package/core/user_manager.py +306 -0
- package/core/user_tracker.py +144 -0
- package/demo/index.html +144 -0
- package/docker-compose.yml +28 -0
- package/docs/assets/logo.svg +15 -0
- package/docs/index.html +355 -0
- package/heart/__init__.py +93 -0
- package/heart/afterglow.py +215 -0
- package/heart/attachment.py +186 -0
- package/heart/circadian.py +251 -0
- package/heart/complex_emotions.py +114 -0
- package/heart/conflicts.py +589 -0
- package/heart/core.py +387 -0
- package/heart/emotional_decay.py +59 -0
- package/heart/emotional_memory.py +261 -0
- package/heart/emotional_state.py +146 -0
- package/heart/emotional_variability.py +156 -0
- package/heart/hormonal.py +424 -0
- package/heart/inconsistency.py +1222 -0
- package/heart/integrity.py +469 -0
- package/heart/interoception.py +997 -0
- package/heart/love.py +120 -0
- package/heart/manifest.md +25 -0
- package/heart/mood_shifts.py +169 -0
- package/heart/phantom_somatic.py +259 -0
- package/heart/predictive.py +374 -0
- package/heart/scars.py +474 -0
- package/heart/somatic.py +482 -0
- package/heart/soul.py +633 -0
- package/heart/telemetry.py +942 -0
- package/heart/triggers.py +119 -0
- package/heart/unconscious.py +443 -0
- package/input/__init__.py +1 -0
- package/input/manifest.md +24 -0
- package/input/telegram/__init__.py +1 -0
- package/input/telegram/commands.py +762 -0
- package/input/telegram/listener.py +532 -0
- package/main.py +90 -0
- package/manifest.md +28 -0
- package/mypics/.gitkeep +1 -0
- package/myvids/.gitkeep +1 -0
- package/output/__init__.py +1 -0
- package/output/images/__init__.py +1 -0
- package/output/images/fal_gen.py +43 -0
- package/output/manifest.md +26 -0
- package/output/text/__init__.py +1 -0
- package/output/text/sender.py +22 -0
- package/output/voice/__init__.py +64 -0
- package/output/voice/google_tts.py +252 -0
- package/output/voice/gtts_tts.py +214 -0
- package/output/voice/vibe_tts.py +190 -0
- package/package.json +58 -0
- package/pyproject.toml +23 -0
- package/requirements.txt +21 -0
- package/skills/__init__.py +1 -0
- package/skills/anticipation_engine/__init__.py +8 -0
- package/skills/anticipation_engine/engine.py +618 -0
- package/skills/anticipation_engine/manifest.md +192 -0
- package/skills/calendar/__init__.py +1 -0
- package/skills/content_unlocks/__init__.py +8 -0
- package/skills/content_unlocks/manifest.md +231 -0
- package/skills/content_unlocks/unlocks.py +945 -0
- package/skills/exclusive_moments/__init__.py +8 -0
- package/skills/exclusive_moments/manifest.md +145 -0
- package/skills/exclusive_moments/moments.py +506 -0
- package/skills/intimacy_layers/__init__.py +8 -0
- package/skills/intimacy_layers/layers.py +703 -0
- package/skills/intimacy_layers/manifest.md +203 -0
- package/skills/manifest.md +67 -0
- package/skills/memory_callbacks/__init__.py +9 -0
- package/skills/memory_callbacks/callbacks.py +748 -0
- package/skills/memory_callbacks/manifest.md +170 -0
- package/skills/message_scheduler/__init__.py +19 -0
- package/skills/message_scheduler/manifest.md +107 -0
- package/skills/message_scheduler/scheduler.py +510 -0
- package/skills/photo_manager/__init__.py +1 -0
- package/skills/photo_manager/scanner.py +296 -0
- package/skills/relationship_milestones/__init__.py +8 -0
- package/skills/relationship_milestones/manifest.md +206 -0
- package/skills/relationship_milestones/tracker.py +494 -0
- package/skills/self_authorship/__init__.py +23 -0
- package/skills/self_authorship/author.py +331 -0
- package/skills/self_authorship/manifest.md +24 -0
- package/skills/video_manager/__init__.py +5 -0
- package/skills/video_manager/manifest.md +37 -0
- package/skills/video_manager/scanner.py +229 -0
- package/webui/__init__.py +3 -0
- package/webui/app.py +936 -0
- package/webui/bridge.py +366 -0
- 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
|
+
)
|