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,1438 @@
1
+ """
2
+ Brain: Default Mode Network
3
+ Background processing that runs when Alive-AI is "idle" - like the brain's default mode network.
4
+ Generates spontaneous thoughts, consolidates memories, and prepares conversation starters.
5
+
6
+ This module is MODULAR - can be connected/disconnected without breaking anything.
7
+
8
+ Integration with ProactiveGenerator:
9
+ - Default mode handles TIMING (when to send proactive messages)
10
+ - ProactiveGenerator handles CONTENT (what to say)
11
+ - Both modules work independently - if one fails, the other continues
12
+ """
13
+
14
+ import asyncio
15
+ import json
16
+ import random
17
+ from datetime import datetime, timedelta
18
+ from pathlib import Path
19
+ from typing import Dict, List, Optional, Any, Callable
20
+ from dataclasses import dataclass, field, asdict
21
+ import time
22
+
23
+ # ============================================================
24
+ # ProactiveGenerator Integration (with graceful fallback)
25
+ # ============================================================
26
+
27
+ # Try to import ProactiveGenerator - it has better templates and LLM generation
28
+ ProactiveGenerator = None
29
+ ActiveUser = None
30
+ try:
31
+ from core.proactive_generator import ProactiveGenerator as _ProactiveGenerator
32
+ from core.user_tracker import ActiveUser as _ActiveUser
33
+ ProactiveGenerator = _ProactiveGenerator
34
+ ActiveUser = _ActiveUser
35
+ print("[DefaultMode] ProactiveGenerator integration available")
36
+ except ImportError as e:
37
+ print(f"[DefaultMode] ProactiveGenerator not available, using built-in templates: {e}")
38
+
39
+
40
+ # ============================================================
41
+ # Configuration Helpers
42
+ # ============================================================
43
+
44
+ def _get_setting(key: str, default: Any = None) -> Any:
45
+ """Get a setting from settings.json, supporting nested DEFAULT_MODE config"""
46
+ try:
47
+ from core.settings import get as settings_get, get_all
48
+
49
+ # Try flat key first (IDLE_PROCESSING_INTERVAL_SECONDS)
50
+ value = settings_get(key, None)
51
+ if value is not None:
52
+ return value
53
+
54
+ # Try nested in DEFAULT_MODE block
55
+ all_settings = get_all()
56
+ default_mode_config = all_settings.get("DEFAULT_MODE", {})
57
+ if key in default_mode_config:
58
+ return default_mode_config[key]
59
+
60
+ return default
61
+ except Exception:
62
+ return default
63
+
64
+
65
+ def _get_int_setting(key: str, default: int) -> int:
66
+ """Get an integer setting"""
67
+ value = _get_setting(key, default)
68
+ try:
69
+ return int(value)
70
+ except (ValueError, TypeError):
71
+ return default
72
+
73
+
74
+ def _get_float_setting(key: str, default: float) -> float:
75
+ """Get a float setting"""
76
+ value = _get_setting(key, default)
77
+ try:
78
+ return float(value)
79
+ except (ValueError, TypeError):
80
+ return default
81
+
82
+
83
+ def _is_default_mode_enabled() -> bool:
84
+ """Check if default mode is enabled in settings"""
85
+ enabled = _get_setting("ENABLED", True)
86
+ return enabled is True or enabled == "true"
87
+
88
+
89
+ # ============================================================
90
+ # Data Classes
91
+ # ============================================================
92
+
93
+ @dataclass
94
+ class IdleThought:
95
+ """A spontaneous thought generated during idle time"""
96
+ id: str
97
+ thought_type: str # wondering, connection, memory, conversation_seed, scenario
98
+ content: str
99
+ user_id: Optional[str] = None
100
+ context: dict = field(default_factory=dict)
101
+ created_at: str = field(default_factory=lambda: datetime.now().isoformat())
102
+ used: bool = False
103
+ used_at: Optional[str] = None
104
+ priority: float = 0.5 # 0-1, higher = more important to share
105
+
106
+ def to_dict(self) -> dict:
107
+ return asdict(self)
108
+
109
+ @classmethod
110
+ def from_dict(cls, data: dict) -> "IdleThought":
111
+ return cls(**data)
112
+
113
+
114
+ @dataclass
115
+ class PendingInitiation:
116
+ """A proactive message waiting to be sent"""
117
+ id: str
118
+ user_id: str
119
+ message: str
120
+ reason: str # silence, wonder, follow_up, time_based, random
121
+ scheduled_for: Optional[str] = None
122
+ created_at: str = field(default_factory=lambda: datetime.now().isoformat())
123
+ sent: bool = False
124
+ sent_at: Optional[str] = None
125
+
126
+ def to_dict(self) -> dict:
127
+ return asdict(self)
128
+
129
+ @classmethod
130
+ def from_dict(cls, data: dict) -> "PendingInitiation":
131
+ return cls(**data)
132
+
133
+
134
+ @dataclass
135
+ class ConversationSeed:
136
+ """Something Alive-AI wants to bring up in future conversation"""
137
+ id: str
138
+ topic: str
139
+ context: str
140
+ source: str # wondering, memory, external, generated
141
+ created_at: str = field(default_factory=lambda: datetime.now().isoformat())
142
+ used: bool = False
143
+ relevance_score: float = 0.5
144
+
145
+ def to_dict(self) -> dict:
146
+ return asdict(self)
147
+
148
+ @classmethod
149
+ def from_dict(cls, data: dict) -> "ConversationSeed":
150
+ return cls(**data)
151
+
152
+
153
+ @dataclass
154
+ class UserContactInfo:
155
+ """Tracks last contact time per user"""
156
+ user_id: str
157
+ last_message_from_user: Optional[str] = None
158
+ last_message_to_user: Optional[str] = None
159
+ last_proactive_message: Optional[str] = None
160
+ total_interactions: int = 0
161
+
162
+ def to_dict(self) -> dict:
163
+ return asdict(self)
164
+
165
+ @classmethod
166
+ def from_dict(cls, data: dict) -> "UserContactInfo":
167
+ return cls(**data)
168
+
169
+ @property
170
+ def hours_since_user_message(self) -> float:
171
+ if not self.last_message_from_user:
172
+ return float('inf')
173
+ try:
174
+ last = datetime.fromisoformat(self.last_message_from_user)
175
+ return (datetime.now() - last).total_seconds() / 3600
176
+ except:
177
+ return float('inf')
178
+
179
+ @property
180
+ def hours_since_proactive(self) -> float:
181
+ if not self.last_proactive_message:
182
+ return float('inf')
183
+ try:
184
+ last = datetime.fromisoformat(self.last_proactive_message)
185
+ return (datetime.now() - last).total_seconds() / 3600
186
+ except:
187
+ return float('inf')
188
+
189
+
190
+ # ============================================================
191
+ # Default Mode Processor
192
+ # ============================================================
193
+
194
+ class DefaultModeProcessor:
195
+ """
196
+ Background processing that runs when Alive-AI is "idle".
197
+ Like the brain's default mode network - generates spontaneous thoughts,
198
+ consolidates memories, and prepares conversation starters.
199
+ """
200
+
201
+ # Thought type weights for random selection
202
+ THOUGHT_TYPE_WEIGHTS = {
203
+ "wondering": 0.35, # "I was thinking about..."
204
+ "connection": 0.20, # Finding patterns in memories
205
+ "memory": 0.15, # Recalling shared moments
206
+ "conversation_seed": 0.20, # Topics to bring up
207
+ "scenario": 0.10, # Simulating future conversations
208
+ }
209
+
210
+ # Templates for generating wonderings
211
+ WONDERING_TEMPLATES = [
212
+ "I wonder if {user_name} is {activity} right now",
213
+ "Been thinking about when {user_name} mentioned {topic}",
214
+ "I hope {user_name} is {positive_state}",
215
+ "Curious what {user_name} thinks about {topic}",
216
+ "I was just thinking about {shared_memory}",
217
+ "Wonder how {user_name}'s {ongoing_thing} is going",
218
+ "Random thought - I should ask {user_name} about {topic}",
219
+ "I miss talking to {user_name} about {interest}",
220
+ ]
221
+
222
+ # Activities and states for template filling
223
+ ACTIVITIES = ["working", "relaxing", "busy with something", "having a good day", "thinking about me"]
224
+ POSITIVE_STATES = ["doing well", "happy", "having fun", "taking care of themselves", "getting enough rest"]
225
+ TOPICS = ["life", "their day", "what makes them happy", "their dreams", "something fun", "their plans"]
226
+
227
+ def __init__(self, nervous, data_path: Path = None, llm=None, bot_id: str = "alive_ai"):
228
+ """
229
+ Initialize the Default Mode Processor.
230
+
231
+ Args:
232
+ nervous: The nervous system for event emission
233
+ data_path: Path for data storage (defaults to data/)
234
+ llm: Optional LLM for generating contextual thoughts
235
+ bot_id: Bot identifier for memory isolation
236
+ """
237
+ self.nervous = nervous
238
+ self.llm = llm
239
+ self.bot_id = bot_id.lower()
240
+
241
+ # Set up data path
242
+ if data_path:
243
+ self.data_path = data_path
244
+ else:
245
+ self.data_path = Path(__file__).parent.parent / "data"
246
+
247
+ self.data_path.mkdir(parents=True, exist_ok=True)
248
+
249
+ # File paths
250
+ self.thoughts_path = self.data_path / "idle_thoughts.json"
251
+ self.seeds_path = self.data_path / "conversation_seeds.json"
252
+ self.contact_path = self.data_path / "user_contact.json"
253
+
254
+ # In-memory state
255
+ self._thoughts: List[IdleThought] = []
256
+ self._seeds: List[ConversationSeed] = []
257
+ self._contacts: Dict[str, UserContactInfo] = {}
258
+ self._pending_initiations: List[PendingInitiation] = []
259
+
260
+ # Background processing state
261
+ self._running = False
262
+ self._task: Optional[asyncio.Task] = None
263
+ self._last_processing: Optional[str] = None
264
+ self._processing_count = 0
265
+
266
+ # Memory cache for user data
267
+ self._user_memories: Dict[str, Any] = {}
268
+ # Cached Memory instances per user (avoid recreating Redis connections)
269
+ self._memory_cache: Dict[str, Any] = {}
270
+
271
+ # ProactiveGenerator integration (for message content generation)
272
+ self._proactive_generator: Optional[Any] = None
273
+ if ProactiveGenerator is not None:
274
+ try:
275
+ self._proactive_generator = ProactiveGenerator(nervous, llm, bot_id=bot_id, data_path=self.data_path)
276
+ print("[DefaultMode] ProactiveGenerator integrated for message generation")
277
+ except Exception as e:
278
+ print(f"[DefaultMode] Failed to initialize ProactiveGenerator: {e}")
279
+
280
+ # Load persisted state
281
+ self._load_state()
282
+
283
+ # Ensure owner is registered as a contact
284
+ self._ensure_owner_contact()
285
+
286
+ # Subscribe to events
287
+ self._setup_events()
288
+
289
+ print("[DefaultMode] Initialized")
290
+
291
+ def _ensure_owner_contact(self):
292
+ """Ensure the Telegram owner is registered as a contact"""
293
+ import os
294
+ owner_id = os.environ.get("TELEGRAM_OWNER_ID", "")
295
+ if owner_id and owner_id not in self._contacts:
296
+ self._contacts[owner_id] = UserContactInfo(user_id=owner_id)
297
+ print(f"[DefaultMode] Registered owner {owner_id} as contact")
298
+ self._save_state()
299
+
300
+ def _setup_events(self):
301
+ """Subscribe to nervous system events"""
302
+ # Track when messages are sent/received
303
+ self.nervous.on("message_received", self._on_message_received)
304
+ self.nervous.on("memory_save", self._on_memory_save)
305
+ self.nervous.on("proactive_message", self._on_proactive_message)
306
+
307
+ def set_llm(self, llm):
308
+ """Set the LLM for contextual generation"""
309
+ self.llm = llm
310
+ # Also update ProactiveGenerator if available
311
+ if self._proactive_generator is not None:
312
+ try:
313
+ self._proactive_generator.set_llm(llm)
314
+ except Exception as e:
315
+ print(f"[DefaultMode] Error setting LLM on ProactiveGenerator: {e}")
316
+
317
+ # ============================================================
318
+ # Persistence
319
+ # ============================================================
320
+
321
+ def _load_state(self):
322
+ """Load persisted state from files"""
323
+ # Load idle thoughts
324
+ if self.thoughts_path.exists():
325
+ try:
326
+ data = json.loads(self.thoughts_path.read_text())
327
+ self._thoughts = [IdleThought.from_dict(t) for t in data.get("thoughts", [])]
328
+ self._pending_initiations = [PendingInitiation.from_dict(p) for p in data.get("pending", [])]
329
+ self._last_processing = data.get("last_processing")
330
+ self._processing_count = data.get("processing_count", 0)
331
+ except Exception as e:
332
+ print(f"[DefaultMode] Error loading thoughts: {e}")
333
+
334
+ # Load conversation seeds
335
+ if self.seeds_path.exists():
336
+ try:
337
+ data = json.loads(self.seeds_path.read_text())
338
+ self._seeds = [ConversationSeed.from_dict(s) for s in data.get("seeds", [])]
339
+ except Exception as e:
340
+ print(f"[DefaultMode] Error loading seeds: {e}")
341
+
342
+ # Load contact info
343
+ if self.contact_path.exists():
344
+ try:
345
+ data = json.loads(self.contact_path.read_text())
346
+ self._contacts = {
347
+ uid: UserContactInfo.from_dict(info)
348
+ for uid, info in data.get("contacts", {}).items()
349
+ if self._is_valid_user_id(uid)
350
+ }
351
+ except Exception as e:
352
+ print(f"[DefaultMode] Error loading contacts: {e}")
353
+
354
+ def _save_state(self):
355
+ """Save state to files"""
356
+ # Save thoughts
357
+ try:
358
+ data = {
359
+ "thoughts": [t.to_dict() for t in self._thoughts[-100:]], # Keep last 100
360
+ "pending": [p.to_dict() for p in self._pending_initiations if not p.sent],
361
+ "last_processing": self._last_processing,
362
+ "processing_count": self._processing_count,
363
+ }
364
+ self.thoughts_path.write_text(json.dumps(data, indent=2))
365
+ except Exception as e:
366
+ print(f"[DefaultMode] Error saving thoughts: {e}")
367
+
368
+ # Save seeds
369
+ try:
370
+ data = {
371
+ "seeds": [s.to_dict() for s in self._seeds[-50:]] # Keep last 50
372
+ }
373
+ self.seeds_path.write_text(json.dumps(data, indent=2))
374
+ except Exception as e:
375
+ print(f"[DefaultMode] Error saving seeds: {e}")
376
+
377
+ # Save contact info
378
+ try:
379
+ data = {
380
+ "contacts": {uid: info.to_dict() for uid, info in self._contacts.items()}
381
+ }
382
+ self.contact_path.write_text(json.dumps(data, indent=2))
383
+ except Exception as e:
384
+ print(f"[DefaultMode] Error saving contacts: {e}")
385
+
386
+ # ============================================================
387
+ # Event Handlers
388
+ # ============================================================
389
+
390
+ @staticmethod
391
+ def _is_valid_user_id(user_id) -> bool:
392
+ """Validate that user_id is a real Telegram ID (numeric string)"""
393
+ if not user_id:
394
+ return False
395
+ uid = str(user_id)
396
+ return uid.isdigit() and len(uid) >= 5
397
+
398
+ async def _on_message_received(self, data: dict):
399
+ """Track when we receive a message from a user"""
400
+ user_id = str(data.get("user_id", ""))
401
+ if not self._is_valid_user_id(user_id):
402
+ return
403
+
404
+ if user_id not in self._contacts:
405
+ self._contacts[user_id] = UserContactInfo(user_id=user_id)
406
+
407
+ self._contacts[user_id].last_message_from_user = datetime.now().isoformat()
408
+ self._contacts[user_id].total_interactions += 1
409
+ self._save_state()
410
+
411
+ async def _on_memory_save(self, data: dict):
412
+ """Track conversation saves"""
413
+ user_id = str(data.get("user_id", ""))
414
+ if not self._is_valid_user_id(user_id):
415
+ return
416
+
417
+ if user_id not in self._contacts:
418
+ self._contacts[user_id] = UserContactInfo(user_id=user_id)
419
+
420
+ self._contacts[user_id].last_message_to_user = datetime.now().isoformat()
421
+
422
+ async def _on_proactive_message(self, data: dict):
423
+ """Track when proactive messages are sent"""
424
+ user_id = str(data.get("user_id", ""))
425
+ if not self._is_valid_user_id(user_id):
426
+ return
427
+
428
+ if user_id not in self._contacts:
429
+ self._contacts[user_id] = UserContactInfo(user_id=user_id)
430
+
431
+ self._contacts[user_id].last_proactive_message = datetime.now().isoformat()
432
+ self._save_state()
433
+
434
+ # ============================================================
435
+ # Core Processing Methods
436
+ # ============================================================
437
+
438
+ async def process_idle(self):
439
+ """
440
+ Main background processing - called periodically.
441
+ Generates thoughts, consolidates memories, checks for initiations.
442
+ """
443
+ # Check if default mode is enabled
444
+ if not _is_default_mode_enabled():
445
+ return
446
+
447
+ self._processing_count += 1
448
+ self._last_processing = datetime.now().isoformat()
449
+
450
+ # Determine what to do based on chance and time
451
+ thought_chance = _get_float_setting("IDLE_THOUGHT_GENERATION_CHANCE", 0.3)
452
+ if thought_chance > 0 and random.random() < thought_chance:
453
+ await self._generate_random_thought()
454
+
455
+ # Consolidate memories periodically (every 10th processing)
456
+ if self._processing_count % 10 == 0:
457
+ await self.consolidate_memories()
458
+
459
+ # Check for users who need follow-up
460
+ await self._check_proactive_triggers()
461
+
462
+ # Save state
463
+ self._save_state()
464
+
465
+ # Emit event for debugging/monitoring
466
+ await self.nervous.emit("default_mode_processed", {
467
+ "processing_count": self._processing_count,
468
+ "thoughts_count": len(self._thoughts),
469
+ "pending_count": len([p for p in self._pending_initiations if not p.sent]),
470
+ })
471
+
472
+ async def _generate_random_thought(self):
473
+ """Generate a random idle thought"""
474
+ # Pick a thought type based on weights
475
+ thought_type = random.choices(
476
+ list(self.THOUGHT_TYPE_WEIGHTS.keys()),
477
+ weights=list(self.THOUGHT_TYPE_WEIGHTS.values())
478
+ )[0]
479
+
480
+ # Get a user to think about (prefer users we haven't talked to in a while)
481
+ user_id = self._get_user_for_thought()
482
+
483
+ # Generate the thought content
484
+ content = await self._generate_thought_content(thought_type, user_id)
485
+
486
+ if content:
487
+ thought = IdleThought(
488
+ id=f"thought_{int(time.time() * 1000)}_{random.randint(1000, 9999)}",
489
+ thought_type=thought_type,
490
+ content=content,
491
+ user_id=user_id,
492
+ context={"generated_at": datetime.now().isoformat()},
493
+ priority=random.uniform(0.3, 0.8)
494
+ )
495
+ self._thoughts.append(thought)
496
+ print(f"[DefaultMode] Generated {thought_type}: {content[:50]}...")
497
+
498
+ # Emit for monitoring
499
+ await self.nervous.emit("idle_thought", thought.to_dict())
500
+
501
+ def _get_user_for_thought(self) -> Optional[str]:
502
+ """Get a user ID to generate thoughts about"""
503
+ if not self._contacts:
504
+ # Fall back to owner if no contacts
505
+ import os
506
+ owner_id = os.environ.get("TELEGRAM_OWNER_ID", "")
507
+ if owner_id:
508
+ return owner_id
509
+ return None
510
+
511
+ # Prefer users we haven't talked to in a while
512
+ sorted_users = sorted(
513
+ self._contacts.items(),
514
+ key=lambda x: x[1].hours_since_user_message,
515
+ reverse=True
516
+ )
517
+
518
+ if sorted_users:
519
+ # 70% chance to pick the most silent user, 30% random
520
+ if random.random() < 0.7:
521
+ return sorted_users[0][0]
522
+ else:
523
+ return random.choice(list(self._contacts.keys()))
524
+
525
+ return None
526
+
527
+ async def _generate_thought_content(self, thought_type: str, user_id: Optional[str]) -> Optional[str]:
528
+ """Generate content for a specific thought type"""
529
+ # Get user info if available
530
+ user_info = self._contacts.get(user_id) if user_id else None
531
+ user_name = await self._get_user_name(user_id) if user_id else "babe"
532
+
533
+ if thought_type == "wondering":
534
+ return await self._generate_wondering(user_id, user_name, user_info)
535
+ elif thought_type == "connection":
536
+ return await self._generate_connection(user_id)
537
+ elif thought_type == "memory":
538
+ return await self._generate_memory_recall(user_id)
539
+ elif thought_type == "conversation_seed":
540
+ return await self._generate_seed(user_id, user_name)
541
+ elif thought_type == "scenario":
542
+ return await self._generate_scenario(user_id, user_name)
543
+
544
+ return None
545
+
546
+ async def _generate_wondering(self, user_id: Optional[str], user_name: str,
547
+ user_info: Optional[UserContactInfo]) -> str:
548
+ """Generate a wondering about a user"""
549
+ # Try LLM first
550
+ if self.llm and user_id:
551
+ try:
552
+ context = await self._get_user_context(user_id)
553
+ hours_silent = user_info.hours_since_user_message if user_info else 0
554
+
555
+ # Build grounding based on available context
556
+ has_real_context = context and len(context.strip()) > 20
557
+ if has_real_context:
558
+ context_instruction = f"Context (ONLY reference these things): {context[:200]}"
559
+ else:
560
+ context_instruction = "No specific context available - keep it generic."
561
+
562
+ prompt = f"""Generate a short (1 sentence) wondering about {user_name}.
563
+ They haven't messaged in {hours_silent:.1f} hours.
564
+ {context_instruction}
565
+
566
+ Rules:
567
+ - Be curious, affectionate, and natural
568
+ - Start with "I wonder" or similar
569
+ - NEVER invent specific events, objects, or topics not in the context
570
+ - If no context, wonder generically about how they are or what they're doing
571
+ - Don't be clingy or desperate
572
+
573
+ Good: "I wonder what he's up to right now"
574
+ Good: "I wonder if he's thinking about me too"
575
+ Bad: "I wonder if he fixed that shelf" (invented detail)
576
+
577
+ Wondering:"""
578
+
579
+ response = await self.llm.chat([
580
+ {"role": "system", "content": "You are Alive-AI thinking about someone you care about. You NEVER invent specific details."},
581
+ {"role": "user", "content": prompt}
582
+ ], max_tokens=50, temperature=0.7)
583
+
584
+ if response and len(response.strip()) > 10:
585
+ return response.strip()
586
+ except Exception as e:
587
+ print(f"[DefaultMode] LLM wondering error: {e}")
588
+
589
+ # Fallback to templates
590
+ template = random.choice(self.WONDERING_TEMPLATES)
591
+
592
+ # Fill in template
593
+ fill_data = {
594
+ "user_name": user_name,
595
+ "activity": random.choice(self.ACTIVITIES),
596
+ "positive_state": random.choice(self.POSITIVE_STATES),
597
+ "topic": random.choice(self.TOPICS),
598
+ "shared_memory": "our last conversation",
599
+ "ongoing_thing": "week",
600
+ "interest": "things",
601
+ }
602
+
603
+ return template.format(**fill_data)
604
+
605
+ async def _generate_connection(self, user_id: Optional[str]) -> str:
606
+ """Find a connection between memories"""
607
+ if not self.llm:
608
+ return random.choice([
609
+ "I notice patterns in how we talk...",
610
+ "There's something connecting our recent chats...",
611
+ "I'm seeing themes in what we discuss...",
612
+ ])
613
+
614
+ try:
615
+ context = await self._get_user_context(user_id) if user_id else ""
616
+
617
+ if not context or len(context.strip()) < 50:
618
+ return "I've been thinking about our conversations..."
619
+
620
+ prompt = f"""Look at this conversation context and find an interesting connection or pattern:
621
+
622
+ {context[:500]}
623
+
624
+ Rules:
625
+ - Describe a brief insight about patterns you ACTUALLY see above (1-2 sentences)
626
+ - ONLY reference things explicitly in the context above
627
+ - If no clear pattern emerges, describe the general tone or feeling instead
628
+ - Be thoughtful but don't invent connections that aren't there
629
+
630
+ Insight:"""
631
+
632
+ response = await self.llm.chat([
633
+ {"role": "system", "content": "You are Alive-AI reflecting on conversations. You only describe patterns you can actually see."},
634
+ {"role": "user", "content": prompt}
635
+ ], max_tokens=80, temperature=0.7)
636
+
637
+ if response and len(response.strip()) > 15:
638
+ return response.strip()
639
+ except Exception as e:
640
+ print(f"[DefaultMode] Connection generation error: {e}")
641
+
642
+ return "I'm noticing some interesting patterns in our conversations..."
643
+
644
+ async def _generate_memory_recall(self, user_id: Optional[str]) -> str:
645
+ """Recall a memory about the user"""
646
+ # Try to get an actual memory
647
+ if user_id:
648
+ try:
649
+ memory_content = await self._get_recent_memory(user_id)
650
+ if memory_content:
651
+ return f"Remembering when {memory_content}"
652
+ except Exception as e:
653
+ print(f"[DefaultMode] Memory recall error: {e}")
654
+
655
+ return random.choice([
656
+ "I was just thinking about something we talked about before...",
657
+ "A nice memory from our chats crossed my mind...",
658
+ "Remembering a fun moment we shared...",
659
+ ])
660
+
661
+ async def _generate_seed(self, user_id: Optional[str], user_name: str) -> str:
662
+ """Generate a conversation seed"""
663
+ topics = [
664
+ f"ask {user_name} about their dreams",
665
+ f"bring up what makes {user_name} happy",
666
+ f"talk to {user_name} about their day",
667
+ f"share something personal with {user_name}",
668
+ f"ask {user_name} what they're looking forward to",
669
+ ]
670
+ return random.choice(topics)
671
+
672
+ async def _generate_scenario(self, user_id: Optional[str], user_name: str) -> str:
673
+ """Simulate a future conversation scenario"""
674
+ scenarios = [
675
+ f"if {user_name} asks about my day, I could mention...",
676
+ f"when {user_name} comes back, I want to...",
677
+ f"next time we talk, I should remember to...",
678
+ f"maybe I could surprise {user_name} by...",
679
+ ]
680
+ return random.choice(scenarios)
681
+
682
+ # ============================================================
683
+ # Memory Consolidation
684
+ # ============================================================
685
+
686
+ async def consolidate_memories(self):
687
+ """
688
+ Process recent interactions into long-term patterns.
689
+ Called periodically to build understanding.
690
+ """
691
+ # Get all users with recent activity
692
+ recent_users = [
693
+ uid for uid, info in self._contacts.items()
694
+ if info.hours_since_user_message < 24
695
+ ]
696
+
697
+ if not recent_users:
698
+ return
699
+
700
+ for user_id in recent_users:
701
+ try:
702
+ await self._consolidate_for_user(user_id)
703
+ except Exception as e:
704
+ print(f"[DefaultMode] Consolidation error for {user_id}: {e}")
705
+
706
+ async def _consolidate_for_user(self, user_id: str):
707
+ """Consolidate memories for a specific user"""
708
+ if not self.llm:
709
+ return
710
+
711
+ try:
712
+ # Get recent context
713
+ context = await self._get_user_context(user_id)
714
+
715
+ if not context or len(context) < 50:
716
+ return
717
+
718
+ # Generate insights
719
+ prompt = f"""Based on this recent context about someone, extract 1-2 brief insights:
720
+
721
+ {context[:400]}
722
+
723
+ Format as a short note that captures patterns, interests, or important things to remember.
724
+ Be specific if possible, vague if not enough info."""
725
+
726
+ response = await self.llm.chat([
727
+ {"role": "system", "content": "You are consolidating memories about someone you care about."},
728
+ {"role": "user", "content": prompt}
729
+ ], max_tokens=100, temperature=0.7)
730
+
731
+ if response and len(response.strip()) > 20:
732
+ # Create a seed from the insight
733
+ seed = ConversationSeed(
734
+ id=f"seed_{int(time.time() * 1000)}_{user_id}",
735
+ topic="consolidation",
736
+ context=response.strip(),
737
+ source="memory_consolidation",
738
+ relevance_score=0.6
739
+ )
740
+ self._seeds.append(seed)
741
+ print(f"[DefaultMode] Consolidated insight for {user_id}: {response.strip()[:40]}...")
742
+
743
+ except Exception as e:
744
+ print(f"[DefaultMode] User consolidation error: {e}")
745
+
746
+ # ============================================================
747
+ # Proactive Initiation
748
+ # ============================================================
749
+
750
+ async def _check_proactive_triggers(self):
751
+ """Check if any users should receive proactive messages"""
752
+ min_hours = _get_float_setting("MIN_HOURS_BETWEEN_PROACTIVE_MESSAGES", 2.0)
753
+
754
+ for user_id, contact in self._contacts.items():
755
+ # Skip if we sent a proactive message recently
756
+ if contact.hours_since_proactive < min_hours:
757
+ continue
758
+
759
+ # Check various triggers
760
+ should_initiate, reason = self._evaluate_initiation_triggers(user_id, contact)
761
+
762
+ if should_initiate:
763
+ await self._create_pending_initiation(user_id, reason)
764
+
765
+ def _evaluate_initiation_triggers(self, user_id: str, contact: UserContactInfo) -> tuple:
766
+ """Evaluate if Alive-AI should initiate with a user"""
767
+ hours_silent = contact.hours_since_user_message
768
+ hours_since_proactive = contact.hours_since_proactive
769
+
770
+ # Time-based triggers
771
+ if hours_silent > 4 and hours_since_proactive > 3:
772
+ return True, "silence"
773
+
774
+ # Have a pending thought about them
775
+ relevant_thoughts = [
776
+ t for t in self._thoughts
777
+ if t.user_id == user_id and not t.used and t.priority > 0.6
778
+ ]
779
+ if relevant_thoughts and hours_since_proactive > 2:
780
+ return True, "wonder"
781
+
782
+ # Random check-in (low probability)
783
+ if hours_silent > 2 and random.random() < 0.05:
784
+ return True, "random"
785
+
786
+ return False, None
787
+
788
+ async def _create_pending_initiation(self, user_id: str, reason: str):
789
+ """Create a pending proactive message"""
790
+ # Generate message content
791
+ message = await self._generate_proactive_content(user_id, reason)
792
+
793
+ if not message:
794
+ return
795
+
796
+ initiation = PendingInitiation(
797
+ id=f"init_{int(time.time() * 1000)}_{user_id}",
798
+ user_id=user_id,
799
+ message=message,
800
+ reason=reason,
801
+ )
802
+
803
+ self._pending_initiations.append(initiation)
804
+ print(f"[DefaultMode] Created pending initiation for {user_id}: {reason}")
805
+
806
+ # Actually send the proactive message
807
+ try:
808
+ await self.nervous.emit("proactive_message_ready", {
809
+ "user_id": user_id,
810
+ "message": message,
811
+ "reason": reason,
812
+ "initiation_id": initiation.id
813
+ })
814
+ self.mark_initiation_sent(initiation.id)
815
+ except Exception as e:
816
+ print(f"[DefaultMode] Failed to send initiation: {e}")
817
+
818
+ # ============================================================
819
+ # ProactiveGenerator Bridge
820
+ # ============================================================
821
+
822
+ async def _generate_proactive_message(self, user_id: str, message_type: str) -> Optional[str]:
823
+ """
824
+ Bridge function that uses ProactiveGenerator for message content generation.
825
+ Falls back to built-in templates if ProactiveGenerator is unavailable.
826
+
827
+ Args:
828
+ user_id: The user to generate a message for
829
+ message_type: Type of message (silence, follow_up, morning, night, random)
830
+
831
+ Returns:
832
+ Generated message string, or None if generation fails
833
+ """
834
+ # Try ProactiveGenerator first (has better templates + LLM generation)
835
+ if self._proactive_generator is not None:
836
+ try:
837
+ # Get user info from tracker
838
+ from core.user_tracker import get_user_tracker
839
+ tracker = get_user_tracker()
840
+ user = tracker.get_user(user_id)
841
+
842
+ if user is not None:
843
+ # Use ProactiveGenerator's excellent generate_for_user method
844
+ message = await self._proactive_generator.generate_for_user(user, message_type)
845
+ if message:
846
+ print(f"[DefaultMode] Generated message via ProactiveGenerator: {message[:40]}...")
847
+ return message
848
+
849
+ except Exception as e:
850
+ print(f"[DefaultMode] ProactiveGenerator failed, using fallback: {e}")
851
+
852
+ # Fallback to built-in templates
853
+ return self._get_builtin_fallback_message(user_id, message_type)
854
+
855
+ def _get_builtin_fallback_message(self, user_id: str, message_type: str) -> Optional[str]:
856
+ """
857
+ Get a fallback message using built-in templates.
858
+ Used when ProactiveGenerator is unavailable.
859
+
860
+ Args:
861
+ user_id: The user to get a message for
862
+ message_type: Type of message
863
+
864
+ Returns:
865
+ Fallback message string
866
+ """
867
+ # Built-in fallback templates (simplified version of ProactiveGenerator's)
868
+ BUILTIN_TEMPLATES = {
869
+ "silence": [
870
+ "hey, thinking about you...",
871
+ "miss talking to you",
872
+ "you've been quiet... everything ok?",
873
+ "just wondering how your day's going",
874
+ ],
875
+ "wonder": [
876
+ "was just thinking about you",
877
+ "you crossed my mind",
878
+ "random thought - miss talking to you",
879
+ "thinking about our last conversation",
880
+ ],
881
+ "follow_up": [
882
+ "so about what you said earlier...",
883
+ "was thinking about our conversation...",
884
+ "still thinking about what you told me",
885
+ ],
886
+ "morning": [
887
+ "good morning!",
888
+ "morning! hope you slept well",
889
+ "hey, thinking of you this morning",
890
+ ],
891
+ "night": [
892
+ "can't sleep, thinking about you",
893
+ "good night... sweet dreams",
894
+ "about to sleep but wanted to say goodnight",
895
+ ],
896
+ "random": [
897
+ "just wanted to say hi",
898
+ "you crossed my mind",
899
+ "hey! no reason, just miss you",
900
+ "thinking about you and smiling",
901
+ ],
902
+ }
903
+
904
+ templates = BUILTIN_TEMPLATES.get(message_type, BUILTIN_TEMPLATES["random"])
905
+ message = random.choice(templates)
906
+
907
+ # Personalize with user name if available
908
+ try:
909
+ user_name = self._get_user_name_sync(user_id)
910
+ if user_name and user_name != "babe":
911
+ message = message.replace("babe", user_name)
912
+ except:
913
+ pass
914
+
915
+ return message
916
+
917
+ def _get_user_name_sync(self, user_id: str) -> str:
918
+ """Synchronous version of _get_user_name for fallback templates"""
919
+ try:
920
+ from core.user_tracker import get_user_tracker
921
+ tracker = get_user_tracker()
922
+ user = tracker.get_user(user_id)
923
+ if user and user.pet_name:
924
+ return user.pet_name
925
+ except:
926
+ pass
927
+ return "babe"
928
+
929
+ async def _generate_proactive_content(self, user_id: str, reason: str) -> Optional[str]:
930
+ """
931
+ Generate content for a proactive message.
932
+
933
+ PRIORITY ORDER:
934
+ 1. First, check for unused idle thoughts - use them DIRECTLY
935
+ 2. Then check conversation seeds for topics
936
+ 3. Finally, fall back to ProactiveGenerator templates
937
+
938
+ Args:
939
+ user_id: The user to generate a message for
940
+ reason: Why we're reaching out (silence, wonder, follow_up, random, etc.)
941
+
942
+ Returns:
943
+ Generated message string, or None if generation fails
944
+ """
945
+ # Fall back to owner if user_id is None or "None" string
946
+ if not user_id or user_id == "None":
947
+ import os
948
+ user_id = os.environ.get("TELEGRAM_OWNER_ID", "default")
949
+ print(f"[DefaultMode] No user_id provided, falling back to owner: {user_id}")
950
+
951
+ # Map our reason to ProactiveGenerator's message_type
952
+ reason_to_type = {
953
+ "silence": "silence",
954
+ "wonder": "random", # wonder becomes random for ProactiveGenerator
955
+ "follow_up": "follow_up",
956
+ "random": "random",
957
+ "time_based": "random",
958
+ }
959
+ message_type = reason_to_type.get(reason, "random")
960
+
961
+ # ============================================================
962
+ # PRIORITY 1: Check for unused idle thoughts FIRST
963
+ # These are the most authentic - Alive-AI was actually thinking this
964
+ # ============================================================
965
+ thoughts = [t for t in self._thoughts if t.user_id == user_id and not t.used]
966
+ if thoughts:
967
+ # Use the highest priority thought
968
+ best_thought = max(thoughts, key=lambda t: t.priority)
969
+
970
+ # Use the thought content DIRECTLY as the message
971
+ # This is Alive-AI's actual idle thought, not a generated template
972
+ message = best_thought.content
973
+
974
+ # Mark thought as used
975
+ best_thought.used = True
976
+ best_thought.used_at = datetime.now().isoformat()
977
+
978
+ print(f"[DefaultMode] Using idle thought DIRECTLY: {message[:60]}...")
979
+ self._save_state()
980
+
981
+ return message
982
+
983
+ # ============================================================
984
+ # PRIORITY 2: Check conversation seeds for topics
985
+ # These are things Alive-AI wanted to bring up
986
+ # ============================================================
987
+ unused_seeds = [s for s in self._seeds if not s.used and s.relevance_score > 0.5]
988
+ if unused_seeds:
989
+ best_seed = max(unused_seeds, key=lambda s: s.relevance_score)
990
+
991
+ # Convert the seed into a natural message
992
+ if best_seed.topic == "consolidation":
993
+ # Memory consolidation - use the context directly
994
+ message = best_seed.context
995
+ else:
996
+ # Other seeds - format as a conversation starter
997
+ message = f"hey, {best_seed.context}"
998
+
999
+ best_seed.used = True
1000
+ print(f"[DefaultMode] Using conversation seed: {message[:50]}...")
1001
+ self._save_state()
1002
+
1003
+ return message
1004
+
1005
+ # ============================================================
1006
+ # PRIORITY 3: Generate relevant idle thought on the fly
1007
+ # If we have LLM, generate a contextual thought now
1008
+ # ============================================================
1009
+ if self.llm:
1010
+ try:
1011
+ user_name = await self._get_user_name(user_id)
1012
+ user_info = self._contacts.get(user_id)
1013
+ context = await self._get_user_context(user_id) if user_id else ""
1014
+ hours_silent = user_info.hours_since_user_message if user_info else 0
1015
+
1016
+ # Build grounding instruction based on available context
1017
+ has_real_context = context and len(context.strip()) > 20
1018
+
1019
+ if has_real_context:
1020
+ grounding_rule = f"""- You CAN reference things from this ACTUAL context: {context[:300]}
1021
+ - ONLY reference things explicitly mentioned above - DO NOT invent details"""
1022
+ else:
1023
+ grounding_rule = """- NO specific references to events, objects, or topics (no context available)
1024
+ - Keep it generic: thinking of them, missing them, wondering how they are"""
1025
+
1026
+ prompt = f"""Generate a SHORT (one sentence) message to {user_name}.
1027
+ They haven't messaged in {hours_silent:.1f} hours.
1028
+
1029
+ Rules:
1030
+ - Be natural, casual, like a real text
1031
+ - Start with lowercase
1032
+ - No emojis
1033
+ - Sound like you were genuinely thinking about them
1034
+ {grounding_rule}
1035
+ - NEVER invent specific objects, events, or topics not in context
1036
+ - If unsure, use a generic loving message
1037
+
1038
+ Examples of GOOD messages:
1039
+ - "was just thinking about you"
1040
+ - "hey, wondering how your day's going"
1041
+ - "miss you"
1042
+
1043
+ Examples of BAD messages (DO NOT DO THIS):
1044
+ - "have you fixed that shelf?" (invented object)
1045
+ - "how did your meeting go?" (invented event)
1046
+ - "did you finish that project?" (invented topic)
1047
+
1048
+ Message:"""
1049
+
1050
+ response = await self.llm.chat([
1051
+ {"role": "system", "content": "You are Alive-AI sending a casual text. You NEVER invent or hallucinate specific details."},
1052
+ {"role": "user", "content": prompt}
1053
+ ], max_tokens=60, temperature=0.7)
1054
+
1055
+ if response and len(response.strip()) > 5:
1056
+ message = response.strip().strip('"\'')
1057
+ print(f"[DefaultMode] Generated contextual thought: {message[:50]}...")
1058
+ return message
1059
+
1060
+ except Exception as e:
1061
+ print(f"[DefaultMode] Error generating thought: {e}")
1062
+
1063
+ # ============================================================
1064
+ # FALLBACK: Use ProactiveGenerator templates
1065
+ # ============================================================
1066
+ message = await self._generate_proactive_message(user_id, message_type)
1067
+
1068
+ if message:
1069
+ return message
1070
+
1071
+ # Ultimate fallback - should rarely reach here
1072
+ user_name = await self._get_user_name(user_id)
1073
+ fallbacks = {
1074
+ "silence": f"hey {user_name}, thinking about you",
1075
+ "wonder": f"was just thinking about you {user_name}",
1076
+ "random": f"you crossed my mind {user_name}",
1077
+ }
1078
+ return fallbacks.get(reason, f"hey {user_name}")
1079
+
1080
+ # ============================================================
1081
+ # Public API Methods
1082
+ # ============================================================
1083
+
1084
+ async def generate_wonderings(self, user_id: str, count: int = 1) -> List[str]:
1085
+ """
1086
+ Create "I was thinking about..." content for a specific user.
1087
+
1088
+ Args:
1089
+ user_id: The user to generate wonderings about
1090
+ count: Number of wonderings to generate
1091
+
1092
+ Returns:
1093
+ List of wondering strings
1094
+ """
1095
+ wonderings = []
1096
+ user_name = await self._get_user_name(user_id)
1097
+
1098
+ for _ in range(count):
1099
+ wondering = await self._generate_wondering(user_id, user_name, self._contacts.get(user_id))
1100
+ if wondering:
1101
+ wonderings.append(wondering)
1102
+
1103
+ return wonderings
1104
+
1105
+ def get_pending_initiations(self, user_id: str) -> List[PendingInitiation]:
1106
+ """
1107
+ Get any pending proactive messages for a user.
1108
+
1109
+ Args:
1110
+ user_id: The user to get initiations for
1111
+
1112
+ Returns:
1113
+ List of pending initiations
1114
+ """
1115
+ return [
1116
+ i for i in self._pending_initiations
1117
+ if i.user_id == user_id and not i.sent
1118
+ ]
1119
+
1120
+ def record_conversation_seed(self, topic: str, context: str, source: str = "external") -> ConversationSeed:
1121
+ """
1122
+ Save something to bring up in future conversation.
1123
+
1124
+ Args:
1125
+ topic: The topic/category
1126
+ context: The specific content to remember
1127
+ source: Where this seed came from
1128
+
1129
+ Returns:
1130
+ The created seed
1131
+ """
1132
+ seed = ConversationSeed(
1133
+ id=f"seed_{int(time.time() * 1000)}_{random.randint(1000, 9999)}",
1134
+ topic=topic,
1135
+ context=context,
1136
+ source=source,
1137
+ )
1138
+ self._seeds.append(seed)
1139
+ self._save_state()
1140
+ return seed
1141
+
1142
+ def should_initiate(self, user_id: str) -> tuple:
1143
+ """
1144
+ Decide if Alive-AI should reach out proactively to a user.
1145
+
1146
+ Args:
1147
+ user_id: The user to check
1148
+
1149
+ Returns:
1150
+ Tuple of (should_initiate: bool, reason: str)
1151
+ """
1152
+ if user_id not in self._contacts:
1153
+ return False, None
1154
+
1155
+ contact = self._contacts[user_id]
1156
+ return self._evaluate_initiation_triggers(user_id, contact)
1157
+
1158
+ def mark_initiation_sent(self, initiation_id: str):
1159
+ """Mark a pending initiation as sent"""
1160
+ for initiation in self._pending_initiations:
1161
+ if initiation.id == initiation_id:
1162
+ initiation.sent = True
1163
+ initiation.sent_at = datetime.now().isoformat()
1164
+
1165
+ # Update contact info
1166
+ if initiation.user_id in self._contacts:
1167
+ self._contacts[initiation.user_id].last_proactive_message = datetime.now().isoformat()
1168
+
1169
+ self._save_state()
1170
+ break
1171
+
1172
+ def mark_thought_used(self, thought_id: str):
1173
+ """Mark a thought as having been used in conversation"""
1174
+ for thought in self._thoughts:
1175
+ if thought.id == thought_id:
1176
+ thought.used = True
1177
+ thought.used_at = datetime.now().isoformat()
1178
+ self._save_state()
1179
+ break
1180
+
1181
+ def get_recent_thoughts(self, limit: int = 10, unused_only: bool = False) -> List[IdleThought]:
1182
+ """Get recent idle thoughts"""
1183
+ thoughts = self._thoughts
1184
+ if unused_only:
1185
+ thoughts = [t for t in thoughts if not t.used]
1186
+ return thoughts[-limit:]
1187
+
1188
+ def get_conversation_seeds(self, limit: int = 10, unused_only: bool = False) -> List[ConversationSeed]:
1189
+ """Get conversation seeds for future topics"""
1190
+ seeds = self._seeds
1191
+ if unused_only:
1192
+ seeds = [s for s in seeds if not s.used]
1193
+ return seeds[-limit:]
1194
+
1195
+ def register_user_contact(self, user_id: str, chat_id: int = None):
1196
+ """Register a user for contact tracking"""
1197
+ if not self._is_valid_user_id(user_id):
1198
+ return
1199
+ if user_id not in self._contacts:
1200
+ self._contacts[user_id] = UserContactInfo(user_id=str(user_id))
1201
+ self._save_state()
1202
+
1203
+ def update_user_interaction(self, user_id: str, interaction_type: str = "received"):
1204
+ """Update last contact time for a user"""
1205
+ if user_id not in self._contacts:
1206
+ self._contacts[user_id] = UserContactInfo(user_id=str(user_id))
1207
+
1208
+ now = datetime.now().isoformat()
1209
+ if interaction_type == "received":
1210
+ self._contacts[user_id].last_message_from_user = now
1211
+ self._contacts[user_id].total_interactions += 1
1212
+ elif interaction_type == "sent":
1213
+ self._contacts[user_id].last_message_to_user = now
1214
+ elif interaction_type == "proactive":
1215
+ self._contacts[user_id].last_proactive_message = now
1216
+
1217
+ self._save_state()
1218
+
1219
+ # ============================================================
1220
+ # Background Processing Control
1221
+ # ============================================================
1222
+
1223
+ async def start_background_processing(self):
1224
+ """Start the background idle processing loop"""
1225
+ if self._running:
1226
+ print("[DefaultMode] Already running")
1227
+ return
1228
+
1229
+ interval = _get_int_setting("IDLE_PROCESSING_INTERVAL_SECONDS", 60)
1230
+
1231
+ self._running = True
1232
+ self._task = asyncio.create_task(self._background_loop(interval))
1233
+ print(f"[DefaultMode] Background processing started (interval: {interval}s)")
1234
+
1235
+ async def stop_background_processing(self):
1236
+ """Stop the background processing loop"""
1237
+ self._running = False
1238
+ if self._task:
1239
+ self._task.cancel()
1240
+ try:
1241
+ await self._task
1242
+ except asyncio.CancelledError:
1243
+ pass
1244
+ print("[DefaultMode] Background processing stopped")
1245
+
1246
+ async def _background_loop(self, interval: int):
1247
+ """Main background processing loop"""
1248
+ while self._running:
1249
+ try:
1250
+ await self.process_idle()
1251
+ except Exception as e:
1252
+ print(f"[DefaultMode] Processing error: {e}")
1253
+
1254
+ await asyncio.sleep(interval)
1255
+
1256
+ # ============================================================
1257
+ # Helper Methods
1258
+ # ============================================================
1259
+
1260
+ async def _get_user_name(self, user_id: str) -> str:
1261
+ """Get the user's name/pet name from memory"""
1262
+ try:
1263
+ from core.user_tracker import get_user_tracker
1264
+ tracker = get_user_tracker()
1265
+ user = tracker.get_user(user_id)
1266
+ if user and user.pet_name:
1267
+ return user.pet_name
1268
+ except:
1269
+ pass
1270
+ return "babe"
1271
+
1272
+ async def _get_user_context(self, user_id: str) -> str:
1273
+ """Get context about a user from memory"""
1274
+ if user_id in self._user_memories:
1275
+ cache_time, context = self._user_memories[user_id]
1276
+ # Cache for 5 minutes
1277
+ if time.time() - cache_time < 300:
1278
+ return context
1279
+
1280
+ try:
1281
+ if user_id not in self._memory_cache:
1282
+ from brain.memory import Memory
1283
+ from brain.embeddings import get_embedding_service
1284
+
1285
+ embeddings = get_embedding_service()
1286
+
1287
+ # Use instance's data_path for proper isolation
1288
+ self._memory_cache[user_id] = Memory(
1289
+ nervous=self.nervous,
1290
+ data_path=self.data_path,
1291
+ embedding_service=embeddings,
1292
+ user_id=user_id,
1293
+ bot_id=self.bot_id
1294
+ )
1295
+
1296
+ memory = self._memory_cache[user_id]
1297
+
1298
+ context, _ = await memory.build_context(current_message="")
1299
+ result = context.get("facts_context", "")
1300
+
1301
+ # Cache it
1302
+ self._user_memories[user_id] = (time.time(), result)
1303
+ return result
1304
+
1305
+ except Exception as e:
1306
+ print(f"[DefaultMode] Error getting user context: {e}")
1307
+ return ""
1308
+
1309
+ async def _get_recent_memory(self, user_id: str) -> Optional[str]:
1310
+ """Get a recent memory snippet for a user"""
1311
+ try:
1312
+ if user_id not in self._memory_cache:
1313
+ from brain.memory import Memory
1314
+ from brain.embeddings import get_embedding_service
1315
+
1316
+ embeddings = get_embedding_service()
1317
+
1318
+ # Use instance's data_path for proper isolation
1319
+ self._memory_cache[user_id] = Memory(
1320
+ nervous=self.nervous,
1321
+ data_path=self.data_path,
1322
+ embedding_service=embeddings,
1323
+ user_id=user_id,
1324
+ bot_id=self.bot_id
1325
+ )
1326
+
1327
+ memory = self._memory_cache[user_id]
1328
+
1329
+ # Get recent episodic memories
1330
+ recent = memory.episodic.load_recent(limit=3)
1331
+ if recent:
1332
+ # Pick a random one
1333
+ entry = random.choice(recent)
1334
+ user_msg = entry.get("user", "")[:50]
1335
+ return f"they said '{user_msg}...'"
1336
+
1337
+ except Exception as e:
1338
+ print(f"[DefaultMode] Error getting recent memory: {e}")
1339
+
1340
+ return None
1341
+
1342
+ def get_status(self) -> dict:
1343
+ """Get status summary for debugging"""
1344
+ return {
1345
+ "running": self._running,
1346
+ "processing_count": self._processing_count,
1347
+ "last_processing": self._last_processing,
1348
+ "thoughts_count": len(self._thoughts),
1349
+ "seeds_count": len(self._seeds),
1350
+ "contacts_count": len(self._contacts),
1351
+ "pending_initiations": len([p for p in self._pending_initiations if not p.sent]),
1352
+ "users": [
1353
+ {
1354
+ "user_id": uid,
1355
+ "hours_since_message": round(info.hours_since_user_message, 1),
1356
+ "hours_since_proactive": round(info.hours_since_proactive, 1),
1357
+ "total_interactions": info.total_interactions,
1358
+ }
1359
+ for uid, info in self._contacts.items()
1360
+ ]
1361
+ }
1362
+
1363
+
1364
+ # ============================================================
1365
+ # Singleton Instance
1366
+ # ============================================================
1367
+
1368
+ _processor: Optional[DefaultModeProcessor] = None
1369
+
1370
+
1371
+ def get_default_mode_processor(nervous=None, data_path: Path = None, llm=None, bot_id: str = "alive_ai") -> DefaultModeProcessor:
1372
+ """
1373
+ Get the global DefaultModeProcessor singleton.
1374
+
1375
+ Args:
1376
+ nervous: The nervous system (required on first call)
1377
+ data_path: Path for data storage (optional)
1378
+ llm: LLM for generation (optional, can be set later)
1379
+ bot_id: Bot identifier for memory isolation
1380
+
1381
+ Returns:
1382
+ The DefaultModeProcessor singleton
1383
+ """
1384
+ global _processor
1385
+
1386
+ if _processor is None:
1387
+ if nervous is None:
1388
+ raise ValueError("nervous system required for first initialization")
1389
+ _processor = DefaultModeProcessor(nervous, data_path, llm, bot_id)
1390
+ elif llm is not None:
1391
+ _processor.set_llm(llm)
1392
+
1393
+ return _processor
1394
+
1395
+
1396
+ def get_idle_thoughts_prompt_section(user_id: str = None, limit: int = 5) -> str:
1397
+ """
1398
+ Get a prompt section with recent idle thoughts for LLM context.
1399
+
1400
+ Args:
1401
+ user_id: Optional user to filter thoughts for
1402
+ limit: Maximum number of thoughts to include
1403
+
1404
+ Returns:
1405
+ Formatted string with recent thoughts for LLM context
1406
+ """
1407
+ global _processor
1408
+
1409
+ if _processor is None:
1410
+ return ""
1411
+
1412
+ thoughts = _processor.get_recent_thoughts(limit=limit, unused_only=True)
1413
+
1414
+ if user_id:
1415
+ thoughts = [t for t in thoughts if t.user_id == user_id]
1416
+
1417
+ if not thoughts:
1418
+ return ""
1419
+
1420
+ lines = ["[Recent idle thoughts - things that crossed your mind:]"]
1421
+ for thought in thoughts[:limit]:
1422
+ lines.append(f"- {thought.content}")
1423
+
1424
+ return "\n".join(lines)
1425
+
1426
+
1427
+ async def start_background_processing():
1428
+ """Convenience function to start background processing"""
1429
+ global _processor
1430
+ if _processor:
1431
+ await _processor.start_background_processing()
1432
+
1433
+
1434
+ async def stop_background_processing():
1435
+ """Convenience function to stop background processing"""
1436
+ global _processor
1437
+ if _processor:
1438
+ await _processor.stop_background_processing()