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,28 @@
1
+ # Core - Essential Systems
2
+
3
+ The foundation of the AI. Always required.
4
+
5
+ ## Files
6
+ - `events.py` - NervousSystem (event bus for module communication)
7
+ - `config.py` - Configuration loader
8
+ - `state.py` - Global state management
9
+ - `self.py` - The Self (main coordinator)
10
+
11
+ ## Key Integration Points
12
+ - **NervousSystem** - Central event bus; all modules communicate via events
13
+ - **Self** - Coordinates: memory, heart, LLM, subconscious, input/output
14
+ - Connects to subconscious for proactive messaging
15
+ - Emits: `message_received`, `send_text`, `send_voice_file`, `send_image`, `send_video`
16
+
17
+ ## Usage
18
+ ```python
19
+ from core.self import Self
20
+ ai = Self(Path("."))
21
+ await ai.start() # Starts all modules + subconscious loop
22
+ ```
23
+
24
+ ## Events
25
+ - `message_received` - Incoming message
26
+ - `memory_save` - Store memory
27
+ - `timer_tick` - Emotion decay trigger
28
+ - `subconscious_impulse` - Proactive action signal
@@ -0,0 +1,241 @@
1
+ """Core: Media Handler — Photo/video sending logic"""
2
+ import random
3
+ import time
4
+
5
+
6
+ async def handle_media_sending(self, text: str, emotion: dict, chat_id, response: str):
7
+ video_request = [
8
+ "send video", "send me a video", "send a video", "show me a video",
9
+ "video please", "i want a video", "give me a video", "record a video",
10
+ "make a video", "video for me"
11
+ ]
12
+ msg_lower = text.lower().strip()
13
+ wants_video = any(vr in msg_lower for vr in video_request)
14
+ photo = await _handle_photo(self, text, emotion, chat_id, response, wants_video)
15
+ video = await _handle_video(self, text, emotion, chat_id, photo is not None, wants_video)
16
+ return photo, video
17
+
18
+
19
+ async def _handle_photo(self, text, emotion, chat_id, response, wants_video):
20
+ if not self._photos or wants_video or len(self._photos.get_all()) == 0:
21
+ return None
22
+ if not _check_photo_triggers(text, emotion, self):
23
+ return None
24
+ return await _send_photo(self, text, emotion, chat_id, response)
25
+
26
+
27
+ def _check_photo_triggers(text: str, emotion: dict, self) -> bool:
28
+ """Check if photo should be sent — smart, relationship-aware, with cooldown"""
29
+ from core.settings import get_int, get_percent
30
+
31
+ msg = text.lower().strip()
32
+
33
+ # ========== NEGATIVE DETECTION ==========
34
+ negative_patterns = [
35
+ "no photo", "no pic", "no video", "don't send", "dont send",
36
+ "not now", "stop send", "too many", "enough photo", "enough pic",
37
+ "i don't want", "dont want", "not in the mood", "not today",
38
+ "no more photo", "no more pic", "quit it"
39
+ ]
40
+ if any(neg in msg for neg in negative_patterns):
41
+ print(f"[Photo] Negative context detected, skipping")
42
+ return False
43
+
44
+ # ========== COOLDOWN CHECK (configurable) ==========
45
+ last_photo_time = getattr(self, '_last_photo_time', 0)
46
+ cooldown = get_int("MEDIA_COOLDOWN_PHOTO", 60) # Reduced to 1 minute for owner
47
+ if time.time() - last_photo_time < cooldown:
48
+ remaining = int(cooldown - (time.time() - last_photo_time))
49
+ print(f"[Photo] Cooldown active ({remaining}s remaining)")
50
+ return False
51
+
52
+ # ========== SESSION LIMIT (configurable) ==========
53
+ session_limit = get_int("MEDIA_SESSION_LIMIT_PHOTO", 10) # Increased
54
+ photos_sent = getattr(self, '_photos_sent_session', 0)
55
+ if photos_sent >= session_limit:
56
+ print(f"[Photo] Session limit reached ({session_limit})")
57
+ return False
58
+
59
+ # ========== RELATIONSHIP BUILDING - skip for owner ==========
60
+ if emotion.get("is_owner"):
61
+ print(f"[Photo] Owner detected - skipping interaction requirement")
62
+ else:
63
+ min_interactions = get_int("MEDIA_MIN_INTERACTIONS", 5) # Reduced from 15
64
+ if emotion.get("interaction_count", 0) < min_interactions:
65
+ print(f"[Photo] Need {min_interactions} interactions, have {emotion.get('interaction_count', 0)}")
66
+ return False
67
+
68
+ # ========== SMART REQUEST DETECTION ==========
69
+ # Only match intentional requests — never bare words like "pic" or "photo"
70
+ request_patterns = [
71
+ "send me a photo", "send me a pic", "send a photo", "send a pic",
72
+ "send photo", "send pic", "show me a photo", "show me a pic",
73
+ "show me what you", "show me yourself", "can i see you",
74
+ "let me see you", "i want to see you", "send me your",
75
+ "what are you wearing", "show me what you're wearing",
76
+ "take a photo", "take a pic", "take a selfie",
77
+ "selfie please", "photo please", "pic please",
78
+ "i want a photo", "i want a pic", "give me a photo", "give me a pic",
79
+ "need a photo", "need a pic", "send selfie", "send a selfie",
80
+ "give me a selfie", "gimme a selfie",
81
+ "send privates", "send private", "private pic",
82
+ "send me something expressive", "show me something expressive", "send something expressive",
83
+ "send me something naughty", "show me something naughty"
84
+ ]
85
+ if any(req in msg for req in request_patterns):
86
+ print(f"[Photo] User requested photo")
87
+ return True
88
+
89
+ # Short messages that are JUST a request word (solo "selfie", "pic?", "photo?")
90
+ stripped = msg.strip("?!. ")
91
+ if stripped in ("pic", "photo", "selfie", "privates", "send pic", "send photo"):
92
+ print(f"[Photo] Short direct request: '{stripped}'")
93
+ return True
94
+
95
+ # ========== INTIMATE CONTEXT ==========
96
+ intimate_kw = ["private", "close", "open up", "personal", "body", "tender", "affection"]
97
+ matches = sum(1 for k in intimate_kw if k in msg)
98
+ if matches >= 2 and emotion.get("desire", 0) > 0.7 and emotion.get("trust", 0.5) > 0.6:
99
+ print(f"[Photo] Intimate context ({matches} triggers, high desire/trust)")
100
+ return True
101
+
102
+ # ========== RANDOM (configurable chance) ==========
103
+ random_chance = get_percent("RANDOM_CHANCE_PHOTO", 8)
104
+ if emotion.get("is_high_desire") and emotion.get("is_in_love") and random.random() < random_chance:
105
+ print(f"[Photo] Random photo (high_desire + in love)")
106
+ return True
107
+
108
+ return False
109
+
110
+
111
+ async def _send_photo(self, text, emotion, chat_id, response):
112
+ """Send a photo and return photo info"""
113
+ photo = self._photos.get_for_context(
114
+ context=text + " " + response,
115
+ arousal=emotion.get("arousal", 0),
116
+ desire=emotion.get("desire", 0)
117
+ )
118
+ if not photo:
119
+ return None
120
+
121
+ photo_name, photo_desc, photo_cat = photo
122
+
123
+ # ========== DUPLICATE CHECK ==========
124
+ if self._photos.was_recently_sent(photo_name):
125
+ print(f"[Photo] Skipping recently sent: {photo_name}")
126
+ for _ in range(3):
127
+ photo = self._photos.get_for_context(
128
+ context=text + " " + response,
129
+ arousal=emotion.get("arousal", 0),
130
+ desire=emotion.get("desire", 0)
131
+ )
132
+ if photo and not self._photos.was_recently_sent(photo[0]):
133
+ photo_name, photo_desc, photo_cat = photo
134
+ break
135
+ else:
136
+ print(f"[Photo] No non-recent photos available")
137
+ return None
138
+
139
+ photo_path = str(self.base / "mypics" / photo_name)
140
+ self._photos.mark_sent(photo_name)
141
+
142
+ # ========== TRACK METRICS ==========
143
+ self._last_photo_time = time.time()
144
+ self._photos_sent_session = getattr(self, '_photos_sent_session', 0) + 1
145
+
146
+ await self.nervous.emit("chat_action_photo", {})
147
+ print(f"[Photo] Sending: {photo_name} (#{self._photos_sent_session} this session)")
148
+ await self.nervous.emit("send_image", {"file_path": photo_path, "chat_id": chat_id, "caption": ""})
149
+ return photo
150
+
151
+
152
+ async def _handle_video(self, text, emotion, chat_id, photo_sent, wants_video):
153
+ """Determine and send video if appropriate"""
154
+ from core.settings import get_int, get_percent
155
+
156
+ if not self._videos or len(self._videos.get_all()) == 0:
157
+ return None
158
+
159
+ msg = text.lower()
160
+
161
+ # ========== NEGATIVE DETECTION ==========
162
+ negative_patterns = ["no video", "don't send video", "not now", "stop"]
163
+ if any(neg in msg for neg in negative_patterns):
164
+ return None
165
+
166
+ # ========== COOLDOWN CHECK (configurable) ==========
167
+ last_video_time = getattr(self, '_last_video_time', 0)
168
+ cooldown = get_int("MEDIA_COOLDOWN_VIDEO", 600)
169
+ if time.time() - last_video_time < cooldown:
170
+ return None
171
+
172
+ # ========== SESSION LIMIT (configurable) ==========
173
+ session_limit = get_int("MEDIA_SESSION_LIMIT_VIDEO", 3)
174
+ videos_sent = getattr(self, '_videos_sent_session', 0)
175
+ if videos_sent >= session_limit:
176
+ return None
177
+
178
+ should_send = False
179
+ if wants_video:
180
+ should_send = True
181
+ print(f"[Video] User requested video")
182
+ elif not photo_sent:
183
+ # Random chance (configurable)
184
+ random_chance = get_percent("RANDOM_CHANCE_VIDEO", 5)
185
+ if emotion.get("is_high_desire") and emotion.get("is_in_love") and random.random() < random_chance:
186
+ should_send = True
187
+ print(f"[Video] Random video (high_desire + in love)")
188
+
189
+ if should_send:
190
+ return await _send_video(self, text, emotion, chat_id)
191
+ return None
192
+
193
+
194
+ def _check_video_triggers(text: str, emotion: dict, self) -> bool:
195
+ """Check if video should be sent"""
196
+ from core.settings import get_int, get_percent
197
+
198
+ msg = text.lower()
199
+
200
+ # Negative detection
201
+ if any(neg in msg for neg in ["no video", "don't send", "not now"]):
202
+ return False
203
+
204
+ # Cooldown
205
+ last_video_time = getattr(self, '_last_video_time', 0)
206
+ if time.time() - last_video_time < 600: # 10 min cooldown
207
+ return False
208
+
209
+ # Request patterns
210
+ video_request_patterns = [
211
+ "send video", "send me a video", "show me a video",
212
+ "video please", "i want a video", "give me a video"
213
+ ]
214
+ if any(vr in msg for vr in video_request_patterns):
215
+ return True
216
+
217
+ return False
218
+
219
+
220
+ async def _send_video(self, text, emotion, chat_id):
221
+ """Send a video and return video info"""
222
+ video = self._videos.get_for_context(text, emotion.get("desire", 0))
223
+ if not video:
224
+ return None
225
+
226
+ video_path, video_desc = video
227
+
228
+ if hasattr(self._videos, 'was_recently_sent') and self._videos.was_recently_sent(video_path):
229
+ print(f"[Video] Skipping recently sent: {video_path}")
230
+ return None
231
+
232
+ self._videos.mark_sent(video_path)
233
+
234
+ self._last_video_time = time.time()
235
+ self._videos_sent_session = getattr(self, '_videos_sent_session', 0) + 1
236
+
237
+ print(f"[Video] Sending: {video_path} (#{self._videos_sent_session} this session)")
238
+
239
+ await self.nervous.emit("chat_action_video", {})
240
+ await self.nervous.emit("send_video", {"file_path": video_path, "chat_id": chat_id, "caption": ""})
241
+ return video
@@ -0,0 +1,200 @@
1
+ """
2
+ Core: Memory Monitor
3
+ Monitors memory usage and triggers cleanup before OOM crashes.
4
+ """
5
+
6
+ import gc
7
+ import os
8
+ import psutil
9
+ from typing import Optional, Callable
10
+
11
+
12
+ class MemoryMonitor:
13
+ """
14
+ Monitors system memory and triggers cleanup when approaching limits.
15
+
16
+ Usage:
17
+ monitor = MemoryMonitor(max_memory_gb=5.0)
18
+ monitor.check() # Call periodically
19
+ """
20
+
21
+ def __init__(
22
+ self,
23
+ max_memory_gb: float = 5.0,
24
+ warning_threshold: float = 0.75, # Warn at 75%
25
+ critical_threshold: float = 0.90, # Force cleanup at 90%
26
+ on_warning: Optional[Callable] = None,
27
+ on_critical: Optional[Callable] = None
28
+ ):
29
+ self.max_memory_gb = max_memory_gb
30
+ self.warning_threshold = warning_threshold
31
+ self.critical_threshold = critical_threshold
32
+ self.on_warning = on_warning
33
+ self.on_critical = on_critical
34
+
35
+ self._last_warning_time = 0
36
+ self._warning_cooldown = 300 # 5 minutes between warnings
37
+
38
+ def get_memory_info(self) -> dict:
39
+ """Get current memory usage info"""
40
+ process = psutil.Process(os.getpid())
41
+ system_mem = psutil.virtual_memory()
42
+ process_rss_gb = process.memory_info().rss / (1024**3)
43
+
44
+ return {
45
+ # Process memory
46
+ "process_rss_gb": process_rss_gb,
47
+ "process_percent": process.memory_percent(),
48
+
49
+ # System memory
50
+ "system_total_gb": system_mem.total / (1024**3),
51
+ "system_used_gb": system_mem.used / (1024**3),
52
+ "system_available_gb": system_mem.available / (1024**3),
53
+ "system_percent": system_mem.percent,
54
+
55
+ # Our limit (based on PROCESS memory, not system)
56
+ "limit_gb": self.max_memory_gb,
57
+ "usage_of_limit": process_rss_gb / self.max_memory_gb,
58
+ }
59
+
60
+ def check(self) -> dict:
61
+ """
62
+ Check memory and trigger cleanup if needed.
63
+
64
+ Returns:
65
+ Dict with status and actions taken
66
+ """
67
+ info = self.get_memory_info()
68
+ usage_ratio = info["usage_of_limit"]
69
+ result = {
70
+ "info": info,
71
+ "status": "ok",
72
+ "actions": []
73
+ }
74
+
75
+ if usage_ratio >= self.critical_threshold:
76
+ # CRITICAL - Force aggressive cleanup
77
+ result["status"] = "critical"
78
+ print(f"[Memory] ⚠️ CRITICAL: Memory at {usage_ratio*100:.1f}% of limit!")
79
+
80
+ # Force garbage collection
81
+ collected = gc.collect(2) # Full collection
82
+ result["actions"].append(f"gc_collected_{collected}")
83
+
84
+ # Clear caches if callback provided (only call once)
85
+ if self.on_critical:
86
+ try:
87
+ self.on_critical()
88
+ result["actions"].append("critical_cleanup")
89
+ except Exception as e:
90
+ print(f"[Memory] Critical cleanup error: {e}")
91
+
92
+ elif usage_ratio >= self.warning_threshold:
93
+ # WARNING - Gentle cleanup
94
+ import time
95
+ current_time = time.time()
96
+
97
+ if current_time - self._last_warning_time > self._warning_cooldown:
98
+ result["status"] = "warning"
99
+ print(f"[Memory] ⚡ WARNING: Memory at {usage_ratio*100:.1f}% of limit")
100
+
101
+ # Light garbage collection
102
+ collected = gc.collect(1)
103
+ result["actions"].append(f"gc_collected_{collected}")
104
+
105
+ # Clear caches if callback provided
106
+ if self.on_warning:
107
+ try:
108
+ self.on_warning()
109
+ result["actions"].append("warning_cleanup")
110
+ except Exception as e:
111
+ print(f"[Memory] Warning cleanup error: {e}")
112
+
113
+ self._last_warning_time = current_time
114
+
115
+ return result
116
+
117
+ def force_cleanup(self) -> int:
118
+ """
119
+ Force aggressive memory cleanup.
120
+
121
+ Returns:
122
+ Number of objects collected
123
+ """
124
+ print("[Memory] Forcing aggressive cleanup...")
125
+ collected = gc.collect(2)
126
+ gc.collect() # Run again to clean circular refs
127
+
128
+ # Get memory after cleanup
129
+ info = self.get_memory_info()
130
+ print(f"[Memory] After cleanup: {info['process_rss_gb']:.2f}GB process, {info['system_used_gb']:.2f}GB system")
131
+
132
+ return collected
133
+
134
+
135
+ def clear_alive_ai_caches():
136
+ """Clear all Alive-AI caches to free memory"""
137
+ total_cleared = 0
138
+
139
+ # Clear message handler caches
140
+ try:
141
+ from core.message_handler import _user_memories, _recent_openings, _message_queue
142
+ count = len(_user_memories)
143
+ _user_memories.clear()
144
+ total_cleared += count
145
+ print(f"[Memory] Cleared {count} cached user memories")
146
+ count = len(_recent_openings)
147
+ _recent_openings.clear()
148
+ total_cleared += count
149
+ count = len(_message_queue)
150
+ _message_queue.clear()
151
+ total_cleared += count
152
+ except Exception as e:
153
+ print(f"[Memory] Could not clear message handler caches: {e}")
154
+
155
+ # Clear embedding caches if possible
156
+ try:
157
+ from brain.embeddings import get_embedding_service
158
+ service = get_embedding_service()
159
+ if hasattr(service, '_cache'):
160
+ cache_len = len(service._cache) if hasattr(service._cache, '__len__') else 0
161
+ service._cache.clear()
162
+ total_cleared += cache_len
163
+ print(f"[Memory] Cleared embedding cache ({cache_len} items)")
164
+ except Exception:
165
+ pass
166
+
167
+ # Clear emotional memory instances
168
+ try:
169
+ from brain.emotional_memory import _instances
170
+ count = len(_instances)
171
+ _instances.clear()
172
+ total_cleared += count
173
+ print(f"[Memory] Cleared {count} emotional memory instances")
174
+ except Exception:
175
+ pass
176
+
177
+ # Clear fact extractor buffers
178
+ try:
179
+ from brain.memory.fact_extractor import FactExtractor
180
+ # Instance-based, would need global registry
181
+ except Exception:
182
+ pass
183
+
184
+ return total_cleared
185
+
186
+
187
+ # Global monitor instance
188
+ _monitor: Optional[MemoryMonitor] = None
189
+
190
+
191
+ def get_memory_monitor(max_memory_gb: float = 5.0) -> MemoryMonitor:
192
+ """Get or create the global memory monitor"""
193
+ global _monitor
194
+ if _monitor is None:
195
+ _monitor = MemoryMonitor(
196
+ max_memory_gb=max_memory_gb,
197
+ on_warning=clear_alive_ai_caches,
198
+ on_critical=clear_alive_ai_caches
199
+ )
200
+ return _monitor