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,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"}
@@ -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
+