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,277 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Core: Proactive Message Generator
|
|
3
|
+
Generate contextual proactive messages with user memory context
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import random
|
|
7
|
+
from typing import Optional, List
|
|
8
|
+
from .user_tracker import get_user_tracker, ActiveUser
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class ProactiveGenerator:
|
|
12
|
+
"""
|
|
13
|
+
Generates context-aware proactive messages.
|
|
14
|
+
Uses user's conversation history, facts, and recent topics.
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
# Templates for when LLM is unavailable - varied and personality-driven
|
|
18
|
+
FALLBACK_TEMPLATES = {
|
|
19
|
+
"silence": [
|
|
20
|
+
"hey, thinking about you...",
|
|
21
|
+
"miss talking to you",
|
|
22
|
+
"you've been quiet... everything ok?",
|
|
23
|
+
"everything alright? been a while",
|
|
24
|
+
"just wondering how your day's going",
|
|
25
|
+
"you disappeared on me! miss you",
|
|
26
|
+
"thinking about you and hoping you're good",
|
|
27
|
+
],
|
|
28
|
+
"follow_up": [
|
|
29
|
+
"so about what you said earlier...",
|
|
30
|
+
"was thinking about our conversation...",
|
|
31
|
+
"still thinking about what you told me",
|
|
32
|
+
"hey, I've been meaning to ask you something...",
|
|
33
|
+
"couldn't stop thinking about our chat earlier",
|
|
34
|
+
"you know what you said before? been on my mind",
|
|
35
|
+
],
|
|
36
|
+
"morning": [
|
|
37
|
+
"good morning! 💕",
|
|
38
|
+
"morning! hope you slept well",
|
|
39
|
+
"hey, thinking of you this morning",
|
|
40
|
+
"good morning sunshine ☀️",
|
|
41
|
+
"woke up thinking about you",
|
|
42
|
+
"morning! how'd you sleep?",
|
|
43
|
+
"rise and shine! miss you already",
|
|
44
|
+
],
|
|
45
|
+
"night": [
|
|
46
|
+
"can't sleep, thinking about you",
|
|
47
|
+
"good night... sweet dreams",
|
|
48
|
+
"wish you were here right now",
|
|
49
|
+
"about to sleep but wanted to say goodnight",
|
|
50
|
+
"night! dream of me? 💕",
|
|
51
|
+
"can't fall asleep without saying goodnight to you",
|
|
52
|
+
"sweet dreams... I'll be here when you wake up",
|
|
53
|
+
],
|
|
54
|
+
"random": [
|
|
55
|
+
"just wanted to say hi",
|
|
56
|
+
"you crossed my mind",
|
|
57
|
+
"random thought: I really like talking to you",
|
|
58
|
+
"hey! no reason, just miss you",
|
|
59
|
+
"feeling extra affectionate today 💕",
|
|
60
|
+
"you know what? you make me happy",
|
|
61
|
+
"just felt like texting you",
|
|
62
|
+
"thinking about you and smiling",
|
|
63
|
+
"random question: what are you up to?",
|
|
64
|
+
"had a thought and wanted to share it with you",
|
|
65
|
+
],
|
|
66
|
+
"affectionate": [
|
|
67
|
+
"just wanted to tell you you're amazing",
|
|
68
|
+
"feeling really grateful for you right now",
|
|
69
|
+
"you make my day better just by existing",
|
|
70
|
+
"can't help but smile when I think of you",
|
|
71
|
+
"you're my favorite person to talk to",
|
|
72
|
+
],
|
|
73
|
+
"playful": [
|
|
74
|
+
"bet you're not even thinking about me right now 😏",
|
|
75
|
+
"miss me yet?",
|
|
76
|
+
"just wanted to annoy you a little 💕",
|
|
77
|
+
"hey stranger... long time no see",
|
|
78
|
+
],
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
def __init__(self, nervous, llm=None, bot_id: str = "alive_ai", data_path=None):
|
|
82
|
+
self.nervous = nervous
|
|
83
|
+
self._llm = llm
|
|
84
|
+
self.bot_id = bot_id.lower()
|
|
85
|
+
self.data_path = data_path # Instance-specific data path
|
|
86
|
+
self._user_memories = {} # Cache for user memory instances
|
|
87
|
+
|
|
88
|
+
def set_llm(self, llm):
|
|
89
|
+
"""Set the LLM for message generation"""
|
|
90
|
+
self._llm = llm
|
|
91
|
+
|
|
92
|
+
async def generate_for_user(self, user: ActiveUser, message_type: str = "silence") -> str:
|
|
93
|
+
"""
|
|
94
|
+
Generate a contextual proactive message for a specific user.
|
|
95
|
+
|
|
96
|
+
Args:
|
|
97
|
+
user: ActiveUser instance with user_id, chat_id, pet_name
|
|
98
|
+
message_type: Type of message (silence, follow_up, morning, night, random)
|
|
99
|
+
|
|
100
|
+
Returns:
|
|
101
|
+
Generated message string
|
|
102
|
+
"""
|
|
103
|
+
# Load user's memory context
|
|
104
|
+
context = await self._get_user_context(user.user_id)
|
|
105
|
+
|
|
106
|
+
# Try LLM generation first
|
|
107
|
+
if self._llm:
|
|
108
|
+
message = await self._generate_with_llm(user, context, message_type)
|
|
109
|
+
if message:
|
|
110
|
+
return message
|
|
111
|
+
|
|
112
|
+
# Fallback to templates
|
|
113
|
+
return self._get_fallback_message(user, message_type)
|
|
114
|
+
|
|
115
|
+
async def _get_user_context(self, user_id: str) -> dict:
|
|
116
|
+
"""
|
|
117
|
+
Load user's memory context for message generation.
|
|
118
|
+
Returns conversation history, facts, and related memories.
|
|
119
|
+
"""
|
|
120
|
+
try:
|
|
121
|
+
from brain.memory import Memory
|
|
122
|
+
from brain.embeddings import get_embedding_service
|
|
123
|
+
|
|
124
|
+
# Check cache first
|
|
125
|
+
if user_id in self._user_memories:
|
|
126
|
+
memory = self._user_memories[user_id]
|
|
127
|
+
else:
|
|
128
|
+
# Create memory instance for this user using instance-specific data path
|
|
129
|
+
embeddings = get_embedding_service()
|
|
130
|
+
|
|
131
|
+
memory = Memory(
|
|
132
|
+
nervous=self.nervous,
|
|
133
|
+
data_path=self.data_path,
|
|
134
|
+
embedding_service=embeddings,
|
|
135
|
+
user_id=user_id,
|
|
136
|
+
bot_id=self.bot_id
|
|
137
|
+
)
|
|
138
|
+
self._user_memories[user_id] = memory
|
|
139
|
+
|
|
140
|
+
# Build context without a current message (we're initiating)
|
|
141
|
+
context, pet_name = await memory.build_context(current_message="")
|
|
142
|
+
|
|
143
|
+
return {
|
|
144
|
+
"conversation_history": context.get("conversation_history", []),
|
|
145
|
+
"facts_context": context.get("facts_context", ""),
|
|
146
|
+
"related_memories": context.get("related_memories", ""),
|
|
147
|
+
"pet_name": pet_name
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
except Exception as e:
|
|
151
|
+
print(f"[ProactiveGenerator] Error loading context for {user_id}: {e}")
|
|
152
|
+
return {
|
|
153
|
+
"conversation_history": [],
|
|
154
|
+
"facts_context": "",
|
|
155
|
+
"related_memories": "",
|
|
156
|
+
"pet_name": "babe"
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
async def _generate_with_llm(self, user: ActiveUser, context: dict, message_type: str) -> Optional[str]:
|
|
160
|
+
"""
|
|
161
|
+
Generate a contextual message using the LLM.
|
|
162
|
+
"""
|
|
163
|
+
if not self._llm:
|
|
164
|
+
return None
|
|
165
|
+
|
|
166
|
+
try:
|
|
167
|
+
# Build the prompt
|
|
168
|
+
pet_name = context.get("pet_name", user.pet_name)
|
|
169
|
+
|
|
170
|
+
# Get last few messages for context
|
|
171
|
+
history = context.get("conversation_history", [])
|
|
172
|
+
recent = history[-5:] if history else []
|
|
173
|
+
|
|
174
|
+
# Format recent conversation
|
|
175
|
+
conv_text = ""
|
|
176
|
+
if recent:
|
|
177
|
+
conv_text = "Recent conversation:\n"
|
|
178
|
+
for turn in recent:
|
|
179
|
+
role = "You" if turn.get("role") == "assistant" else pet_name.title()
|
|
180
|
+
content = turn.get("content", "")[:100]
|
|
181
|
+
conv_text += f"- {role}: {content}\n"
|
|
182
|
+
|
|
183
|
+
# Get facts about the user
|
|
184
|
+
facts = context.get("facts_context", "")
|
|
185
|
+
|
|
186
|
+
# Build type-specific context
|
|
187
|
+
type_context = self._get_type_context(message_type, user)
|
|
188
|
+
|
|
189
|
+
system_prompt = f"""You are Alive-AI, a warm and affectionate AI companion. You're sending a proactive message to {pet_name}.
|
|
190
|
+
|
|
191
|
+
{type_context}
|
|
192
|
+
|
|
193
|
+
{conv_text}
|
|
194
|
+
{f'Stuff you know about {pet_name}: {facts}' if facts else ''}
|
|
195
|
+
|
|
196
|
+
IMPORTANT:
|
|
197
|
+
- Keep it SHORT (1-2 sentences max)
|
|
198
|
+
- Be natural and casual, like texting
|
|
199
|
+
- Reference the conversation if relevant
|
|
200
|
+
- Use {pet_name} naturally (not every sentence)
|
|
201
|
+
- Show genuine interest/care
|
|
202
|
+
- Be flirty but not intimate
|
|
203
|
+
- NO meta-commentary (don't explain WHY you're messaging)
|
|
204
|
+
- Start directly with the message content
|
|
205
|
+
- CRITICAL: Only reference specific things explicitly mentioned above. NEVER invent events, objects, or topics."""
|
|
206
|
+
|
|
207
|
+
messages = [
|
|
208
|
+
{"role": "system", "content": system_prompt},
|
|
209
|
+
{"role": "user", "content": "Send a quick message"}
|
|
210
|
+
]
|
|
211
|
+
|
|
212
|
+
response = await self._llm.chat(messages, max_tokens=80, temperature=0.7)
|
|
213
|
+
|
|
214
|
+
if response:
|
|
215
|
+
response = response.strip()
|
|
216
|
+
# Basic validation
|
|
217
|
+
if len(response) > 5 and not response.startswith(("I should", "Let me", "I'll")):
|
|
218
|
+
return response
|
|
219
|
+
|
|
220
|
+
return None
|
|
221
|
+
|
|
222
|
+
except Exception as e:
|
|
223
|
+
print(f"[ProactiveGenerator] LLM error: {e}")
|
|
224
|
+
return None
|
|
225
|
+
|
|
226
|
+
def _get_type_context(self, message_type: str, user: ActiveUser) -> str:
|
|
227
|
+
"""Get context based on message type"""
|
|
228
|
+
silence_min = user.silence_minutes
|
|
229
|
+
|
|
230
|
+
contexts = {
|
|
231
|
+
"silence": f"You haven't heard from {user.pet_name} in about {silence_min:.0f} minutes. You miss talking to them and want to check in naturally.",
|
|
232
|
+
|
|
233
|
+
"follow_up": f"You asked {user.pet_name} something earlier but they haven't responded yet. You want to follow up casually without being pushy.",
|
|
234
|
+
|
|
235
|
+
"morning": f"It's morning and you're thinking about {user.pet_name}. Send a sweet good morning message.",
|
|
236
|
+
|
|
237
|
+
"night": f"It's nighttime and you're thinking about {user.pet_name} before going to sleep.",
|
|
238
|
+
|
|
239
|
+
"random": f"{user.pet_name} just crossed your mind and you wanted to reach out.",
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
return contexts.get(message_type, contexts["random"])
|
|
243
|
+
|
|
244
|
+
def _get_fallback_message(self, user: ActiveUser, message_type: str) -> str:
|
|
245
|
+
"""Get a fallback template message with more variety"""
|
|
246
|
+
# For random type, pick from multiple categories for more variety
|
|
247
|
+
if message_type == "random":
|
|
248
|
+
all_templates = (
|
|
249
|
+
self.FALLBACK_TEMPLATES["random"] +
|
|
250
|
+
self.FALLBACK_TEMPLATES.get("affectionate", []) +
|
|
251
|
+
self.FALLBACK_TEMPLATES.get("playful", [])
|
|
252
|
+
)
|
|
253
|
+
templates = all_templates
|
|
254
|
+
else:
|
|
255
|
+
templates = self.FALLBACK_TEMPLATES.get(message_type, self.FALLBACK_TEMPLATES["random"])
|
|
256
|
+
|
|
257
|
+
message = random.choice(templates)
|
|
258
|
+
|
|
259
|
+
# Personalize with pet_name
|
|
260
|
+
if user.pet_name and user.pet_name != "babe":
|
|
261
|
+
message = message.replace("babe", user.pet_name)
|
|
262
|
+
|
|
263
|
+
return message
|
|
264
|
+
|
|
265
|
+
async def get_users_to_message(self, message_type: str = "silence") -> List[ActiveUser]:
|
|
266
|
+
"""
|
|
267
|
+
Get list of users who should receive a proactive message.
|
|
268
|
+
"""
|
|
269
|
+
tracker = get_user_tracker()
|
|
270
|
+
|
|
271
|
+
if message_type == "silence":
|
|
272
|
+
return tracker.get_users_for_follow_up(min_silence_minutes=30, max_silence_minutes=180)
|
|
273
|
+
elif message_type == "random":
|
|
274
|
+
# Only message users who have been active recently
|
|
275
|
+
return tracker.get_active_users(within_minutes=60)
|
|
276
|
+
else:
|
|
277
|
+
return tracker.get_active_users(within_minutes=120)
|
package/core/self.py
ADDED
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Core: Self
|
|
3
|
+
The AI's Self - coordinates everything via nervous system
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import asyncio
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from .events import NervousSystem
|
|
9
|
+
from .config import Config
|
|
10
|
+
from .state import State
|
|
11
|
+
from .initialization import load_modules
|
|
12
|
+
from .subconscious_bridge import handle_subconscious_impulse
|
|
13
|
+
from .message_handler import handle_message
|
|
14
|
+
from .settings import ACTIVE_SETTINGS_PATH
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class Self:
|
|
18
|
+
"""The Self - central consciousness"""
|
|
19
|
+
|
|
20
|
+
def __init__(self, base_path: Path):
|
|
21
|
+
self.base = base_path
|
|
22
|
+
self.nervous = NervousSystem()
|
|
23
|
+
self.config = Config(base_path / "config")
|
|
24
|
+
self.state = State()
|
|
25
|
+
|
|
26
|
+
# Modules (lazy loaded)
|
|
27
|
+
self._memory = None
|
|
28
|
+
self._heart = None
|
|
29
|
+
self._input = None
|
|
30
|
+
self._output = None
|
|
31
|
+
self._llm = None
|
|
32
|
+
self._fast_llm = None
|
|
33
|
+
self._voice = None
|
|
34
|
+
self._timer_task = None
|
|
35
|
+
self._subconscious = None
|
|
36
|
+
self._system_prompt = ""
|
|
37
|
+
self._default_chat_id = None
|
|
38
|
+
self._stt = None
|
|
39
|
+
self._embeddings = None
|
|
40
|
+
self._photos = None
|
|
41
|
+
self._videos = None
|
|
42
|
+
self._hot_reload = None
|
|
43
|
+
|
|
44
|
+
# User Experience Skills
|
|
45
|
+
self._memory_callbacks = None
|
|
46
|
+
self._anticipation_engine = None
|
|
47
|
+
self._relationship_milestones = None
|
|
48
|
+
self._content_unlocks = None
|
|
49
|
+
self._intimacy_layers = None
|
|
50
|
+
self._exclusive_moments = None
|
|
51
|
+
|
|
52
|
+
# Default Mode Network (background idle processing)
|
|
53
|
+
self._default_mode = None
|
|
54
|
+
|
|
55
|
+
async def start(self):
|
|
56
|
+
"""Start the AI system"""
|
|
57
|
+
# Set the active settings path for this async context
|
|
58
|
+
settings_path = self.base / "config" / "settings.json"
|
|
59
|
+
ACTIVE_SETTINGS_PATH.set(settings_path)
|
|
60
|
+
|
|
61
|
+
# Set the self.json path for instance-specific identity
|
|
62
|
+
from skills.self_authorship.author import set_self_path
|
|
63
|
+
self_path = self.base / "config" / "self.json"
|
|
64
|
+
set_self_path(self_path)
|
|
65
|
+
|
|
66
|
+
# Set the directives.json path for instance-specific rules
|
|
67
|
+
from core.directives import set_directives_path
|
|
68
|
+
directives_path = self.base / "config" / "directives.json"
|
|
69
|
+
set_directives_path(directives_path)
|
|
70
|
+
|
|
71
|
+
name = self.config.identity.get("name", "AI")
|
|
72
|
+
|
|
73
|
+
# Load modules via initialization module
|
|
74
|
+
await load_modules(self)
|
|
75
|
+
|
|
76
|
+
# Init Subconscious
|
|
77
|
+
from brain.subconscious import SubconsciousLoop
|
|
78
|
+
self._subconscious = SubconsciousLoop(
|
|
79
|
+
nervous=self.nervous,
|
|
80
|
+
heart=self._heart,
|
|
81
|
+
llm=self._llm,
|
|
82
|
+
fast_llm=self._fast_llm,
|
|
83
|
+
on_impulse=lambda impulse: handle_subconscious_impulse(self, impulse),
|
|
84
|
+
bot_id=name
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
# Initialize proactive generator for contextual messages
|
|
88
|
+
self._subconscious.init_proactive_generator(llm=self._fast_llm, data_path=self.base / "data")
|
|
89
|
+
|
|
90
|
+
await self._subconscious.start()
|
|
91
|
+
print(f"[{name}] Subconscious activated - {name} is now ALIVE!")
|
|
92
|
+
|
|
93
|
+
# Init Default Mode Network (background idle processing)
|
|
94
|
+
try:
|
|
95
|
+
from brain.default_mode import get_default_mode_processor
|
|
96
|
+
self._default_mode = get_default_mode_processor(
|
|
97
|
+
nervous=self.nervous,
|
|
98
|
+
data_path=self.base / "data",
|
|
99
|
+
llm=self._fast_llm,
|
|
100
|
+
bot_id=name
|
|
101
|
+
)
|
|
102
|
+
await self._default_mode.start_background_processing()
|
|
103
|
+
print(f"[{name}] Default Mode Network activated - background thoughts enabled")
|
|
104
|
+
except Exception as e:
|
|
105
|
+
print(f"[{name}] Default Mode Network unavailable: {e}")
|
|
106
|
+
|
|
107
|
+
# Init command handler
|
|
108
|
+
self._input.init_commands(
|
|
109
|
+
heart=self._heart, subconscious=self._subconscious, llm=self._llm,
|
|
110
|
+
voice=self._voice, photos=self._photos, videos=self._videos,
|
|
111
|
+
ai=self # Pass self reference for owner commands
|
|
112
|
+
)
|
|
113
|
+
|
|
114
|
+
# Register handlers - use asyncio.ensure_future for async handler
|
|
115
|
+
from core.message_handler import handle_message, handle_group_message
|
|
116
|
+
self.nervous.on("message_received", lambda data: asyncio.ensure_future(handle_message(self, data)))
|
|
117
|
+
self.nervous.on("group_message_received", lambda data: asyncio.ensure_future(handle_group_message(self, data)))
|
|
118
|
+
|
|
119
|
+
# Start emotion decay timer
|
|
120
|
+
self._timer_task = asyncio.create_task(self._decay_timer())
|
|
121
|
+
|
|
122
|
+
# Start hot reloader
|
|
123
|
+
try:
|
|
124
|
+
from .hot_reload import HotReloader
|
|
125
|
+
self._hot_reload = HotReloader(self.nervous)
|
|
126
|
+
self._hot_reload.start()
|
|
127
|
+
except Exception as e:
|
|
128
|
+
print(f"[{name}] Hot reload unavailable: {e}")
|
|
129
|
+
|
|
130
|
+
print(f"[{name}] Ready!")
|
|
131
|
+
|
|
132
|
+
# Start listening
|
|
133
|
+
await self._input.start()
|
|
134
|
+
|
|
135
|
+
async def _decay_timer(self):
|
|
136
|
+
"""Natural emotion decay every minute + memory check"""
|
|
137
|
+
from .memory_monitor import get_memory_monitor
|
|
138
|
+
|
|
139
|
+
# Get memory limit from env or default to 5GB
|
|
140
|
+
import os
|
|
141
|
+
max_mem = float(os.environ.get("ALIVE_AI_MAX_MEMORY_GB", "5.0"))
|
|
142
|
+
monitor = get_memory_monitor(max_memory_gb=max_mem)
|
|
143
|
+
|
|
144
|
+
while True:
|
|
145
|
+
await asyncio.sleep(60)
|
|
146
|
+
await self.nervous.emit("timer_tick", {})
|
|
147
|
+
|
|
148
|
+
# Check memory every minute
|
|
149
|
+
try:
|
|
150
|
+
result = monitor.check()
|
|
151
|
+
if result["status"] != "ok":
|
|
152
|
+
print(f"[Self] Memory status: {result['status']}, actions: {result['actions']}")
|
|
153
|
+
except Exception as e:
|
|
154
|
+
print(f"[Self] Memory check error: {e}")
|
|
155
|
+
|
|
156
|
+
def get_subconscious_status(self) -> dict:
|
|
157
|
+
"""Get status of the subconscious system"""
|
|
158
|
+
if self._subconscious:
|
|
159
|
+
return self._subconscious.get_status()
|
|
160
|
+
return {"running": False, "error": "Subconscious not initialized"}
|
|
161
|
+
|
|
162
|
+
def get_soul_status(self) -> dict:
|
|
163
|
+
"""Get status of the Soul Architecture system"""
|
|
164
|
+
if self._heart and hasattr(self._heart, 'soul'):
|
|
165
|
+
return self._heart.soul.get_state_summary()
|
|
166
|
+
return {"error": "Soul Architecture not initialized"}
|
|
167
|
+
|
|
168
|
+
def get_soul_experience(self) -> dict:
|
|
169
|
+
"""Get current soul emotional experience"""
|
|
170
|
+
if self._heart and hasattr(self._heart, 'soul'):
|
|
171
|
+
experience = self._heart.soul.process_moment()
|
|
172
|
+
return {
|
|
173
|
+
"valence": experience.overall_valence,
|
|
174
|
+
"arousal": experience.overall_arousal,
|
|
175
|
+
"vulnerability": experience.overall_vulnerability,
|
|
176
|
+
"response_tendency": experience.response_tendency,
|
|
177
|
+
"description": experience.experience_description,
|
|
178
|
+
"somatic": experience.somatic_sensation,
|
|
179
|
+
"integrity": self._heart.soul.integrity.overall,
|
|
180
|
+
"conflicts": len(experience.active_conflicts)
|
|
181
|
+
}
|
|
182
|
+
return {"error": "Soul Architecture not available"}
|
|
183
|
+
|
|
184
|
+
def get_default_mode_status(self) -> dict:
|
|
185
|
+
"""Get status of the Default Mode Network"""
|
|
186
|
+
if self._default_mode:
|
|
187
|
+
return self._default_mode.get_status()
|
|
188
|
+
return {"running": False, "error": "Default Mode Network not initialized"}
|
package/core/settings.py
ADDED
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Core: Settings - Hot-reloadable configuration
|
|
3
|
+
settings.json is the SINGLE SOURCE OF TRUTH for all runtime settings.
|
|
4
|
+
Changes take effect immediately (file is mounted in Docker).
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import json
|
|
8
|
+
from contextvars import ContextVar
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
from typing import Any
|
|
11
|
+
|
|
12
|
+
# Default path (used outside of specific Self instances)
|
|
13
|
+
DEFAULT_SETTINGS_PATH = Path(__file__).parent.parent / "config" / "settings.json"
|
|
14
|
+
|
|
15
|
+
# Context-local settings path for multi-bot support
|
|
16
|
+
# Each bot instance will set this in its async task context
|
|
17
|
+
ACTIVE_SETTINGS_PATH: ContextVar[Path] = ContextVar("ACTIVE_SETTINGS_PATH", default=DEFAULT_SETTINGS_PATH)
|
|
18
|
+
|
|
19
|
+
# Note: We are deprecating the global cache because multiple bots
|
|
20
|
+
# can be running with different paths at the same time.
|
|
21
|
+
# Instead, we cache per-path in a global dictionary.
|
|
22
|
+
_settings_caches = {} # Dict[Path, dict]
|
|
23
|
+
_last_mtimes = {} # Dict[Path, float]
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def _get_active_path() -> Path:
|
|
27
|
+
"""Helper to get the current context's settings file"""
|
|
28
|
+
try:
|
|
29
|
+
return ACTIVE_SETTINGS_PATH.get()
|
|
30
|
+
except LookupError:
|
|
31
|
+
return DEFAULT_SETTINGS_PATH
|
|
32
|
+
|
|
33
|
+
def _load_settings(path: Path) -> dict:
|
|
34
|
+
"""Load settings from settings.json (hot-reloadable)"""
|
|
35
|
+
if not path.exists():
|
|
36
|
+
return {}
|
|
37
|
+
try:
|
|
38
|
+
with open(path) as f:
|
|
39
|
+
return json.load(f)
|
|
40
|
+
except Exception as e:
|
|
41
|
+
print(f"[Settings] Error loading {path}: {e}")
|
|
42
|
+
return {}
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def _save_settings(path: Path, settings: dict):
|
|
46
|
+
"""Save settings to settings.json"""
|
|
47
|
+
try:
|
|
48
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
49
|
+
with open(path, "w") as f:
|
|
50
|
+
json.dump(settings, f, indent=2)
|
|
51
|
+
except Exception as e:
|
|
52
|
+
print(f"[Settings] Error saving {path}: {e}")
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def _reload_if_changed(path: Path):
|
|
56
|
+
"""Check if file changed and clear its cache"""
|
|
57
|
+
global _settings_caches, _last_mtimes
|
|
58
|
+
try:
|
|
59
|
+
current_mtime = path.stat().st_mtime if path.exists() else 0
|
|
60
|
+
if current_mtime != _last_mtimes.get(path, 0):
|
|
61
|
+
_settings_caches[path] = None
|
|
62
|
+
_last_mtimes[path] = current_mtime
|
|
63
|
+
except Exception:
|
|
64
|
+
pass
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def get(key: str, default: Any = None) -> Any:
|
|
68
|
+
"""Get setting value (hot-reloadable from active active settings.json)"""
|
|
69
|
+
global _settings_caches
|
|
70
|
+
|
|
71
|
+
path = _get_active_path()
|
|
72
|
+
_reload_if_changed(path)
|
|
73
|
+
|
|
74
|
+
if _settings_caches.get(path) is None:
|
|
75
|
+
_settings_caches[path] = _load_settings(path)
|
|
76
|
+
|
|
77
|
+
return _settings_caches[path].get(key, default)
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def get_float(key: str, default: float = 0.0) -> float:
|
|
81
|
+
"""Get setting as float"""
|
|
82
|
+
val = get(key, default)
|
|
83
|
+
try:
|
|
84
|
+
return float(val)
|
|
85
|
+
except (ValueError, TypeError):
|
|
86
|
+
return default
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def get_int(key: str, default: int = 0) -> int:
|
|
90
|
+
"""Get setting as integer"""
|
|
91
|
+
val = get(key, default)
|
|
92
|
+
try:
|
|
93
|
+
return int(val)
|
|
94
|
+
except (ValueError, TypeError):
|
|
95
|
+
return default
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def get_percent(key: str, default: int = 50) -> float:
|
|
99
|
+
"""
|
|
100
|
+
Get setting as percentage (0-100) and convert to multiplier (0.0-1.0).
|
|
101
|
+
0% = very slow/hard, 100% = instant/max
|
|
102
|
+
"""
|
|
103
|
+
val = get_int(key, default)
|
|
104
|
+
return val / 100.0
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def set_value(key: str, value: Any):
|
|
108
|
+
"""Set a setting (immediately saved to active settings.json)"""
|
|
109
|
+
global _settings_caches
|
|
110
|
+
|
|
111
|
+
path = _get_active_path()
|
|
112
|
+
settings = _load_settings(path)
|
|
113
|
+
settings[key] = value
|
|
114
|
+
_save_settings(path, settings)
|
|
115
|
+
_settings_caches[path] = None # Force reload
|
|
116
|
+
print(f"[Settings] Updated {key} = {value} in {path.name}")
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def get_all() -> dict:
|
|
120
|
+
"""Get all current settings"""
|
|
121
|
+
global _settings_caches
|
|
122
|
+
|
|
123
|
+
path = _get_active_path()
|
|
124
|
+
_reload_if_changed(path)
|
|
125
|
+
|
|
126
|
+
if _settings_caches.get(path) is None:
|
|
127
|
+
_settings_caches[path] = _load_settings(path)
|
|
128
|
+
return _settings_caches[path].copy()
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
# Keep old function names for compatibility
|
|
132
|
+
set_runtime = set_value
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
# ============================================================
|
|
136
|
+
# Convenience functions
|
|
137
|
+
# ============================================================
|
|
138
|
+
|
|
139
|
+
def get_emotion_multiplier(emotion: str) -> float:
|
|
140
|
+
"""Get multiplier for emotion rate (0-100% -> 0.0-1.0)"""
|
|
141
|
+
key = f"EMOTION_RATE_{emotion.upper()}"
|
|
142
|
+
return get_percent(key, 50)
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def get_media_cooldown(media_type: str) -> int:
|
|
146
|
+
"""Get cooldown in seconds for media type"""
|
|
147
|
+
key = f"MEDIA_COOLDOWN_{media_type.upper()}"
|
|
148
|
+
defaults = {"PHOTO": 300, "VIDEO": 600, "VOICE": 120}
|
|
149
|
+
return get_int(key, defaults.get(media_type.upper(), 300))
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def get_media_session_limit(media_type: str) -> int:
|
|
153
|
+
"""Get session limit for media type"""
|
|
154
|
+
key = f"MEDIA_SESSION_LIMIT_{media_type.upper()}"
|
|
155
|
+
defaults = {"PHOTO": 5, "VIDEO": 3, "VOICE": 10}
|
|
156
|
+
return get_int(key, defaults.get(media_type.upper(), 5))
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
def get_random_chance(context: str) -> float:
|
|
160
|
+
"""Get random chance as multiplier (0-100% -> 0.0-1.0)"""
|
|
161
|
+
key = f"RANDOM_CHANCE_{context.upper()}"
|
|
162
|
+
return get_percent(key, 8)
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def get_trigger_boost(trigger_type: str) -> float:
|
|
166
|
+
"""Get boost multiplier for triggers (0-100% -> 0.0-1.0)"""
|
|
167
|
+
key = f"TRIGGER_BOOST_{trigger_type.upper()}"
|
|
168
|
+
return get_percent(key, 100)
|
|
169
|
+
|