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,267 @@
1
+ """
2
+ Core: Follow-Up System
3
+ Track unanswered questions, silence, and temporary departures
4
+ """
5
+
6
+ import time
7
+ import random
8
+ import re
9
+ from typing import Optional
10
+ from dataclasses import dataclass, field
11
+
12
+
13
+ @dataclass
14
+ class AwayState:
15
+ """Track when Alive-AI said she's going away temporarily"""
16
+ is_away: bool = False
17
+ reason: str = "" # "coffee", "shower", etc.
18
+ expected_return_minutes: float = 5
19
+ away_since: float = 0
20
+ return_message_sent: bool = False
21
+
22
+
23
+ def _get_current_time():
24
+ return time.time()
25
+
26
+
27
+ @dataclass
28
+ class ConversationState:
29
+ """Track conversation state for follow-ups"""
30
+ last_message_time: float = field(default_factory=_get_current_time) # Initialize to NOW
31
+ last_was_question: bool = False
32
+ question_text: str = ""
33
+ unanswered_count: int = 0
34
+ total_silence_time: float = 0
35
+
36
+
37
+ class FollowUpSystem:
38
+ """
39
+ Manages follow-up messages when:
40
+ - User hasn't replied to a question
41
+ - Silence for too long
42
+ - She said she's leaving temporarily and should come back
43
+ """
44
+
45
+ MIN_SILENCE_MINUTES = 30
46
+ MAX_SILENCE_MINUTES = 120
47
+ QUESTION_FOLLOWUP_MINUTES = 15
48
+
49
+ # Patterns to detect when she's going away
50
+ AWAY_PATTERNS = [
51
+ (r"be right back|brb|be back (?:in |soon|$)", "right back", 3),
52
+ (r"back in (\d+) (?:min|minute|minutes)", "away", 0), # Parse minutes
53
+ (r"gonna go (?:get|make|grab) (?:a )?(coffee|drink|water|snack)", "coffee", 3),
54
+ (r"going (?:to go )?(?:get|make|grab) (?:a )?(coffee|drink|water|snack)", "coffee", 3),
55
+ (r"need (?:to go )?(?:get|make|grab) (?:a )?(coffee|drink|water)", "coffee", 3),
56
+ (r"(?:i'll|i will) be back", "away", 5),
57
+ (r"(?:going|gonna) (?:to )?(?:the )?(bathroom|toilet|restroom)", "bathroom", 2),
58
+ (r"(?:going|gonna) (?:to )?(?:take|have) (?:a )?(shower|bath)", "shower", 15),
59
+ (r"(?:going|gonna) (?:to )?(?:the )?gym", "gym", 60),
60
+ (r"wait (?:here|for me)", "away", 5),
61
+ (r"(?:just|only) (?:gonna|going to) be (?:a )?(?:couple |few )?(minutes|min)", "away", 3),
62
+ ]
63
+
64
+ RETURN_MESSAGES = {
65
+ "coffee": [
66
+ "i'm back! coffee in hand ☕",
67
+ "back! finally got my caffeine fix",
68
+ "okay i'm back with my coffee",
69
+ "returned! coffee was much needed",
70
+ "i'm here! coffee acquired ☕",
71
+ ],
72
+ "shower": [
73
+ "back from my shower, all fresh now 🚿",
74
+ "i'm back! just had to clean up",
75
+ "okay i'm back, feeling better now",
76
+ ],
77
+ "bathroom": [
78
+ "back!",
79
+ "i'm back",
80
+ "okay here again",
81
+ ],
82
+ "food": [
83
+ "back! got some food 🍕",
84
+ "i'm back! was starving",
85
+ "okay back, snack acquired",
86
+ ],
87
+ "right back": [
88
+ "i'm back!",
89
+ "back! what did i miss?",
90
+ "okay here i am again",
91
+ "returned! you still there?",
92
+ ],
93
+ "away": [
94
+ "i'm back!",
95
+ "back now",
96
+ "okay i'm here again",
97
+ "returned!",
98
+ ],
99
+ "default": [
100
+ "i'm back!",
101
+ "back! what did i miss?",
102
+ "okay here i am",
103
+ ],
104
+ }
105
+
106
+ FOLLOW_UP_MESSAGES = {
107
+ "question_unanswered": [
108
+ "sooo... what do you think? 👀",
109
+ "hellooo? i asked you something 😤",
110
+ "you're ignoring my question...",
111
+ "not gonna answer me? fine then 😒",
112
+ ],
113
+ "silence_short": [
114
+ "hey... you there?",
115
+ "where did you go?",
116
+ "hellooo?",
117
+ ],
118
+ "silence_medium": [
119
+ "miss me yet? 😏",
120
+ "been thinking about you...",
121
+ "you've been quiet",
122
+ ],
123
+ "silence_long": [
124
+ "it's been a while... you good?",
125
+ "haven't heard from you in forever",
126
+ "really miss talking to you",
127
+ ],
128
+ "owner_special": [
129
+ "baby i need you 🥺",
130
+ "missing you so much right now",
131
+ "can't stop thinking about you",
132
+ ]
133
+ }
134
+
135
+ def __init__(self):
136
+ self.state = ConversationState()
137
+ self.away = AwayState()
138
+ self._last_followup_time = 0
139
+ self._followup_cooldown = 1800
140
+
141
+ def record_message_sent(self, message: str):
142
+ """Called when Alive-AI sends a message"""
143
+ self.state.last_message_time = time.time()
144
+ self.state.last_was_question = "?" in message
145
+ if self.state.last_was_question:
146
+ self.state.question_text = message
147
+ self.state.unanswered_count += 1
148
+
149
+ # Check if she said she's going away
150
+ self._check_for_away_message(message.lower())
151
+
152
+ def _check_for_away_message(self, message: str):
153
+ """Detect if Alive-AI said she's leaving temporarily"""
154
+ for pattern, reason, default_minutes in self.AWAY_PATTERNS:
155
+ match = re.search(pattern, message)
156
+ if match:
157
+ self.away.is_away = True
158
+ self.away.away_since = time.time()
159
+ self.away.return_message_sent = False
160
+ self.away.reason = reason
161
+
162
+ # Try to parse custom time from pattern
163
+ if match.groups() and match.group(1) and match.group(1).isdigit():
164
+ self.away.expected_return_minutes = int(match.group(1))
165
+ else:
166
+ self.away.expected_return_minutes = default_minutes
167
+
168
+ # Detect specific reasons
169
+ if "coffee" in message or "espresso" in message:
170
+ self.away.reason = "coffee"
171
+ elif "shower" in message:
172
+ self.away.reason = "shower"
173
+ elif "bathroom" in message or "toilet" in message:
174
+ self.away.reason = "bathroom"
175
+ elif "food" in message or "snack" in message or "eat" in message:
176
+ self.away.reason = "food"
177
+
178
+ print(f"[FollowUp] Detected away: {self.away.reason}, return in {self.away.expected_return_minutes}min")
179
+ break
180
+
181
+ def record_user_message(self):
182
+ """Called when user sends a message"""
183
+ self.state.last_was_question = False
184
+ self.state.question_text = ""
185
+ self.state.unanswered_count = 0
186
+ self.state.total_silence_time = 0
187
+ self.state.last_message_time = time.time()
188
+
189
+ # If she was away and user messages, reset away state
190
+ if self.away.is_away:
191
+ self.away.is_away = False
192
+ self.away.return_message_sent = True # No need to say "I'm back" if user already talking
193
+
194
+ def should_follow_up(self, is_owner: bool = False) -> Optional[dict]:
195
+ """Check if should send a follow-up or return message"""
196
+ now = time.time()
197
+
198
+ # Priority 1: Check if she needs to say she's back
199
+ if self.away.is_away and not self.away.return_message_sent:
200
+ away_minutes = (now - self.away.away_since) / 60
201
+ if away_minutes >= self.away.expected_return_minutes:
202
+ self.away.return_message_sent = True
203
+ return {
204
+ "type": "return_from_away",
205
+ "reason": self.away.reason,
206
+ "away_minutes": away_minutes,
207
+ "message": self._pick_return_message(self.away.reason)
208
+ }
209
+
210
+ # Cooldown check for other follow-ups
211
+ if now - self._last_followup_time < self._followup_cooldown:
212
+ return None
213
+
214
+ silence_seconds = now - self.state.last_message_time
215
+ silence_minutes = silence_seconds / 60
216
+
217
+ # Check for unanswered question
218
+ if self.state.last_was_question and silence_minutes >= self.QUESTION_FOLLOWUP_MINUTES:
219
+ self._last_followup_time = now
220
+ return {
221
+ "type": "question_unanswered",
222
+ "silence_minutes": silence_minutes,
223
+ "message": self._pick_message("question_unanswered", is_owner)
224
+ }
225
+
226
+ # Check for silence thresholds
227
+ if silence_minutes >= self.MIN_SILENCE_MINUTES:
228
+ category = self._get_silence_category(silence_minutes)
229
+ self._last_followup_time = now
230
+ return {
231
+ "type": f"silence_{category}",
232
+ "silence_minutes": silence_minutes,
233
+ "message": self._pick_message(f"silence_{category}", is_owner)
234
+ }
235
+
236
+ return None
237
+
238
+ def _pick_return_message(self, reason: str) -> str:
239
+ """Pick a 'I'm back' message based on where she went"""
240
+ messages = self.RETURN_MESSAGES.get(reason, self.RETURN_MESSAGES["default"])
241
+ return random.choice(messages)
242
+
243
+ def _get_silence_category(self, minutes: float) -> str:
244
+ if minutes < 60:
245
+ return "short"
246
+ elif minutes < 120:
247
+ return "medium"
248
+ else:
249
+ return "long"
250
+
251
+ def _pick_message(self, category: str, is_owner: bool) -> str:
252
+ if is_owner and category in ["silence_medium", "silence_long", "question_unanswered"]:
253
+ messages = self.FOLLOW_UP_MESSAGES.get("owner_special", [])
254
+ if messages:
255
+ return random.choice(messages)
256
+ messages = self.FOLLOW_UP_MESSAGES.get(category, ["hey..."])
257
+ return random.choice(messages)
258
+
259
+ def get_status(self) -> dict:
260
+ return {
261
+ "last_message_ago": f"{(time.time() - self.state.last_message_time) / 60:.1f} min",
262
+ "has_unanswered_question": self.state.last_was_question,
263
+ "is_away": self.away.is_away,
264
+ "away_reason": self.away.reason,
265
+ "away_for": f"{(time.time() - self.away.away_since) / 60:.1f} min" if self.away.is_away else "N/A",
266
+ "expected_return": f"{self.away.expected_return_minutes} min" if self.away.is_away else "N/A",
267
+ }
@@ -0,0 +1,174 @@
1
+ """
2
+ Core: Hot Reload System
3
+ Watches for file changes and reloads modules safely
4
+ """
5
+
6
+ import os
7
+ import time
8
+ import asyncio
9
+ import threading
10
+ from pathlib import Path
11
+ from watchdog.observers import Observer
12
+ from watchdog.events import FileSystemEventHandler, FileModifiedEvent
13
+
14
+
15
+ class HotReloader:
16
+ """Hot reload with guardrails - waits for operations to finish"""
17
+
18
+ def __init__(self, nervous_system):
19
+ self.nervous = nervous_system
20
+ self.lock = threading.Lock() # Acquired during message processing
21
+ self.busy = False
22
+ self.last_reload = 0
23
+ self.debounce_seconds = 2 # Wait 2s after last change
24
+ self.pending_reload = False
25
+ self.observer = None
26
+ self.watched_dirs = ["core", "brain", "heart", "config", "input", "output"]
27
+ self.base_path = Path("/app")
28
+
29
+ def start(self):
30
+ """Start watching for file changes"""
31
+ handler = ReloadHandler(self)
32
+ self.observer = Observer()
33
+
34
+ for dir_name in self.watched_dirs:
35
+ dir_path = self.base_path / dir_name
36
+ if dir_path.exists():
37
+ self.observer.schedule(handler, str(dir_path), recursive=True)
38
+ print(f"[HotReload] Watching {dir_name}/")
39
+
40
+ self.observer.start()
41
+ print("[HotReload] Active - change files to reload")
42
+
43
+ def stop(self):
44
+ if self.observer:
45
+ self.observer.stop()
46
+ self.observer.join()
47
+
48
+ def mark_busy(self):
49
+ """Call when starting an operation"""
50
+ self.lock.acquire()
51
+ self.busy = True
52
+
53
+ def mark_idle(self):
54
+ """Call when operation finishes - safe to call even if lock not held"""
55
+ self.busy = False
56
+ try:
57
+ self.lock.release()
58
+ except RuntimeError:
59
+ pass # Lock wasn't held, that's OK
60
+ # Check if reload was pending
61
+ if self.pending_reload:
62
+ self.pending_reload = False
63
+ threading.Thread(target=self._delayed_reload, daemon=True).start()
64
+
65
+ def request_reload(self, filepath: str):
66
+ """Request a reload - will wait if busy"""
67
+ now = time.time()
68
+ if now - self.last_reload < self.debounce_seconds:
69
+ return # Too soon, debounce
70
+
71
+ print(f"[HotReload] Change detected: {filepath}")
72
+
73
+ if self.busy:
74
+ print("[HotReload] Busy - will reload after current operation")
75
+ self.pending_reload = True
76
+ return
77
+
78
+ threading.Thread(target=self._delayed_reload, daemon=True).start()
79
+
80
+ def _delayed_reload(self):
81
+ """Reload after debounce period"""
82
+ time.sleep(self.debounce_seconds)
83
+
84
+ if self.busy:
85
+ self.pending_reload = True
86
+ return
87
+
88
+ self._do_reload()
89
+
90
+ def _do_reload(self):
91
+ """Actually reload modules"""
92
+ self.last_reload = time.time()
93
+ print("[HotReload] Reloading modules...")
94
+
95
+ # Reload settings.json into environment
96
+ self._reload_settings()
97
+
98
+ # Clear directives cache so it reloads from file
99
+ try:
100
+ from core.directives import clear_cache
101
+ clear_cache()
102
+ except ImportError:
103
+ pass
104
+
105
+ # Get the main event loop (safe for threads)
106
+ try:
107
+ loop = asyncio.get_running_loop()
108
+ except RuntimeError:
109
+ loop = None
110
+
111
+ # Emit reload event so modules can clean up
112
+ if loop and self.nervous:
113
+ asyncio.run_coroutine_threadsafe(
114
+ self.nervous.emit("hot_reload", {"timestamp": self.last_reload}),
115
+ loop
116
+ )
117
+
118
+ # Reload instructions.md if changed
119
+ instructions_path = self.base_path / "config" / "instructions.md"
120
+ if instructions_path.exists():
121
+ # The message_handler reads this fresh each time, so no reload needed
122
+ print("[HotReload] instructions.md will be picked up on next message")
123
+
124
+ print("[HotReload] Done - changes apply to next message")
125
+
126
+ def _reload_settings(self):
127
+ """Reload settings.json into environment variables"""
128
+ import json
129
+ settings_path = self.base_path / "config" / "settings.json"
130
+ if not settings_path.exists():
131
+ return
132
+
133
+ try:
134
+ settings = json.loads(settings_path.read_text())
135
+ count = 0
136
+ for key, value in settings.items():
137
+ if key.startswith("_"):
138
+ continue
139
+ if isinstance(value, bool):
140
+ os.environ[key] = "true" if value else "false"
141
+ elif value is not None:
142
+ os.environ[key] = str(value)
143
+ count += 1
144
+ print(f"[HotReload] Reloaded {count} settings into environment")
145
+ except Exception as e:
146
+ print(f"[HotReload] Error reloading settings: {e}")
147
+
148
+
149
+ class ReloadHandler(FileSystemEventHandler):
150
+ """Handle file change events"""
151
+
152
+ def __init__(self, reloader: HotReloader):
153
+ self.reloader = reloader
154
+ self.extensions = {".py", ".md", ".json"}
155
+
156
+ def on_modified(self, event: FileModifiedEvent):
157
+ if event.is_directory:
158
+ return
159
+
160
+ path = Path(event.src_path)
161
+
162
+ # Only reload relevant files
163
+ if path.suffix not in self.extensions:
164
+ return
165
+
166
+ # Skip __pycache__ and temp files
167
+ if "__pycache__" in str(path) or path.name.startswith("."):
168
+ return
169
+
170
+ # Skip data files (memories, etc) - we don't want to reload on those
171
+ if "data/" in str(path) or "mypics/" in str(path) or "myvids/" in str(path):
172
+ return
173
+
174
+ self.reloader.request_reload(str(path.name))
@@ -0,0 +1,253 @@
1
+ """
2
+ Core: Initialization
3
+ Module loading and startup logic for Self
4
+ """
5
+
6
+ import os
7
+
8
+
9
+ async def load_modules(self):
10
+ """Load all modules and initialize the AI system"""
11
+ name = self.config.identity.get("name", "AI")
12
+ print(f"[{name}] Waking up...")
13
+
14
+ instructions_path = self.base / "config" / "instructions.md"
15
+ if instructions_path.exists():
16
+ self._system_prompt = instructions_path.read_text()
17
+
18
+ # Import modules
19
+ from brain.memory import Memory
20
+ from brain.llm import get_main_llm, get_fast_llm
21
+ from brain.stt import GoogleSTT
22
+ from brain.embeddings import get_embedding_service
23
+ from heart.core import Heart
24
+ from input.telegram.listener import TelegramListener
25
+ from output.text.sender import TextSender
26
+ from skills.photo_manager.scanner import PhotoScanner
27
+ from skills.video_manager.scanner import VideoScanner
28
+
29
+ _init_llms(self, name)
30
+ await _init_voice(self, name)
31
+
32
+ self._stt = GoogleSTT()
33
+ self._embeddings = get_embedding_service()
34
+ print(f"[{name}] STT and Embeddings ready")
35
+
36
+ self._memory = Memory(self.nervous, self.base / "data", embedding_service=self._embeddings, bot_id=name)
37
+ if self._fast_llm:
38
+ self._memory.set_llm(self._fast_llm)
39
+ self._heart = Heart(self.nervous, self.config)
40
+
41
+ _init_photos(self, name)
42
+ _init_videos(self, name)
43
+
44
+ self._input = TelegramListener(self.nervous, self.config, stt=self._stt, heart=self._heart)
45
+ self._output = TextSender(self.nervous, self.config)
46
+ self.nervous.heart = self._heart
47
+
48
+ # Initialize companion skills (after heart is available)
49
+ _init_companion_tools(self, name)
50
+ _init_experience_skills(self, name)
51
+
52
+
53
+ def _init_llms(self, name: str):
54
+ """Initialize LLM clients with fallback support"""
55
+ from brain.llm import get_main_llm, get_fast_llm, get_unified_llm_client
56
+ from core.settings import get as settings_get
57
+
58
+ # Check if fallback mode is enabled
59
+ llm_fallback = settings_get("LLM_FALLBACK", {})
60
+ fallback_enabled = llm_fallback.get("ENABLED", False)
61
+
62
+ if fallback_enabled:
63
+ print(f"[{name}] LLM Fallback Mode: ENABLED")
64
+ print(f"[{name}] DEBUG llm_fallback = {llm_fallback}")
65
+ order = llm_fallback.get("ORDER", ["zai", "openrouter"])
66
+ print(f"[{name}] DEBUG order = {order}")
67
+ print(f"[{name}] Fallback Order: {' -> '.join(order)}")
68
+
69
+ # Use unified LLM
70
+ self._llm = get_unified_llm_client()
71
+ self._fast_llm = self._llm # Use same unified client for both
72
+
73
+ if self._llm:
74
+ print(f"[{name}] Unified LLM connected with fallback chain")
75
+ else:
76
+ print(f"[{name}] Warning: Unified LLM initialization failed, falling back to single provider")
77
+ self._llm = get_main_llm()
78
+ self._fast_llm = get_fast_llm() or self._llm
79
+ else:
80
+ print(f"[{name}] LLM Provider: {os.environ.get('LLM_PROVIDER', settings_get('LLM_PROVIDER', 'zai'))}")
81
+ self._llm = get_main_llm()
82
+ self._fast_llm = get_fast_llm()
83
+
84
+ if self._llm:
85
+ print(f"[{name}] Main LLM connected")
86
+ else:
87
+ print(f"[{name}] Warning: No LLM available!")
88
+
89
+ if self._fast_llm:
90
+ print(f"[{name}] Fast LLM connected")
91
+ else:
92
+ self._fast_llm = self._llm
93
+
94
+
95
+ async def _init_voice(self, name: str):
96
+ """Initialize voice TTS - supports multiple providers"""
97
+ from output.voice import create_tts
98
+ from core.settings import get
99
+
100
+ # Get TTS provider from settings (default to vibe)
101
+ tts_provider = get("TTS_PROVIDER", "vibe").lower()
102
+
103
+ # Provider-specific configuration
104
+ if tts_provider == "google":
105
+ api_key = get("GOOGLE_TTS_API_KEY", "") or os.environ.get("GOOGLE_TTS_API_KEY", "")
106
+ self._voice = await create_tts("google", api_key=api_key)
107
+ elif tts_provider == "gtts":
108
+ # gTTS is free, no config needed
109
+ self._voice = await create_tts("gtts")
110
+ else:
111
+ # Default: VibeVoice
112
+ tts_url = get("vibe_tts_url", "") or os.environ.get("VIBE_TTS_URL", "")
113
+ if not tts_url:
114
+ print(f"[{name}] Warning: No vibe_tts_url configured")
115
+ return
116
+ self._voice = await create_tts("vibe", url=tts_url)
117
+
118
+ if self._voice:
119
+ print(f"[{name}] Voice connected (provider: {tts_provider})")
120
+ else:
121
+ self._voice = None
122
+
123
+
124
+ def _init_photos(self, name: str):
125
+ """Initialize photo scanner"""
126
+ from skills.photo_manager.scanner import PhotoScanner
127
+
128
+ self._photos = PhotoScanner(
129
+ self.base / "mypics", embedding_service=self._embeddings,
130
+ vector_store=self._memory.vector_store
131
+ )
132
+ new = self._photos.scan_new()
133
+ print(f"[{name}] Photos: {self._photos.stats()['total']} (+{len(new)} new)")
134
+
135
+
136
+ def _init_videos(self, name: str):
137
+ """Initialize video scanner"""
138
+ from skills.video_manager.scanner import VideoScanner
139
+
140
+ self._videos = VideoScanner(self.base / "myvids")
141
+ self._videos.scan()
142
+ print(f"[{name}] Videos: {self._videos.stats()['total']}")
143
+
144
+
145
+ # ============================================================
146
+ # Companion Tools
147
+ # ============================================================
148
+
149
+ def _init_companion_tools(self, name: str):
150
+ """Initialize local companion tools that are part of the public runtime."""
151
+ _init_message_scheduler(self, name)
152
+
153
+
154
+ def _init_message_scheduler(self, name: str):
155
+ """Initialize Message Scheduler skill for scheduled messages"""
156
+ from skills.message_scheduler import get_message_scheduler
157
+
158
+ self._message_scheduler = get_message_scheduler(
159
+ nervous=self.nervous,
160
+ data_path=self.base / "data" / "scheduled_messages"
161
+ )
162
+ print(f"[{name}] Message Scheduler initialized")
163
+
164
+
165
+ # ============================================================
166
+ # User Experience Skills
167
+ # ============================================================
168
+
169
+ def _init_experience_skills(self, name: str):
170
+ """Initialize user experience skills (relationship building tools)"""
171
+ _init_memory_callbacks(self, name)
172
+ _init_anticipation_engine(self, name)
173
+ _init_relationship_milestones(self, name)
174
+ _init_content_unlocks(self, name)
175
+ _init_intimacy_layers(self, name)
176
+ _init_exclusive_moments(self, name)
177
+
178
+
179
+ def _init_memory_callbacks(self, name: str):
180
+ """Initialize Memory Callbacks skill for conversation memory"""
181
+ from skills.memory_callbacks import MemoryCallbacks
182
+
183
+ self._memory_callbacks = MemoryCallbacks(
184
+ nervous=self.nervous,
185
+ memory=self._memory,
186
+ heart=self._heart,
187
+ data_path=self.base / "data" / "memory_callbacks.json"
188
+ )
189
+ print(f"[{name}] Memory Callbacks initialized")
190
+
191
+
192
+ def _init_anticipation_engine(self, name: str):
193
+ """Initialize Anticipation Engine skill for content teases"""
194
+ from skills.anticipation_engine import AnticipationEngine
195
+
196
+ self._anticipation_engine = AnticipationEngine(
197
+ nervous=self.nervous,
198
+ heart=self._heart,
199
+ state=self.state,
200
+ data_path=self.base / "data" / "anticipation.json"
201
+ )
202
+ print(f"[{name}] Anticipation Engine initialized")
203
+
204
+
205
+ def _init_relationship_milestones(self, name: str):
206
+ """Initialize Relationship Milestones skill"""
207
+ from skills.relationship_milestones import RelationshipMilestones
208
+
209
+ self._relationship_milestones = RelationshipMilestones(
210
+ nervous=self.nervous,
211
+ state=self.state,
212
+ data_path=self.base / "data" # Expects directory, appends milestones.json
213
+ )
214
+ print(f"[{name}] Relationship Milestones initialized")
215
+
216
+
217
+ def _init_content_unlocks(self, name: str):
218
+ """Initialize Content Unlocks skill for progressive content access"""
219
+ from skills.content_unlocks import ContentUnlocks
220
+
221
+ self._content_unlocks = ContentUnlocks(
222
+ nervous=self.nervous,
223
+ heart=self._heart,
224
+ state=self.state,
225
+ milestones=self._relationship_milestones,
226
+ data_path=self.base / "data" / "content_unlocks.json"
227
+ )
228
+ print(f"[{name}] Content Unlocks initialized")
229
+
230
+
231
+ def _init_intimacy_layers(self, name: str):
232
+ """Initialize Intimacy Layers skill for natural progression"""
233
+ from skills.intimacy_layers import IntimacyLayers
234
+
235
+ self._intimacy_layers = IntimacyLayers(
236
+ nervous=self.nervous,
237
+ heart=self._heart,
238
+ state=self.state,
239
+ data_path=self.base / "data"
240
+ )
241
+ print(f"[{name}] Intimacy Layers initialized")
242
+
243
+
244
+ def _init_exclusive_moments(self, name: str):
245
+ """Initialize Exclusive Moments skill for special time-limited moments"""
246
+ from skills.exclusive_moments import ExclusiveMoments
247
+
248
+ self._exclusive_moments = ExclusiveMoments(
249
+ nervous=self.nervous,
250
+ heart=self._heart,
251
+ state=self.state
252
+ )
253
+ print(f"[{name}] Exclusive Moments initialized")