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,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")
|