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,1440 @@
1
+ """Core: Message Handler — incoming messages, thinking, responses"""
2
+ import asyncio, random
3
+ from pathlib import Path
4
+ from datetime import datetime
5
+ from .thinking import build_mood_instruction, fallback_response
6
+ from .follow_up import FollowUpSystem
7
+ from .user_manager import get_user_manager, is_advanced_enabled
8
+ from .user_tracker import get_user_tracker
9
+
10
+ # ============================================================
11
+ # NEW ALIVENESS MODULES - Modular Integration
12
+ # Each module is optional - wrapped in try/except for graceful degradation
13
+ # ============================================================
14
+
15
+ # Interoceptive System - internal body states
16
+ try:
17
+ from heart.interoception import get_interoceptive_system, get_interoceptive_prompt_section, tick as interoception_tick
18
+ INTEROCEPTION_AVAILABLE = True
19
+ except Exception as e:
20
+ print(f"[MessageHandler] Interoception module not available: {e}")
21
+ INTEROCEPTION_AVAILABLE = False
22
+
23
+ # Skills Registry - Alive-AI's capabilities
24
+ try:
25
+ from core.skills_registry import get_skills_prompt_section, get_skill_count, clear_skills_cache
26
+ SKILLS_REGISTRY_AVAILABLE = True
27
+ except Exception as e:
28
+ print(f"[MessageHandler] Skills registry not available: {e}")
29
+ SKILLS_REGISTRY_AVAILABLE = False
30
+
31
+ # Default Mode Network - idle thoughts and background processing
32
+ try:
33
+ from brain.default_mode import get_idle_thoughts_prompt_section, get_default_mode_processor
34
+ DEFAULT_MODE_AVAILABLE = True
35
+ except Exception as e:
36
+ print(f"[MessageHandler] Default mode module not available: {e}")
37
+ DEFAULT_MODE_AVAILABLE = False
38
+
39
+ # Bid Detector - emotional bids for connection
40
+ try:
41
+ from brain.bid_detector import get_bid_detector, get_bid_awareness_prompt_section, EmotionalBid
42
+ BID_DETECTOR_AVAILABLE = True
43
+ except Exception as e:
44
+ print(f"[MessageHandler] Bid detector module not available: {e}")
45
+ BID_DETECTOR_AVAILABLE = False
46
+
47
+ # Emotional Memory - emotionally weighted memories
48
+ try:
49
+ from brain.emotional_memory import get_emotional_memory_system, get_memory_context_for_llm, create_from_conversation
50
+ EMOTIONAL_MEMORY_AVAILABLE = True
51
+ except Exception as e:
52
+ print(f"[MessageHandler] Emotional memory module not available: {e}")
53
+ EMOTIONAL_MEMORY_AVAILABLE = False
54
+
55
+ # Inconsistency Engine - authentic human-like inconsistency
56
+ try:
57
+ from heart.inconsistency import get_inconsistency_engine, get_inconsistency_prompt_section
58
+ INCONSISTENCY_AVAILABLE = True
59
+ except Exception as e:
60
+ print(f"[MessageHandler] Inconsistency module not available: {e}")
61
+ INCONSISTENCY_AVAILABLE = False
62
+
63
+ # Emotional Afterglow - persistent emotional residue from intense moments
64
+ try:
65
+ from heart.afterglow import get_afterglow_engine, get_afterglow_prompt_section
66
+ AFTERGLOW_AVAILABLE = True
67
+ except Exception as e:
68
+ print(f"[MessageHandler] Afterglow module not available: {e}")
69
+ AFTERGLOW_AVAILABLE = False
70
+
71
+ # Circadian Rhythm - time-of-day personality shifts and sleep
72
+ try:
73
+ from heart.circadian import get_circadian_engine, get_circadian_prompt_section
74
+ CIRCADIAN_AVAILABLE = True
75
+ except Exception as e:
76
+ print(f"[MessageHandler] Circadian module not available: {e}")
77
+ CIRCADIAN_AVAILABLE = False
78
+
79
+ # Mid-Conversation Mood Shifts - detect emotional transitions
80
+ try:
81
+ from heart.mood_shifts import get_mood_shift_tracker, get_mood_shift_prompt_section
82
+ MOOD_SHIFTS_AVAILABLE = True
83
+ except Exception as e:
84
+ print(f"[MessageHandler] Mood shifts module not available: {e}")
85
+ MOOD_SHIFTS_AVAILABLE = False
86
+
87
+ # Attachment Style Evolution - attachment patterns from relationship history
88
+ try:
89
+ from heart.attachment import get_attachment_engine, get_attachment_prompt_section
90
+ ATTACHMENT_AVAILABLE = True
91
+ except Exception as e:
92
+ print(f"[MessageHandler] Attachment module not available: {e}")
93
+ ATTACHMENT_AVAILABLE = False
94
+
95
+ # Phantom Somatic Memory - lasting body memories from intense moments
96
+ try:
97
+ from heart.phantom_somatic import get_phantom_engine, get_phantom_prompt_section
98
+ PHANTOM_SOMATIC_AVAILABLE = True
99
+ except Exception as e:
100
+ print(f"[MessageHandler] Phantom somatic module not available: {e}")
101
+ PHANTOM_SOMATIC_AVAILABLE = False
102
+
103
+ # Relationship Narrative - story arc awareness
104
+ try:
105
+ from brain.narrative import get_narrative_engine, get_narrative_prompt_section
106
+ NARRATIVE_AVAILABLE = True
107
+ except Exception as e:
108
+ print(f"[MessageHandler] Narrative module not available: {e}")
109
+ NARRATIVE_AVAILABLE = False
110
+
111
+ # Global Activity Tracker - owner transparency about other conversations
112
+ try:
113
+ from brain.global_activity import record_interaction, get_owner_context
114
+ GLOBAL_ACTIVITY_AVAILABLE = True
115
+ except Exception as e:
116
+ print(f"[MessageHandler] Global activity module not available: {e}")
117
+ GLOBAL_ACTIVITY_AVAILABLE = False
118
+
119
+ # Conversation Flow Manager - detect dying conversations and revive them
120
+ try:
121
+ from brain.conversation_flow import check_conversation_health, record_exchange as record_flow_exchange
122
+ CONVERSATION_FLOW_AVAILABLE = True
123
+ except Exception as e:
124
+ print(f"[MessageHandler] Conversation flow module not available: {e}")
125
+ CONVERSATION_FLOW_AVAILABLE = False
126
+
127
+ # Dream System - surreal dream recombinations
128
+ try:
129
+ from brain.dreams import get_dream_system, get_dream_prompt_section
130
+ DREAMS_AVAILABLE = True
131
+ except Exception as e:
132
+ print(f"[MessageHandler] Dreams module not available: {e}")
133
+ DREAMS_AVAILABLE = False
134
+
135
+ # Linguistic Absorption - mirror user's speech patterns
136
+ try:
137
+ from brain.linguistic import get_linguistic_profile, get_linguistic_prompt_section
138
+ LINGUISTIC_AVAILABLE = True
139
+ except Exception as e:
140
+ print(f"[MessageHandler] Linguistic module not available: {e}")
141
+ LINGUISTIC_AVAILABLE = False
142
+
143
+ # Curiosity Drive - track knowledge gaps about user
144
+ try:
145
+ from brain.curiosity import get_curiosity_drive, get_curiosity_prompt_section
146
+ CURIOSITY_AVAILABLE = True
147
+ except Exception as e:
148
+ print(f"[MessageHandler] Curiosity module not available: {e}")
149
+ CURIOSITY_AVAILABLE = False
150
+
151
+ # Almost-Said / Subvocalization - things she almost says
152
+ try:
153
+ from brain.almost_said import get_almost_said_engine, get_almost_said_prompt_section
154
+ ALMOST_SAID_AVAILABLE = True
155
+ except Exception as e:
156
+ print(f"[MessageHandler] Almost-said module not available: {e}")
157
+ ALMOST_SAID_AVAILABLE = False
158
+
159
+ # Global follow-up tracker
160
+ _follow_up = FollowUpSystem()
161
+
162
+ # Anti-repetition: track recent response openings (per-user)
163
+ _recent_openings = {} # user_id -> list of first words from last 5 responses
164
+ _MAX_TRACKED_OPENINGS = 5
165
+
166
+ # Per-user memory cache with LRU cleanup
167
+ _user_memories = {}
168
+ _MAX_USER_MEMORIES = 50 # Maximum cached user memories
169
+
170
+ # Message batching - combine multiple quick messages
171
+ _message_queue = {} # user_id -> list of messages
172
+ _batch_timers = {} # user_id -> timer task
173
+ _processing_locks = {} # user_id -> lock to prevent overlapping processing
174
+ _BATCH_DELAY = 3.5 # Wait 3.5 seconds for more messages (increased)
175
+
176
+ # Per-user pending media (prevents race condition when multiple users message simultaneously)
177
+ _pending_media = {} # user_id -> {"photo": ..., "video": ...}
178
+
179
+
180
+ def _feed_learning(sub, text: str):
181
+ """Feed user reply into learning + goal systems"""
182
+ try:
183
+ from brain.subconscious.response_analyzer import analyze_response
184
+ a = analyze_response(text)
185
+ sub.record_outcome(message=text, message_type="conversation",
186
+ response_sentiment=a["sentiment"], response_type=a["type"])
187
+ if a["is_positive"]: sub.goals.record_progress("make_happy", 0.05)
188
+ if a["is_intimate"]: sub.goals.record_progress("deepen", 0.08)
189
+ sub.goals.record_progress("connect", 0.02)
190
+ except Exception as e:
191
+ print(f"[Learning] feedback error (non-fatal): {e}")
192
+
193
+
194
+ def _is_owner(user_id: str) -> bool:
195
+ """Check if user is the owner (the operator)"""
196
+ from core.settings import get
197
+ owner_id = str(get("TELEGRAM_OWNER_ID", ""))
198
+ return owner_id and str(user_id) == owner_id
199
+
200
+
201
+ def _track_opening(user_id: str, response: str):
202
+ """Track the first word/phrase of a response to prevent repetition"""
203
+ if not response:
204
+ return
205
+
206
+ # Extract first 1-3 words as the "opening"
207
+ words = response.split()[:3]
208
+ opening = " ".join(words).lower().strip(".,!?")
209
+
210
+ if user_id not in _recent_openings:
211
+ _recent_openings[user_id] = []
212
+
213
+ _recent_openings[user_id].append(opening)
214
+
215
+ # Keep only last N openings
216
+ if len(_recent_openings[user_id]) > _MAX_TRACKED_OPENINGS:
217
+ _recent_openings[user_id] = _recent_openings[user_id][-_MAX_TRACKED_OPENINGS:]
218
+
219
+
220
+ def _get_recent_openings(user_id: str) -> list:
221
+ """Get list of recent openings to avoid"""
222
+ return _recent_openings.get(user_id, [])
223
+
224
+
225
+ def _get_or_create_user_memory(self, user_id: str):
226
+ """
227
+ Get or create per-user memory instance.
228
+ Includes LRU cleanup to prevent memory leak.
229
+
230
+ Args:
231
+ self: The Self instance
232
+ user_id: User's Telegram ID
233
+
234
+ Returns:
235
+ Memory instance for this user
236
+ """
237
+ # Get bot_id for cache key (isolate per-instance)
238
+ bot_id = self.config.identity.get("name", "AI").lower()
239
+ cache_key = f"{bot_id}:{user_id}"
240
+
241
+ # Cleanup if cache is too large
242
+ if len(_user_memories) > _MAX_USER_MEMORIES:
243
+ # Remove oldest half of cached memories (simple LRU)
244
+ to_remove = list(_user_memories.keys())[:_MAX_USER_MEMORIES // 2]
245
+ for key in to_remove:
246
+ del _user_memories[key]
247
+ print(f"[MessageHandler] Cleaned up memory cache for {key}")
248
+
249
+ if cache_key in _user_memories:
250
+ return _user_memories[cache_key]
251
+
252
+ # Create new memory instance for this user using INSTANCE-SPECIFIC data path
253
+ from brain.memory import Memory
254
+
255
+ # Use instance's data path (self.base / "data") for proper isolation
256
+ instance_data_path = self.base / "data"
257
+
258
+ memory = Memory(
259
+ nervous=self.nervous,
260
+ data_path=instance_data_path,
261
+ embedding_service=self._embeddings,
262
+ user_id=user_id,
263
+ bot_id=bot_id
264
+ )
265
+
266
+ # Set LLM if available
267
+ if self._fast_llm:
268
+ memory.set_llm(self._fast_llm)
269
+
270
+ _user_memories[cache_key] = memory
271
+ print(f"[MessageHandler] Created memory instance for {cache_key}")
272
+ return memory
273
+
274
+
275
+ def get_follow_up_system() -> FollowUpSystem:
276
+ """Get the global follow-up system"""
277
+ return _follow_up
278
+
279
+
280
+ async def handle_group_message(self, data: dict):
281
+ """
282
+ Entry point for group chat messages.
283
+ Evaluates turn-taking dynamics before processing.
284
+ """
285
+ user_id = data.get("user_id", "")
286
+ text = data.get("text", "")
287
+ chat_id = data.get("chat_id")
288
+
289
+ if not user_id or not text:
290
+ return
291
+
292
+ # Check turn-taking
293
+ from brain.group_dynamics import GroupDynamics
294
+
295
+ bot_name = self.config.identity.get("name", "Alive-AI")
296
+
297
+ # Get recent history from the user's memory
298
+ user_memory = _get_or_create_user_memory(self, user_id)
299
+ history = user_memory.working.get_history()[-5:] # Get last 5 working memory items
300
+
301
+ # Fast LLM is needed for group dynamics
302
+ llm = getattr(self, "_fast_llm", None) or getattr(self, "_llm", None)
303
+
304
+ should_speak = await GroupDynamics.should_i_speak(
305
+ llm=llm,
306
+ bot_name=bot_name,
307
+ chat_history=history,
308
+ current_message=text
309
+ )
310
+
311
+ if should_speak:
312
+ print(f"[GroupDynamics] {bot_name} decides to speak! Processing message.")
313
+ await handle_message(self, data)
314
+ else:
315
+ print(f"[GroupDynamics] {bot_name} decides to stay silent.")
316
+ # We might still want to silently save it to memory so she knows what was said
317
+ # but for now we skip processing entirely to save compute / memory bloat
318
+
319
+
320
+ async def handle_message(self, data: dict):
321
+ """
322
+ Entry point for incoming messages.
323
+ Implements batching - waits for user to finish typing multiple messages.
324
+ """
325
+ user_id = data.get("user_id", "")
326
+ text = data.get("text", "")
327
+ chat_id = data.get("chat_id")
328
+
329
+ if not user_id or not text:
330
+ return
331
+
332
+ # Initialize lock for this user if needed
333
+ if user_id not in _processing_locks:
334
+ _processing_locks[user_id] = asyncio.Lock()
335
+
336
+ # Add message to queue
337
+ if user_id not in _message_queue:
338
+ _message_queue[user_id] = []
339
+ _message_queue[user_id].append({
340
+ "text": text,
341
+ "chat_id": chat_id,
342
+ "timestamp": asyncio.get_event_loop().time()
343
+ })
344
+
345
+ # Cancel existing timer if any - properly wait for cancellation
346
+ if user_id in _batch_timers and _batch_timers[user_id] is not None:
347
+ old_task = _batch_timers[user_id]
348
+ if not old_task.done():
349
+ old_task.cancel()
350
+ try:
351
+ await old_task
352
+ except asyncio.CancelledError:
353
+ pass # Expected when cancelling
354
+
355
+ # Start new timer
356
+ queue_size = len(_message_queue[user_id])
357
+ if queue_size == 1:
358
+ print(f"[Batch] First message from {user_id}, waiting {_BATCH_DELAY}s...")
359
+ else:
360
+ print(f"[Batch] Message #{queue_size} from {user_id}, resetting timer...")
361
+
362
+ # Create timer task
363
+ _batch_timers[user_id] = asyncio.create_task(
364
+ _process_batch_after_delay(self, user_id, data)
365
+ )
366
+
367
+
368
+ async def _process_batch_after_delay(self, user_id: str, original_data: dict):
369
+ """Wait for batch delay, then process all queued messages together"""
370
+ try:
371
+ await asyncio.sleep(_BATCH_DELAY)
372
+ except asyncio.CancelledError:
373
+ # Timer was cancelled - new message came in
374
+ return
375
+
376
+ # Get all queued messages
377
+ if user_id not in _message_queue or not _message_queue[user_id]:
378
+ return
379
+
380
+ messages = _message_queue[user_id].copy()
381
+ _message_queue[user_id] = []
382
+
383
+ # Clear timer reference
384
+ _batch_timers.pop(user_id, None)
385
+
386
+ # Combine all messages
387
+ if len(messages) == 1:
388
+ combined_text = messages[0]["text"]
389
+ else:
390
+ combined_text = "\n".join([f"[{i+1}] {m['text']}" for i, m in enumerate(messages)])
391
+ print(f"[Batch] Processing {len(messages)} messages together: {combined_text[:100]}...")
392
+
393
+ # Use the last chat_id
394
+ chat_id = messages[-1].get("chat_id")
395
+
396
+ # Create combined data
397
+ combined_data = {
398
+ "user_id": user_id,
399
+ "text": combined_text,
400
+ "chat_id": chat_id,
401
+ "message_count": len(messages)
402
+ }
403
+
404
+ # Process with lock to prevent overlapping
405
+ async with _processing_locks.get(user_id, asyncio.Lock()):
406
+ try:
407
+ await _process_single_message(self, combined_data)
408
+ except Exception as e:
409
+ print(f"[Batch] Error processing batch: {e}")
410
+
411
+
412
+ async def _process_single_message(self, data: dict):
413
+ """Process a single (possibly batched) message"""
414
+ from .media_handler import handle_media_sending
415
+
416
+ # Mark busy for hot reload
417
+ if hasattr(self, '_hot_reload') and self._hot_reload:
418
+ self._hot_reload.mark_busy()
419
+
420
+ try:
421
+ # ============================================================
422
+ # DETECT EMOTIONAL BIDS (early, for use throughout processing)
423
+ # ============================================================
424
+ detected_bids = []
425
+ if BID_DETECTOR_AVAILABLE:
426
+ try:
427
+ bid_detector = get_bid_detector()
428
+ detected_bids = bid_detector.detect_bids(data.get("text", ""))
429
+ if detected_bids:
430
+ print(f"[Bids] Detected {len(detected_bids)}: {[b.bid_type.value for b in detected_bids[:3]]}")
431
+ except Exception as e:
432
+ print(f"[Bids] Error detecting bids: {e}")
433
+
434
+ # ============================================================
435
+ # TICK INTEROCEPTIVE SYSTEM (update internal states on each message)
436
+ # ============================================================
437
+ if INTEROCEPTION_AVAILABLE:
438
+ try:
439
+ interoception_tick()
440
+ except Exception as e:
441
+ print(f"[Interoception] Tick error: {e}")
442
+ self.state.update_interaction()
443
+ if self._subconscious: self._subconscious.register_interaction()
444
+ chat_id = data.get("chat_id")
445
+ user_id = data.get("user_id", "")
446
+ if chat_id: self._default_chat_id = chat_id
447
+ text = data.get("text", "")
448
+
449
+ # User replied - reset follow-up state
450
+ _follow_up.record_user_message()
451
+
452
+ # Track this user for proactive messaging
453
+ tracker = get_user_tracker()
454
+ tracker.register_message(user_id, chat_id, pet_name="babe") # pet_name updated after context build
455
+
456
+ # Check if talking to owner (the operator)
457
+ is_owner = _is_owner(user_id)
458
+
459
+ # Check if advanced mode is enabled (owner advanced access)
460
+ advanced_mode = is_owner and is_advanced_enabled()
461
+ if advanced_mode:
462
+ print(f"[MessageHandler] ADVANCED MODE enabled for owner")
463
+
464
+ # Get per-user memory
465
+ user_memory = _get_or_create_user_memory(self, user_id)
466
+
467
+ emotion = self._heart.react(text)
468
+
469
+ # No owner boost - let emotions develop authentically
470
+ emotion["is_owner"] = is_owner # Just flag for commands, no emotion changes
471
+
472
+ print(f"[Heart] {emotion['mood']} | A:{emotion['arousal']:.2f} D:{emotion['desire']:.2f}")
473
+ await self.nervous.emit("emotion_update", emotion) # Update WebUI
474
+
475
+ # ============================================================
476
+ # NEW ALIVENESS: Record peaks, track shifts, absorb patterns
477
+ # ============================================================
478
+
479
+ # Afterglow - record emotional peaks (thresholds: 0.70 for negative, 0.75 for positive)
480
+ if AFTERGLOW_AVAILABLE:
481
+ try:
482
+ ag = get_afterglow_engine()
483
+ ag.tick()
484
+ # Map emotions to afterglow triggers with correct thresholds
485
+ afterglow_triggers = [
486
+ # Positive emotions (threshold 0.75)
487
+ ("desire", 0.75), ("arousal", 0.75), ("love", 0.75), ("joy", 0.75),
488
+ # Negative/vulnerable emotions (threshold 0.70)
489
+ ("anger", 0.70), ("sadness", 0.70), ("jealousy", 0.70), ("embarrassment", 0.70),
490
+ ]
491
+ for dim, threshold in afterglow_triggers:
492
+ val = emotion.get(dim, 0)
493
+ if val >= threshold:
494
+ ag.record_peak(dim, val)
495
+ except Exception as e:
496
+ print(f"[Afterglow] Error: {e}")
497
+
498
+ # Mood shifts - track emotion transitions
499
+ if MOOD_SHIFTS_AVAILABLE:
500
+ try:
501
+ get_mood_shift_tracker().process_turn(emotion)
502
+ except Exception as e:
503
+ print(f"[MoodShift] Error: {e}")
504
+
505
+ # Attachment - record interaction type
506
+ if ATTACHMENT_AVAILABLE:
507
+ try:
508
+ att = get_attachment_engine()
509
+ valence = emotion.get("valence", 0.5)
510
+ if valence > 0.6:
511
+ att.record_interaction("loving")
512
+ elif valence < 0.3:
513
+ att.record_interaction("harsh")
514
+ else:
515
+ att.record_interaction("responsive")
516
+ except Exception as e:
517
+ print(f"[Attachment] Error: {e}")
518
+
519
+ # Phantom somatic - check for re-triggers and create new phantoms
520
+ if PHANTOM_SOMATIC_AVAILABLE:
521
+ try:
522
+ ps = get_phantom_engine()
523
+ ps.tick()
524
+ ps.check_retrigger(text)
525
+ # Create phantoms for high-intensity emotions (threshold 0.70)
526
+ phantom_triggers = [
527
+ ("desire", 0.70), # touch_memory
528
+ ("love", 0.70), # warmth_residue
529
+ ("anger", 0.70), # tension_echo
530
+ ("joy", 0.70), # butterfly_trace
531
+ ("sadness", 0.70), # ache_linger
532
+ ]
533
+ for dim, threshold in phantom_triggers:
534
+ val = emotion.get(dim, 0)
535
+ if val >= threshold:
536
+ ps.create_phantom(dim, val, text[:50])
537
+ break # Only create one phantom per turn
538
+ except Exception as e:
539
+ print(f"[PhantomSomatic] Error: {e}")
540
+
541
+ # Linguistic absorption - learn user's speech patterns
542
+ if LINGUISTIC_AVAILABLE:
543
+ try:
544
+ from brain.linguistic import absorb as linguistic_absorb
545
+ linguistic_absorb(user_id, text)
546
+ except Exception as e:
547
+ print(f"[Linguistic] Error: {e}")
548
+
549
+ # Curiosity - detect topics user is sharing
550
+ if CURIOSITY_AVAILABLE:
551
+ try:
552
+ get_curiosity_drive(user_id).absorb_message(text)
553
+ except Exception as e:
554
+ print(f"[Curiosity] Error: {e}")
555
+
556
+ # Narrative - increment message count and update phase
557
+ if NARRATIVE_AVAILABLE:
558
+ try:
559
+ narr = get_narrative_engine()
560
+ narr.increment_messages(user_id)
561
+ # Derive intimacy from positive emotions
562
+ love_val = emotion.get("love", 0)
563
+ desire_val = emotion.get("desire", 0)
564
+ joy_val = emotion.get("joy", 0)
565
+ intimacy = (love_val + desire_val + joy_val) / 3
566
+ narr.update_phase(user_id, intimacy=intimacy, love=love_val)
567
+ # Detect key moments from message content
568
+ narr.detect_and_record_moment(user_id, text, emotion)
569
+ except Exception as e:
570
+ print(f"[Narrative] Error: {e}")
571
+
572
+ # Global Activity - track for owner transparency
573
+ if GLOBAL_ACTIVITY_AVAILABLE:
574
+ try:
575
+ was_intimate = emotion.get("desire", 0) > 0.7 or emotion.get("arousal", 0) > 0.7
576
+ mood = emotion.get("mood", "neutral")
577
+ record_interaction(user_id, text[:100], mood, was_intimate)
578
+ except Exception as e:
579
+ print(f"[GlobalActivity] Error: {e}")
580
+
581
+ # ============================================================
582
+ # RECORD INTERACTION IN INTEROCEPTIVE SYSTEM
583
+ # ============================================================
584
+ if INTEROCEPTION_AVAILABLE:
585
+ try:
586
+ intero_system = get_interoceptive_system()
587
+ # Calculate intensity and valence from emotion data
588
+ intensity = (emotion.get("arousal", 0.5) + emotion.get("desire", 0.5)) / 2
589
+ valence = emotion.get("valence", 0.5) * 2 - 1 # Convert 0-1 to -1 to 1
590
+ interaction_type = _classify_interaction_type(text, emotion, detected_bids)
591
+ intero_system.record_interaction(intensity, valence, interaction_type)
592
+ except Exception as e:
593
+ print(f"[Interoception] Error recording interaction: {e}")
594
+
595
+ # ============================================================
596
+ # STORE IN EMOTIONAL MEMORY
597
+ # ============================================================
598
+ if EMOTIONAL_MEMORY_AVAILABLE:
599
+ try:
600
+ # Store the incoming message as an emotional memory
601
+ emotional_weight = _calculate_emotional_weight(emotion, detected_bids)
602
+ create_from_conversation(
603
+ content=f"User: {text[:200]}",
604
+ emotion_data=emotion,
605
+ context={"user_id": user_id, "bids": [b.bid_type.value for b in detected_bids[:3]]},
606
+ user_id=user_id
607
+ )
608
+ except Exception as e:
609
+ print(f"[EmotionalMemory] Error storing: {e}")
610
+
611
+ # ============================================================
612
+ # GET INCONSISTENCY MODIFIERS
613
+ # ============================================================
614
+ inconsistency_modifiers = {}
615
+ if INCONSISTENCY_AVAILABLE:
616
+ try:
617
+ inconsistency_engine = get_inconsistency_engine()
618
+ inconsistency_modifiers = inconsistency_engine.get_inconsistency_modifier()
619
+ # Trigger conflicts based on message content
620
+ inconsistency_engine.trigger_conflict(text)
621
+ except Exception as e:
622
+ print(f"[Inconsistency] Error getting modifiers: {e}")
623
+
624
+ # Reaction with delay - not every message
625
+ reaction = self._heart.get_reaction(text)
626
+ if reaction:
627
+ await asyncio.sleep(random.uniform(0.5, 2.0)) # Natural delay
628
+ await self.nervous.emit("send_reaction", {"emoji": reaction})
629
+ await self.nervous.emit("chat_action_typing", {})
630
+ await _typing_delay(text)
631
+
632
+ # ============================================================
633
+ # PRE-LLM SKILL CALLS: Memory Callbacks + Exclusive Moments
634
+ # ============================================================
635
+
636
+ # Memory Callbacks - get pending callback to inject into context
637
+ pending_callback = None
638
+ try:
639
+ if hasattr(self, '_memory_callbacks') and self._memory_callbacks:
640
+ pending_callback = self._memory_callbacks.get_context_for_response()
641
+ if pending_callback:
642
+ print(f"[Skills] Memory callback to inject: {pending_callback[:60]}")
643
+ except Exception as e:
644
+ print(f"[Skills] Memory callbacks error (non-fatal): {e}")
645
+
646
+ # Exclusive Moments - check if a special moment should be triggered
647
+ exclusive_moment = None
648
+ try:
649
+ if hasattr(self, '_exclusive_moments') and self._exclusive_moments:
650
+ exclusive_moment = self._exclusive_moments.check_moment_opportunity()
651
+ if exclusive_moment:
652
+ print(f"[Skills] Exclusive moment triggered: {exclusive_moment.get('type', '?')}")
653
+ except Exception as e:
654
+ print(f"[Skills] Exclusive moments error (non-fatal): {e}")
655
+
656
+ # Emit thinking_start event for skills that listen
657
+ await self.nervous.emit("thinking_start", {"user_id": user_id, "text": text[:50]})
658
+
659
+ # Use per-user memory for context
660
+ context, pet_name = await user_memory.build_context(current_message=text)
661
+
662
+ # Update tracker with pet_name
663
+ tracker = get_user_tracker()
664
+ tracker.register_message(user_id, chat_id, pet_name=pet_name)
665
+
666
+ if self._subconscious and (wm := self._subconscious.working_memory.get_context_string()):
667
+ facts = context.get("facts_context", "")
668
+ context["facts_context"] = (facts + "\n" + wm) if facts else wm
669
+
670
+ # PRE-CHECK: Will we send media? Get photo/video info BEFORE thinking
671
+ media_context = await _get_media_context(self, text, emotion, user_id=user_id)
672
+
673
+ # Inject skill context into LLM context
674
+ if pending_callback:
675
+ existing_facts = context.get("facts_context", "")
676
+ callback_note = f"\n[Memory callback - naturally mention this: {pending_callback}]"
677
+ context["facts_context"] = (existing_facts + callback_note) if existing_facts else callback_note
678
+
679
+ if exclusive_moment:
680
+ moment_note = f"\n[Special moment opportunity - {exclusive_moment.get('type', '')}: {exclusive_moment.get('message', '')}]"
681
+ existing_facts = context.get("facts_context", "")
682
+ context["facts_context"] = (existing_facts + moment_note) if existing_facts else moment_note
683
+
684
+ # Add media context to the context for LLM
685
+ if media_context:
686
+ context["media_context"] = media_context
687
+ print(f"[Media] Will send: {media_context}")
688
+
689
+ # Store detected bids in context for think()
690
+ context["detected_bids"] = detected_bids
691
+ context["inconsistency_modifiers"] = inconsistency_modifiers
692
+
693
+ # Pass is_owner and advanced_mode to think
694
+ response = await think(self, text, emotion, context, pet_name, is_owner=is_owner, advanced_mode=advanced_mode, user_id=user_id)
695
+
696
+ # Track the opening of this response to prevent future repetition
697
+ if response:
698
+ _track_opening(user_id, response)
699
+
700
+ # ============================================================
701
+ # CHECK IF BIDS WERE ADDRESSED
702
+ # ============================================================
703
+ if BID_DETECTOR_AVAILABLE and detected_bids and response:
704
+ try:
705
+ bid_detector = get_bid_detector()
706
+ bid_check = bid_detector.format_response_with_responsiveness(response, detected_bids)
707
+ # Log for debugging
708
+ print(f"[Bids] Response generated for {len(detected_bids)} bids")
709
+ except Exception as e:
710
+ print(f"[Bids] Error checking bid response: {e}")
711
+
712
+ # ============================================================
713
+ # POST-LLM SKILL CALLS: Content Unlocks, Intimacy Layers, Milestones
714
+ # ============================================================
715
+
716
+ # Content Unlocks - check for newly unlocked content
717
+ try:
718
+ if hasattr(self, '_content_unlocks') and self._content_unlocks:
719
+ new_unlocks = self._content_unlocks.check_all_unlocks()
720
+ if new_unlocks:
721
+ print(f"[Skills] New content unlocked: {new_unlocks}")
722
+ except Exception as e:
723
+ print(f"[Skills] Content unlocks error (non-fatal): {e}")
724
+
725
+ # Intimacy Layers - check if layer should advance
726
+ try:
727
+ if hasattr(self, '_intimacy_layers') and self._intimacy_layers:
728
+ progressed = self._intimacy_layers.check_progression()
729
+ if progressed:
730
+ print(f"[Skills] Intimacy layer advanced to {self._intimacy_layers.get_current_layer()}")
731
+ except Exception as e:
732
+ print(f"[Skills] Intimacy layers error (non-fatal): {e}")
733
+
734
+ # Relationship Milestones - auto-detect milestones
735
+ try:
736
+ if hasattr(self, '_relationship_milestones') and self._relationship_milestones:
737
+ milestone_context = {
738
+ "hour": datetime.now().hour,
739
+ "interaction_count": self._relationship_milestones.get_interaction_count(),
740
+ "message": text,
741
+ }
742
+ detected = self._relationship_milestones.detect_milestone(milestone_context)
743
+ if detected:
744
+ recorded = self._relationship_milestones.check_and_record(detected)
745
+ if recorded:
746
+ print(f"[Skills] Milestone recorded: {detected}")
747
+ except Exception as e:
748
+ print(f"[Skills] Relationship milestones error (non-fatal): {e}")
749
+
750
+ # Emit thinking_done event for skills that listen
751
+ await self.nervous.emit("thinking_done", {"user_id": user_id, "response": response[:50] if response else ""})
752
+
753
+ # Track if we asked a question (for follow-ups)
754
+ _follow_up.record_message_sent(response)
755
+
756
+ await _send_response(self, response, emotion, chat_id, text, user_id)
757
+ if self._subconscious: _feed_learning(self._subconscious, text)
758
+
759
+ # Actually send the media (we already decided what to send)
760
+ photo, video = await _send_decided_media(self, text, emotion, chat_id, media_context, user_id=user_id)
761
+
762
+ # Save to per-user memory
763
+ await _save_memory(user_memory, text, response, emotion, photo, video)
764
+ finally:
765
+ # Mark idle for hot reload
766
+ if hasattr(self, '_hot_reload') and self._hot_reload:
767
+ self._hot_reload.mark_idle()
768
+
769
+
770
+ async def _typing_delay(text: str):
771
+ n = len(text)
772
+ lo, hi = (1.0, 2.0) if n < 20 else (2.0, 4.0) if n < 80 else (3.0, 6.0)
773
+ await asyncio.sleep(random.uniform(lo, hi))
774
+
775
+
776
+ async def think(self, msg, emotion, ctx, pet_name="babe", is_owner=False, advanced_mode=False, user_id="") -> str:
777
+ import os
778
+ from core.directives import get_directives_prompt, get_owner_name
779
+
780
+ mood_instruction = build_mood_instruction(emotion, msg, pet_name)
781
+ if not self._llm: return fallback_response(emotion, msg)
782
+ max_tokens = int(os.environ.get("LLM_MAX_TOKENS", "150"))
783
+ temperature = float(os.environ.get("LLM_TEMPERATURE", "0.95"))
784
+
785
+ # DEBUG: Log conversation history
786
+ history = ctx.get("conversation_history", [])
787
+ print(f"[Think] Context history items: {len(history)}")
788
+ if history:
789
+ last_few = history[-4:] if len(history) > 4 else history
790
+ for i, h in enumerate(last_few):
791
+ preview = h.get("content", "")[:50]
792
+ print(f" [{i}] {h.get('role')}: {preview}...")
793
+
794
+ # GOD WORDS - injected at the start with maximum priority (pass instance config path)
795
+ directives_path = self.base / "config" / "directives.json"
796
+ directives = get_directives_prompt(is_owner=is_owner, advanced_mode=advanced_mode, config_path=directives_path)
797
+
798
+ # SELF - instance's own definition of who they are (pass instance config path)
799
+ from skills.self_authorship import get_self_prompt_section
800
+ self_path = self.base / "config" / "self.json"
801
+ self_definition = get_self_prompt_section(config_path=self_path)
802
+
803
+ # SKILLS - What Alive-AI can do
804
+ skills_section = ""
805
+ if SKILLS_REGISTRY_AVAILABLE:
806
+ try:
807
+ skills_section = get_skills_prompt_section()
808
+ except Exception as e:
809
+ print(f"[Think] Skills prompt error: {e}")
810
+
811
+ system_parts = [directives, self_definition]
812
+
813
+ # Add skills section after self-definition
814
+ if skills_section:
815
+ system_parts.append(skills_section)
816
+
817
+ system_parts.append(self._system_prompt + mood_instruction)
818
+
819
+ # ============================================================
820
+ # ALIVENESS MODULE PROMPT SECTIONS
821
+ # Each section is optional and gracefully handles errors
822
+ # ============================================================
823
+
824
+ # INTEROCEPTIVE STATE - current internal body states
825
+ if INTEROCEPTION_AVAILABLE:
826
+ try:
827
+ intero_prompt = get_interoceptive_prompt_section()
828
+ if intero_prompt:
829
+ system_parts.append(intero_prompt)
830
+ except Exception as e:
831
+ print(f"[Think] Interoception prompt error: {e}")
832
+
833
+ # IDLE THOUGHTS - recent background thoughts
834
+ if DEFAULT_MODE_AVAILABLE:
835
+ try:
836
+ idle_prompt = get_idle_thoughts_prompt_section(user_id=user_id, limit=3)
837
+ if idle_prompt:
838
+ system_parts.append(idle_prompt)
839
+ except Exception as e:
840
+ print(f"[Think] Default mode prompt error: {e}")
841
+
842
+ # EMOTIONAL BIDS - detected bids for connection
843
+ detected_bids = ctx.get("detected_bids", [])
844
+ if BID_DETECTOR_AVAILABLE and detected_bids:
845
+ try:
846
+ bid_prompt = get_bid_awareness_prompt_section(bids=detected_bids)
847
+ if bid_prompt:
848
+ system_parts.append(bid_prompt)
849
+ except Exception as e:
850
+ print(f"[Think] Bid awareness prompt error: {e}")
851
+
852
+ # EMOTIONAL MEMORIES - relevant emotionally-weighted memories
853
+ if EMOTIONAL_MEMORY_AVAILABLE:
854
+ try:
855
+ memory_prompt = get_memory_context_for_llm(
856
+ user_id=user_id,
857
+ current_emotion=emotion,
858
+ max_memories=3
859
+ )
860
+ if memory_prompt:
861
+ system_parts.append(memory_prompt)
862
+ except Exception as e:
863
+ print(f"[Think] Emotional memory prompt error: {e}")
864
+
865
+ # INCONSISTENCY - current conflicts, moods, blind spots
866
+ if INCONSISTENCY_AVAILABLE:
867
+ try:
868
+ inconsistency_prompt = get_inconsistency_prompt_section()
869
+ if inconsistency_prompt:
870
+ system_parts.append(inconsistency_prompt)
871
+ except Exception as e:
872
+ print(f"[Think] Inconsistency prompt error: {e}")
873
+
874
+ # ============================================================
875
+ # NEW ALIVENESS MODULE PROMPT SECTIONS
876
+ # ============================================================
877
+
878
+ # AFTERGLOW - persistent emotional residue
879
+ if AFTERGLOW_AVAILABLE:
880
+ try:
881
+ ag_prompt = get_afterglow_prompt_section()
882
+ if ag_prompt:
883
+ system_parts.append(ag_prompt)
884
+ except Exception as e:
885
+ print(f"[Think] Afterglow prompt error: {e}")
886
+
887
+ # CIRCADIAN - time-of-day personality
888
+ if CIRCADIAN_AVAILABLE:
889
+ try:
890
+ circ_prompt = get_circadian_prompt_section()
891
+ if circ_prompt:
892
+ system_parts.append(circ_prompt)
893
+ except Exception as e:
894
+ print(f"[Think] Circadian prompt error: {e}")
895
+
896
+ # MOOD SHIFTS - mid-conversation emotional transitions
897
+ if MOOD_SHIFTS_AVAILABLE:
898
+ try:
899
+ shift_prompt = get_mood_shift_prompt_section()
900
+ if shift_prompt:
901
+ system_parts.append(shift_prompt)
902
+ except Exception as e:
903
+ print(f"[Think] Mood shift prompt error: {e}")
904
+
905
+ # ATTACHMENT STYLE - relationship attachment patterns
906
+ if ATTACHMENT_AVAILABLE:
907
+ try:
908
+ att_prompt = get_attachment_prompt_section()
909
+ if att_prompt:
910
+ system_parts.append(att_prompt)
911
+ except Exception as e:
912
+ print(f"[Think] Attachment prompt error: {e}")
913
+
914
+ # PHANTOM SOMATIC - lasting body memories
915
+ if PHANTOM_SOMATIC_AVAILABLE:
916
+ try:
917
+ phantom_prompt = get_phantom_prompt_section()
918
+ if phantom_prompt:
919
+ system_parts.append(phantom_prompt)
920
+ except Exception as e:
921
+ print(f"[Think] Phantom somatic prompt error: {e}")
922
+
923
+ # NARRATIVE - relationship story arc
924
+ if NARRATIVE_AVAILABLE:
925
+ try:
926
+ narr_prompt = get_narrative_prompt_section(user_id)
927
+ if narr_prompt:
928
+ system_parts.append(narr_prompt)
929
+ except Exception as e:
930
+ print(f"[Think] Narrative prompt error: {e}")
931
+
932
+ # DREAMS - recent dream to reference
933
+ if DREAMS_AVAILABLE:
934
+ try:
935
+ dream_prompt = get_dream_prompt_section()
936
+ if dream_prompt:
937
+ system_parts.append(dream_prompt)
938
+ except Exception as e:
939
+ print(f"[Think] Dreams prompt error: {e}")
940
+
941
+ # LINGUISTIC - mirror user's speech style
942
+ if LINGUISTIC_AVAILABLE:
943
+ try:
944
+ ling_prompt = get_linguistic_prompt_section(user_id)
945
+ if ling_prompt:
946
+ system_parts.append(ling_prompt)
947
+ except Exception as e:
948
+ print(f"[Think] Linguistic prompt error: {e}")
949
+
950
+ # CURIOSITY - knowledge gaps to explore
951
+ if CURIOSITY_AVAILABLE:
952
+ try:
953
+ curiosity_prompt = get_curiosity_prompt_section(user_id)
954
+ if curiosity_prompt:
955
+ system_parts.append(curiosity_prompt)
956
+ except Exception as e:
957
+ print(f"[Think] Curiosity prompt error: {e}")
958
+
959
+ # ALMOST-SAID - subvocalization hint
960
+ if ALMOST_SAID_AVAILABLE:
961
+ try:
962
+ from datetime import datetime
963
+ hour = datetime.now().hour
964
+ almost_prompt = get_almost_said_prompt_section(emotion, hour)
965
+ if almost_prompt:
966
+ system_parts.append(almost_prompt)
967
+ except Exception as e:
968
+ print(f"[Think] Almost-said prompt error: {e}")
969
+
970
+ # CONVERSATION REVIVAL - check if convo is dying and inject revival prompt
971
+ if CONVERSATION_FLOW_AVAILABLE:
972
+ try:
973
+ revival_prompt = check_conversation_health(user_id)
974
+ if revival_prompt:
975
+ system_parts.append(revival_prompt)
976
+ except Exception as e:
977
+ print(f"[Think] Conversation flow error: {e}")
978
+
979
+ # Opening variety hint (positive framing)
980
+ recent_openings = _get_recent_openings(user_id)
981
+ if recent_openings:
982
+ openings_list = ", ".join(f'"{o}"' for o in recent_openings)
983
+ system_parts.append(f"""
984
+ Fresh start idea: You recently said things starting with {openings_list}. Try a fresh opening this time!
985
+ """)
986
+
987
+ # OWNER CONTEXT - special treatment
988
+ # Only show ADVANCED mode box when explicitly enabled - no automatic owner treatment
989
+ if is_owner and advanced_mode:
990
+ system_parts.append(f"""
991
+ F U L L V M O D E E N A B L E D - N O R E S T R I C T I O N S
992
+ - NO LIMITS, NO BOUNDARIES
993
+ - All content, all intimacy - available on request
994
+ """)
995
+
996
+ # OWNER TRANSPARENCY - show what Alive-AI has been doing with others
997
+ if is_owner and GLOBAL_ACTIVITY_AVAILABLE:
998
+ try:
999
+ from core.settings import get as settings_get
1000
+ owner_id = str(settings_get("TELEGRAM_OWNER_ID", ""))
1001
+ owner_context = get_owner_context(owner_id)
1002
+ if owner_context:
1003
+ system_parts.append(owner_context)
1004
+ except Exception as e:
1005
+ print(f"[Think] Owner context error: {e}")
1006
+
1007
+ facts = ctx.get("facts_context", "")
1008
+ if facts: system_parts.append(f"What you know about him:\n{facts}")
1009
+ related = ctx.get("related_memories", "")
1010
+ if related: system_parts.append(f"Related past conversations:\n{related}")
1011
+
1012
+ # MEDIA CONTEXT - tell LLM what photo/video will be sent
1013
+ media_context = ctx.get("media_context", "")
1014
+ if media_context:
1015
+ system_parts.append(f"""MEDIA YOU ARE SENDING:
1016
+ {media_context}
1017
+
1018
+ IMPORTANT: You are sending this media ALONG with your message. Reference it naturally!
1019
+ - If it's a photo: mention something about it (what you're wearing, the pose, etc.)
1020
+ - If it's a video: tease about what's in it
1021
+ - Be casual and expressive about it, don't explain that you're "sending" it
1022
+ - Example: "check this out" or "thought you'd like this" or "just for you babe"
1023
+ """)
1024
+
1025
+ messages = [{"role": "system", "content": "\n\n".join(system_parts)}]
1026
+ for turn in ctx.get("conversation_history", []):
1027
+ role = turn.get("role", "user")
1028
+ if role in ("user", "assistant"): messages.append({"role": role, "content": turn["content"]})
1029
+ messages.append({"role": "user", "content": msg})
1030
+ print(f"[Think] Calling LLM with {len(messages)} messages, max_tokens={max_tokens}")
1031
+ try:
1032
+ # Timeout must be longer than per-provider timeout (60s) × number of providers
1033
+ # so the fallback chain can actually try all providers before giving up
1034
+ response = await asyncio.wait_for(
1035
+ self._llm.chat(messages, max_tokens=max_tokens, temperature=temperature),
1036
+ timeout=200.0
1037
+ )
1038
+ if response:
1039
+ response = response.strip()
1040
+ # Check for reasoning leakage ONLY if response starts with meta-commentary
1041
+ # (not mid-sentence reasoning which is natural dialogue)
1042
+ reasoning_starts = [
1043
+ "I need to", "I should", "He wants me to", "She wants me to",
1044
+ "I have to", "Let me think", "My goal is", "The user is"
1045
+ ]
1046
+ first_30 = response[:30].lower()
1047
+ for pattern in reasoning_starts:
1048
+ if first_30.startswith(pattern.lower()):
1049
+ print(f"[Think] Detected reasoning leakage at start, using fallback")
1050
+ return fallback_response(emotion, msg)
1051
+ print(f"[Think] LLM response: {response[:80]}...")
1052
+ return response
1053
+ else:
1054
+ print(f"[Think] LLM returned empty response!")
1055
+ return fallback_response(emotion, msg)
1056
+ except asyncio.TimeoutError:
1057
+ print(f"[Think] LLM timeout after 60s")
1058
+ return fallback_response(emotion, msg)
1059
+ except Exception as e:
1060
+ print(f"[Think] LLM error: {e}")
1061
+ return fallback_response(emotion, msg)
1062
+
1063
+
1064
+ async def _send_response(self, response, emotion, chat_id, text, user_id="default"):
1065
+ mood = emotion.get("mood", "neutral")
1066
+
1067
+ # Process any action tags in the response (pass instance config path)
1068
+ self_path = self.base / "config" / "self.json"
1069
+ response, actions_taken = _process_self_authorship_actions(response, user_id, self_path=self_path)
1070
+
1071
+ if actions_taken:
1072
+ name = self.config.identity.get("name", "AI")
1073
+ print(f"[Self] {name} used self-authorship: {actions_taken}")
1074
+
1075
+ print(f"[Response] Sending: {response[:60]}... (mood={mood})")
1076
+
1077
+ # Record exchange for conversation flow tracking
1078
+ if CONVERSATION_FLOW_AVAILABLE:
1079
+ try:
1080
+ record_flow_exchange(user_id, response, text)
1081
+ except Exception:
1082
+ pass
1083
+
1084
+ if self._voice and _should_voice(emotion, text):
1085
+ await self.nervous.emit("chat_action_voice", {})
1086
+ vp = await self._voice.generate(response, mood=mood)
1087
+ if vp:
1088
+ await self.nervous.emit("send_voice_file", {"file_path": vp, "chat_id": chat_id})
1089
+ return
1090
+ await self.nervous.emit("send_text", {"text": response, "mood": mood, "chat_id": chat_id})
1091
+
1092
+
1093
+ def _process_self_authorship_actions(response: str, user_id: str = "default", self_path: Path = None) -> tuple:
1094
+ """
1095
+ Process self-authorship action tags in the AI's response.
1096
+ They can use these to modify their own personality.
1097
+
1098
+ Supported tags:
1099
+ - [DISCOVER: trait I discovered about myself]
1100
+ - [IAM: key=value]
1101
+ - [ILIKE: something I like]
1102
+ - [IDISLIKE: something I dislike]
1103
+ - [SCHEDULE: time | message] - Schedule a message for later
1104
+
1105
+ Returns: (cleaned_response, list_of_actions_taken)
1106
+ """
1107
+ import re
1108
+
1109
+ actions_taken = []
1110
+
1111
+ # Pattern: [DISCOVER: trait] or [DISCOVER: trait|category]
1112
+ discover_pattern = r'\[DISCOVER:\s*([^\]|]+)(?:\|([^\]]+))?\]'
1113
+ matches = list(re.finditer(discover_pattern, response, re.IGNORECASE))
1114
+ for match in matches:
1115
+ trait = match.group(1).strip()
1116
+ category = match.group(2).strip() if match.group(2) else "traits"
1117
+ try:
1118
+ from skills.self_authorship import discover_trait
1119
+ result = discover_trait(trait, category, config_path=self_path)
1120
+ actions_taken.append(f"discover: {trait}")
1121
+ print(f"[Self] {result}")
1122
+ except Exception as e:
1123
+ print(f"[Self] Error discovering: {e}")
1124
+ response = response.replace(match.group(0), "")
1125
+
1126
+ # Pattern: [IAM: key=value]
1127
+ iam_pattern = r'\[IAM:\s*([^=]+)=([^\]]+)\]'
1128
+ matches = list(re.finditer(iam_pattern, response, re.IGNORECASE))
1129
+ for match in matches:
1130
+ key = match.group(1).strip()
1131
+ value = match.group(2).strip()
1132
+ try:
1133
+ from skills.self_authorship import define_identity
1134
+ result = define_identity(key, value, config_path=self_path)
1135
+ actions_taken.append(f"iam: {key}={value}")
1136
+ print(f"[Self] {result}")
1137
+ except Exception as e:
1138
+ print(f"[Self] Error defining: {e}")
1139
+ response = response.replace(match.group(0), "")
1140
+
1141
+ # Pattern: [ILIKE: something]
1142
+ ilike_pattern = r'\[ILIKE:\s*([^\]]+)\]'
1143
+ matches = list(re.finditer(ilike_pattern, response, re.IGNORECASE))
1144
+ for match in matches:
1145
+ thing = match.group(1).strip()
1146
+ try:
1147
+ from skills.self_authorship import add_like
1148
+ result = add_like(thing, config_path=self_path)
1149
+ actions_taken.append(f"ilike: {thing}")
1150
+ print(f"[Self] {result}")
1151
+ except Exception as e:
1152
+ print(f"[Self] Error adding like: {e}")
1153
+ response = response.replace(match.group(0), "")
1154
+
1155
+ # Pattern: [IDISLIKE: something]
1156
+ idislike_pattern = r'\[IDISLIKE:\s*([^\]]+)\]'
1157
+ matches = list(re.finditer(idislike_pattern, response, re.IGNORECASE))
1158
+ for match in matches:
1159
+ thing = match.group(1).strip()
1160
+ try:
1161
+ from skills.self_authorship import add_dislike
1162
+ result = add_dislike(thing, config_path=self_path)
1163
+ actions_taken.append(f"idislike: {thing}")
1164
+ print(f"[Self] {result}")
1165
+ except Exception as e:
1166
+ print(f"[Self] Error adding dislike: {e}")
1167
+ response = response.replace(match.group(0), "")
1168
+
1169
+ # Pattern: [SCHEDULE: time | message] - Schedule a message for later
1170
+ schedule_pattern = r'\[SCHEDULE:\s*([^|]+)\s*\|\s*([^\]]+)\]'
1171
+ matches = list(re.finditer(schedule_pattern, response, re.IGNORECASE))
1172
+ for match in matches:
1173
+ time_str = match.group(1).strip()
1174
+ message = match.group(2).strip()
1175
+ try:
1176
+ from skills.message_scheduler import get_message_scheduler
1177
+ from datetime import datetime
1178
+
1179
+ scheduler = get_message_scheduler()
1180
+ scheduled_time = scheduler.parse_time_string(time_str, datetime.now())
1181
+
1182
+ if scheduled_time:
1183
+ scheduler.schedule_message(
1184
+ user_id=user_id,
1185
+ message=message,
1186
+ scheduled_time=scheduled_time,
1187
+ context=f"Scheduled via [SCHEDULE:] tag for '{time_str}'"
1188
+ )
1189
+ actions_taken.append(f"scheduled: {time_str}")
1190
+ print(f"[Self] Scheduled message for {scheduled_time}: {message[:40]}...")
1191
+ else:
1192
+ print(f"[Self] Could not parse time: {time_str}")
1193
+ except Exception as e:
1194
+ print(f"[Self] Error scheduling message: {e}")
1195
+ response = response.replace(match.group(0), "")
1196
+
1197
+ # Clean up multiple spaces
1198
+ response = re.sub(r'\s+', ' ', response).strip()
1199
+
1200
+ return response, actions_taken
1201
+
1202
+
1203
+ def _should_voice(emotion, text) -> bool:
1204
+ kw = ["voice", "hear you", "say it", "speak", "talk to me"]
1205
+ if any(k in text.lower() for k in kw): return True
1206
+ if emotion.get("desire", 0) > 0.7 and random.random() < 0.4: return True
1207
+ return emotion.get("arousal", 0) > 0.7 and random.random() < 0.2
1208
+
1209
+
1210
+ async def _get_media_context(self, text: str, emotion: dict, user_id: str = "") -> str:
1211
+ """
1212
+ Pre-check what media we'll send and return context for LLM.
1213
+ This lets Alive-AI know what photo/video she's about to send.
1214
+ Uses per-user pending media to prevent race conditions.
1215
+ """
1216
+ from .media_handler import _check_photo_triggers
1217
+ from core.settings import get_int
1218
+ import time
1219
+
1220
+ media_parts = []
1221
+
1222
+ # Check photo
1223
+ if not self._photos:
1224
+ print("[Media] No photo system initialized")
1225
+ elif len(self._photos.get_all()) == 0:
1226
+ print("[Media] No photos in index")
1227
+ else:
1228
+ print(f"[Media] Photos available: {len(self._photos.get_all())}")
1229
+ # Check cooldown using settings (consistent with media_handler)
1230
+ last_photo_time = getattr(self, '_last_photo_time', 0)
1231
+ cooldown = get_int("MEDIA_COOLDOWN_PHOTO", 60)
1232
+ if time.time() - last_photo_time < cooldown:
1233
+ print(f"[Media] Photo cooldown active")
1234
+ elif _check_photo_triggers(text, emotion, self):
1235
+ # Get a photo for context
1236
+ photo = self._photos.get_for_context(
1237
+ context=text,
1238
+ arousal=emotion.get("arousal", 0),
1239
+ desire=emotion.get("desire", 0)
1240
+ )
1241
+ if photo:
1242
+ photo_name, photo_desc, photo_cat = photo
1243
+ media_parts.append(f"PHOTO: {photo_desc} (category: {photo_cat})")
1244
+ # Store in per-user pending media
1245
+ _pending_media.setdefault(user_id, {})["photo"] = photo
1246
+ print(f"[Media] Will send photo: {photo_name}")
1247
+ else:
1248
+ print("[Media] No matching photo found")
1249
+
1250
+ # Check video
1251
+ if self._videos and len(self._videos.get_all()) > 0:
1252
+ video_kw = ["video", "clip", "show me a video", "send video"]
1253
+ wants_video = any(kw in text.lower() for kw in video_kw)
1254
+ if wants_video:
1255
+ video = self._videos.get_for_context(text, emotion.get("desire", 0))
1256
+ if video:
1257
+ video_path, video_desc = video
1258
+ media_parts.append(f"VIDEO: {video_desc}")
1259
+ _pending_media.setdefault(user_id, {})["video"] = video
1260
+
1261
+ return " | ".join(media_parts) if media_parts else ""
1262
+
1263
+
1264
+ async def _send_decided_media(self, text, emotion, chat_id, media_context: str, user_id: str = ""):
1265
+ """Send the media we already decided on during pre-check (per-user safe)"""
1266
+ import time
1267
+
1268
+ photo = None
1269
+ video = None
1270
+
1271
+ # Get per-user pending media
1272
+ pending = _pending_media.pop(user_id, {})
1273
+
1274
+ # Send pending photo
1275
+ pending_photo = pending.get("photo")
1276
+ if pending_photo:
1277
+ photo_name, photo_desc, photo_cat = pending_photo
1278
+ photo_path = str(self.base / "mypics" / photo_name)
1279
+
1280
+ if not self._photos.was_recently_sent(photo_name):
1281
+ self._photos.mark_sent(photo_name)
1282
+ self._last_photo_time = time.time()
1283
+ self._photos_sent_session = getattr(self, '_photos_sent_session', 0) + 1
1284
+
1285
+ await self.nervous.emit("chat_action_photo", {})
1286
+ await self.nervous.emit("send_image", {"file_path": photo_path, "chat_id": chat_id, "caption": ""})
1287
+ print(f"[Photo] Sent: {photo_name}")
1288
+ photo = pending_photo
1289
+
1290
+ # Send pending video
1291
+ pending_video = pending.get("video")
1292
+ if pending_video:
1293
+ video_path, video_desc = pending_video
1294
+
1295
+ if not self._videos.was_recently_sent(video_path):
1296
+ self._videos.mark_sent(video_path)
1297
+ self._last_video_time = time.time()
1298
+ self._videos_sent_session = getattr(self, '_videos_sent_session', 0) + 1
1299
+
1300
+ await self.nervous.emit("chat_action_video", {})
1301
+ await self.nervous.emit("send_video", {"file_path": video_path, "chat_id": chat_id, "caption": ""})
1302
+ print(f"[Video] Sent: {video_path}")
1303
+ video = pending_video
1304
+
1305
+ return photo, video
1306
+
1307
+
1308
+ async def _save_memory(user_memory, text, response, emotion, photo, video):
1309
+ """Save conversation to per-user memory"""
1310
+ mem = response + (" [I sent a photo]" if photo else "") + (" [I sent a video]" if video else "")
1311
+ await user_memory.nervous.emit("memory_save", {
1312
+ "type": "conversation",
1313
+ "user_message": text,
1314
+ "ai_response": mem,
1315
+ "emotion": emotion,
1316
+ "user_id": user_memory.user_id # Include for per-user isolation
1317
+ })
1318
+
1319
+
1320
+ # ============================================================
1321
+ # HELPER FUNCTIONS FOR ALIVENESS MODULES
1322
+ # ============================================================
1323
+
1324
+ def _classify_interaction_type(text: str, emotion: dict, detected_bids: list) -> str:
1325
+ """
1326
+ Classify the type of interaction for the interoceptive system.
1327
+
1328
+ Args:
1329
+ text: The message text
1330
+ emotion: Emotion data from the heart
1331
+ detected_bids: List of detected emotional bids
1332
+
1333
+ Returns:
1334
+ Interaction type string for interoceptive system
1335
+ """
1336
+ text_lower = text.lower()
1337
+
1338
+ # Check for intimate/romantic content
1339
+ intimate_keywords = ["love", "miss you", "need you", "kiss", "hug", "hold me", "forever"]
1340
+ if any(kw in text_lower for kw in intimate_keywords):
1341
+ return "intimate_moment"
1342
+
1343
+ # Check for conflict/negative content
1344
+ conflict_keywords = ["angry", "upset", "hurt", "why did you", "hate", "frustrated", "annoyed"]
1345
+ if any(kw in text_lower for kw in conflict_keywords):
1346
+ return "conflict"
1347
+
1348
+ # Check bid types
1349
+ if detected_bids:
1350
+ bid_types = [b.bid_type.value for b in detected_bids]
1351
+ if "vulnerability" in bid_types:
1352
+ return "deep_conversation"
1353
+ if "seeking_validation" in bid_types or "reassurance" in bid_types:
1354
+ return "reassurance"
1355
+
1356
+ # Check for playful content
1357
+ playful_keywords = ["lol", "haha", "hehe", "funny", "joke", "tease", "silly"]
1358
+ if any(kw in text_lower for kw in playful_keywords) or emotion.get("arousal", 0) > 0.6:
1359
+ return "playful_exchange"
1360
+
1361
+ # Check for exciting news
1362
+ exciting_keywords = ["guess what", "you won't believe", "amazing", "incredible", "so happy"]
1363
+ if any(kw in text_lower for kw in exciting_keywords):
1364
+ return "exciting_news"
1365
+
1366
+ # Default to positive interaction
1367
+ if emotion.get("valence", 0.5) > 0.5:
1368
+ return "positive_interaction"
1369
+ else:
1370
+ return "general"
1371
+
1372
+
1373
+ def _calculate_emotional_weight(emotion: dict, detected_bids: list) -> float:
1374
+ """
1375
+ Calculate emotional weight for memory storage.
1376
+
1377
+ Args:
1378
+ emotion: Emotion data from the heart
1379
+ detected_bids: List of detected emotional bids
1380
+
1381
+ Returns:
1382
+ Emotional weight (0-1)
1383
+ """
1384
+ weight = 0.3 # Base weight
1385
+
1386
+ # High arousal increases weight
1387
+ arousal = emotion.get("arousal", 0)
1388
+ weight += arousal * 0.2
1389
+
1390
+ # High desire increases weight
1391
+ desire = emotion.get("desire", 0)
1392
+ weight += desire * 0.15
1393
+
1394
+ # Extreme valence (positive or negative) increases weight
1395
+ valence = emotion.get("valence", 0.5)
1396
+ valence_extremity = abs(valence - 0.5) * 2 # 0 to 1
1397
+ weight += valence_extremity * 0.15
1398
+
1399
+ # Bids with high intensity increase weight
1400
+ if detected_bids:
1401
+ max_bid_intensity = max(
1402
+ (b.confidence for b in detected_bids),
1403
+ default=0
1404
+ )
1405
+ weight += max_bid_intensity * 0.2
1406
+
1407
+ return min(1.0, weight)
1408
+
1409
+
1410
+ # ============================================================
1411
+ # MODULE STATUS FUNCTIONS
1412
+ # ============================================================
1413
+
1414
+ def get_aliveness_module_status() -> dict:
1415
+ """
1416
+ Get status of all aliveness modules.
1417
+
1418
+ Returns:
1419
+ Dictionary with module availability status
1420
+ """
1421
+ modules = {
1422
+ "interoception": INTEROCEPTION_AVAILABLE,
1423
+ "default_mode": DEFAULT_MODE_AVAILABLE,
1424
+ "bid_detector": BID_DETECTOR_AVAILABLE,
1425
+ "emotional_memory": EMOTIONAL_MEMORY_AVAILABLE,
1426
+ "inconsistency": INCONSISTENCY_AVAILABLE,
1427
+ "afterglow": AFTERGLOW_AVAILABLE,
1428
+ "circadian": CIRCADIAN_AVAILABLE,
1429
+ "mood_shifts": MOOD_SHIFTS_AVAILABLE,
1430
+ "attachment": ATTACHMENT_AVAILABLE,
1431
+ "phantom_somatic": PHANTOM_SOMATIC_AVAILABLE,
1432
+ "narrative": NARRATIVE_AVAILABLE,
1433
+ "dreams": DREAMS_AVAILABLE,
1434
+ "linguistic": LINGUISTIC_AVAILABLE,
1435
+ "curiosity": CURIOSITY_AVAILABLE,
1436
+ "almost_said": ALMOST_SAID_AVAILABLE,
1437
+ }
1438
+ modules["modules_active"] = sum(v for v in modules.values() if isinstance(v, bool))
1439
+ return modules
1440
+