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
package/core/manifest.md
ADDED
|
@@ -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
|