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,510 @@
1
+ """
2
+ Skills: Message Scheduler
3
+
4
+ Schedule Telegram messages to be sent at specific times.
5
+ Allows Alive-AI to remember to message users when they ask her to.
6
+ """
7
+
8
+ import json
9
+ import uuid
10
+ import re
11
+ from datetime import datetime, timedelta
12
+ from pathlib import Path
13
+ from typing import Dict, List, Optional, Any
14
+ from dataclasses import dataclass, field, asdict
15
+
16
+
17
+ @dataclass
18
+ class ScheduledMessage:
19
+ """A message scheduled for future delivery"""
20
+ id: str
21
+ user_id: str
22
+ message: str
23
+ scheduled_for: str # ISO format datetime
24
+ context: str = "" # Why this was scheduled
25
+ created_at: str = field(default_factory=lambda: datetime.now().isoformat())
26
+ sent: bool = False
27
+ sent_at: Optional[str] = None
28
+ cancelled: bool = False
29
+ cancelled_at: Optional[str] = None
30
+
31
+ def to_dict(self) -> dict:
32
+ return asdict(self)
33
+
34
+ @classmethod
35
+ def from_dict(cls, data: dict) -> "ScheduledMessage":
36
+ return cls(**data)
37
+
38
+ @property
39
+ def scheduled_datetime(self) -> datetime:
40
+ return datetime.fromisoformat(self.scheduled_for)
41
+
42
+ def is_due(self) -> bool:
43
+ """Check if this message should be sent now"""
44
+ if self.sent or self.cancelled:
45
+ return False
46
+ return datetime.now() >= self.scheduled_datetime
47
+
48
+
49
+ class MessageScheduler:
50
+ """
51
+ Manages scheduled messages for Alive-AI.
52
+
53
+ Allows scheduling messages for specific times and checking
54
+ when they're due to be sent.
55
+ """
56
+
57
+ # Regex patterns for natural time parsing
58
+ TIME_PATTERNS = {
59
+ # "at 15:00" or "at 3pm" or "at 3:30pm"
60
+ "specific_time": re.compile(
61
+ r'(?:at\s+)?(\d{1,2})(?::(\d{2}))?\s*(am|pm)?',
62
+ re.IGNORECASE
63
+ ),
64
+ # "in 30 minutes" or "in an hour"
65
+ "relative": re.compile(
66
+ r'in\s+(?:about\s+)?(\d+)\s*(minute|min|hour|hr)s?',
67
+ re.IGNORECASE
68
+ ),
69
+ # "in an hour" (special case)
70
+ "relative_an": re.compile(
71
+ r'in\s+an?\s+(minute|hour)',
72
+ re.IGNORECASE
73
+ ),
74
+ }
75
+
76
+ def __init__(self, nervous=None, data_path: Path = None):
77
+ """
78
+ Initialize the Message Scheduler.
79
+
80
+ Args:
81
+ nervous: The nervous system for event emission
82
+ data_path: Path for data storage (defaults to data/scheduled_messages)
83
+ """
84
+ self.nervous = nervous
85
+
86
+ # Set up data path
87
+ if data_path:
88
+ self.data_path = Path(data_path)
89
+ else:
90
+ self.data_path = Path(__file__).parent.parent.parent / "data" / "scheduled_messages"
91
+
92
+ self.data_path.mkdir(parents=True, exist_ok=True)
93
+
94
+ # File paths
95
+ self.queue_path = self.data_path / "queue.json"
96
+ self.history_path = self.data_path / "history.json"
97
+
98
+ # In-memory state
99
+ self._queue: List[ScheduledMessage] = []
100
+ self._history: List[ScheduledMessage] = []
101
+
102
+ # Load persisted state
103
+ self._load_state()
104
+
105
+ print(f"[MessageScheduler] Initialized with {len(self._queue)} pending messages")
106
+
107
+ def _load_state(self):
108
+ """Load persisted state from files"""
109
+ # Load queue
110
+ if self.queue_path.exists():
111
+ try:
112
+ data = json.loads(self.queue_path.read_text())
113
+ self._queue = [
114
+ ScheduledMessage.from_dict(m)
115
+ for m in data.get("messages", [])
116
+ if not m.get("sent") and not m.get("cancelled")
117
+ ]
118
+ except Exception as e:
119
+ print(f"[MessageScheduler] Error loading queue: {e}")
120
+
121
+ # Load history
122
+ if self.history_path.exists():
123
+ try:
124
+ data = json.loads(self.history_path.read_text())
125
+ self._history = [
126
+ ScheduledMessage.from_dict(m)
127
+ for m in data.get("messages", [])
128
+ ]
129
+ except Exception as e:
130
+ print(f"[MessageScheduler] Error loading history: {e}")
131
+
132
+ def _save_state(self):
133
+ """Save state to files"""
134
+ # Save queue
135
+ try:
136
+ data = {
137
+ "messages": [m.to_dict() for m in self._queue if not m.sent and not m.cancelled],
138
+ "updated_at": datetime.now().isoformat()
139
+ }
140
+ self.queue_path.write_text(json.dumps(data, indent=2))
141
+ except Exception as e:
142
+ print(f"[MessageScheduler] Error saving queue: {e}")
143
+
144
+ # Save history (keep last 100)
145
+ try:
146
+ data = {
147
+ "messages": [m.to_dict() for m in self._history[-100:]],
148
+ "updated_at": datetime.now().isoformat()
149
+ }
150
+ self.history_path.write_text(json.dumps(data, indent=2))
151
+ except Exception as e:
152
+ print(f"[MessageScheduler] Error saving history: {e}")
153
+
154
+ def schedule_message(
155
+ self,
156
+ user_id: str,
157
+ message: str,
158
+ scheduled_time: datetime,
159
+ context: str = ""
160
+ ) -> ScheduledMessage:
161
+ """
162
+ Schedule a message for a specific time.
163
+
164
+ Args:
165
+ user_id: Telegram user ID to send to
166
+ message: The message content
167
+ scheduled_time: When to send the message
168
+ context: Why this message was scheduled (for Alive-AI's reference)
169
+
170
+ Returns:
171
+ The created ScheduledMessage
172
+ """
173
+ msg = ScheduledMessage(
174
+ id=str(uuid.uuid4())[:8],
175
+ user_id=str(user_id),
176
+ message=message,
177
+ scheduled_for=scheduled_time.isoformat(),
178
+ context=context
179
+ )
180
+
181
+ self._queue.append(msg)
182
+ self._save_state()
183
+
184
+ print(f"[MessageScheduler] Scheduled message for {scheduled_time}: {message[:40]}...")
185
+
186
+ return msg
187
+
188
+ def schedule_in(
189
+ self,
190
+ user_id: str,
191
+ message: str,
192
+ minutes: int = 0,
193
+ hours: int = 0,
194
+ context: str = ""
195
+ ) -> ScheduledMessage:
196
+ """
197
+ Schedule a message relative to now.
198
+
199
+ Args:
200
+ user_id: Telegram user ID
201
+ message: The message content
202
+ minutes: Minutes from now
203
+ hours: Hours from now
204
+ context: Why this message was scheduled
205
+
206
+ Returns:
207
+ The created ScheduledMessage
208
+ """
209
+ delta = timedelta(hours=hours, minutes=minutes)
210
+ scheduled_time = datetime.now() + delta
211
+
212
+ return self.schedule_message(user_id, message, scheduled_time, context)
213
+
214
+ def parse_time_string(self, time_str: str, now: datetime = None) -> Optional[datetime]:
215
+ """
216
+ Parse a natural language time string into a datetime.
217
+
218
+ Supports:
219
+ - "at 15:00" / "at 3pm"
220
+ - "in 30 minutes" / "in an hour"
221
+ - "tonight at 8"
222
+
223
+ Args:
224
+ time_str: Natural language time description
225
+ now: Base time (defaults to current time)
226
+
227
+ Returns:
228
+ Parsed datetime or None if parsing fails
229
+ """
230
+ if now is None:
231
+ now = datetime.now()
232
+
233
+ time_str = time_str.lower().strip()
234
+
235
+ # Try relative time first: "in X minutes/hours"
236
+ match = self.TIME_PATTERNS["relative"].search(time_str)
237
+ if match:
238
+ amount = int(match.group(1))
239
+ unit = match.group(2).lower()
240
+
241
+ if unit in ("minute", "min"):
242
+ return now + timedelta(minutes=amount)
243
+ elif unit in ("hour", "hr"):
244
+ return now + timedelta(hours=amount)
245
+
246
+ # Try "in an hour" / "in a minute"
247
+ match = self.TIME_PATTERNS["relative_an"].search(time_str)
248
+ if match:
249
+ unit = match.group(1).lower()
250
+ if unit == "hour":
251
+ return now + timedelta(hours=1)
252
+ elif unit == "minute":
253
+ return now + timedelta(minutes=1)
254
+
255
+ # Try specific time: "at 15:00" / "at 3pm"
256
+ match = self.TIME_PATTERNS["specific_time"].search(time_str)
257
+ if match:
258
+ hour = int(match.group(1))
259
+ minute = int(match.group(2)) if match.group(2) else 0
260
+ am_pm = match.group(3)
261
+
262
+ # Handle 12-hour format
263
+ if am_pm:
264
+ am_pm = am_pm.lower()
265
+ if am_pm == "pm" and hour < 12:
266
+ hour += 12
267
+ elif am_pm == "am" and hour == 12:
268
+ hour = 0
269
+
270
+ # Create datetime for today
271
+ try:
272
+ scheduled = now.replace(hour=hour, minute=minute, second=0, microsecond=0)
273
+
274
+ # If time has passed today, assume tomorrow
275
+ if scheduled <= now:
276
+ scheduled += timedelta(days=1)
277
+
278
+ return scheduled
279
+ except ValueError:
280
+ return None
281
+
282
+ # Check for "tonight"
283
+ if "tonight" in time_str:
284
+ match = self.TIME_PATTERNS["specific_time"].search(time_str)
285
+ if match:
286
+ hour = int(match.group(1))
287
+ minute = int(match.group(2)) if match.group(2) else 0
288
+ am_pm = match.group(3)
289
+
290
+ # Force PM for "tonight"
291
+ if am_pm and am_pm.lower() == "am":
292
+ hour = hour # Keep as is if explicitly AM
293
+ elif hour < 12:
294
+ hour += 12 # Convert to PM
295
+
296
+ try:
297
+ return now.replace(hour=hour, minute=minute, second=0, microsecond=0)
298
+ except ValueError:
299
+ return None
300
+ else:
301
+ # "tonight" without time = 8pm
302
+ return now.replace(hour=20, minute=0, second=0, microsecond=0)
303
+
304
+ # Check for "tomorrow morning"
305
+ if "tomorrow" in time_str:
306
+ tomorrow = now + timedelta(days=1)
307
+
308
+ if "morning" in time_str:
309
+ return tomorrow.replace(hour=9, minute=0, second=0, microsecond=0)
310
+ elif "afternoon" in time_str:
311
+ return tomorrow.replace(hour=14, minute=0, second=0, microsecond=0)
312
+ elif "evening" in time_str or "night" in time_str:
313
+ return tomorrow.replace(hour=19, minute=0, second=0, microsecond=0)
314
+ else:
315
+ # Just "tomorrow" = same time tomorrow
316
+ return tomorrow
317
+
318
+ return None
319
+
320
+ def get_due_messages(self) -> List[ScheduledMessage]:
321
+ """
322
+ Get all messages that are due to be sent.
323
+
324
+ Returns:
325
+ List of ScheduledMessage objects ready to send
326
+ """
327
+ due = [msg for msg in self._queue if msg.is_due()]
328
+ return due
329
+
330
+ def get_pending(self, user_id: str = None) -> List[ScheduledMessage]:
331
+ """
332
+ Get all pending scheduled messages.
333
+
334
+ Args:
335
+ user_id: Optional user ID to filter by
336
+
337
+ Returns:
338
+ List of pending ScheduledMessage objects
339
+ """
340
+ pending = [msg for msg in self._queue if not msg.sent and not msg.cancelled]
341
+
342
+ if user_id:
343
+ pending = [msg for msg in pending if msg.user_id == str(user_id)]
344
+
345
+ # Sort by scheduled time
346
+ pending.sort(key=lambda m: m.scheduled_datetime)
347
+ return pending
348
+
349
+ def get_next_for_user(self, user_id: str) -> Optional[ScheduledMessage]:
350
+ """
351
+ Get the next scheduled message for a user.
352
+
353
+ Args:
354
+ user_id: The user to check
355
+
356
+ Returns:
357
+ The next ScheduledMessage or None
358
+ """
359
+ pending = self.get_pending(user_id)
360
+ return pending[0] if pending else None
361
+
362
+ def mark_sent(self, message_id: str) -> bool:
363
+ """
364
+ Mark a message as sent.
365
+
366
+ Args:
367
+ message_id: The message ID to mark
368
+
369
+ Returns:
370
+ True if found and marked, False otherwise
371
+ """
372
+ for msg in self._queue:
373
+ if msg.id == message_id:
374
+ msg.sent = True
375
+ msg.sent_at = datetime.now().isoformat()
376
+
377
+ # Move to history
378
+ self._history.append(msg)
379
+ self._queue.remove(msg)
380
+
381
+ self._save_state()
382
+ print(f"[MessageScheduler] Marked message {message_id} as sent")
383
+ return True
384
+
385
+ return False
386
+
387
+ def cancel_message(self, message_id: str) -> bool:
388
+ """
389
+ Cancel a scheduled message.
390
+
391
+ Args:
392
+ message_id: The message ID to cancel
393
+
394
+ Returns:
395
+ True if found and cancelled, False otherwise
396
+ """
397
+ for msg in self._queue:
398
+ if msg.id == message_id and not msg.sent:
399
+ msg.cancelled = True
400
+ msg.cancelled_at = datetime.now().isoformat()
401
+
402
+ # Move to history
403
+ self._history.append(msg)
404
+ self._queue.remove(msg)
405
+
406
+ self._save_state()
407
+ print(f"[MessageScheduler] Cancelled message {message_id}")
408
+ return True
409
+
410
+ return False
411
+
412
+ def cancel_all_for_user(self, user_id: str) -> int:
413
+ """
414
+ Cancel all pending messages for a user.
415
+
416
+ Args:
417
+ user_id: The user whose messages to cancel
418
+
419
+ Returns:
420
+ Number of messages cancelled
421
+ """
422
+ count = 0
423
+ for msg in list(self._queue):
424
+ if msg.user_id == str(user_id) and not msg.sent:
425
+ msg.cancelled = True
426
+ msg.cancelled_at = datetime.now().isoformat()
427
+ self._history.append(msg)
428
+ self._queue.remove(msg)
429
+ count += 1
430
+
431
+ if count > 0:
432
+ self._save_state()
433
+ print(f"[MessageScheduler] Cancelled {count} messages for user {user_id}")
434
+
435
+ return count
436
+
437
+ def get_status(self) -> Dict[str, Any]:
438
+ """
439
+ Get scheduler status for debugging.
440
+
441
+ Returns:
442
+ Dict with status information
443
+ """
444
+ return {
445
+ "pending_count": len([m for m in self._queue if not m.sent and not m.cancelled]),
446
+ "history_count": len(self._history),
447
+ "due_count": len(self.get_due_messages()),
448
+ "pending_messages": [
449
+ {
450
+ "id": m.id,
451
+ "user_id": m.user_id,
452
+ "scheduled_for": m.scheduled_for,
453
+ "message_preview": m.message[:50] + "..." if len(m.message) > 50 else m.message,
454
+ "context": m.context
455
+ }
456
+ for m in self.get_pending()
457
+ ]
458
+ }
459
+
460
+
461
+ # ============================================================
462
+ # Singleton Instance
463
+ # ============================================================
464
+
465
+ _scheduler: Optional[MessageScheduler] = None
466
+
467
+
468
+ def get_message_scheduler(nervous=None, data_path: Path = None) -> MessageScheduler:
469
+ """
470
+ Get the global MessageScheduler singleton.
471
+
472
+ Args:
473
+ nervous: The nervous system (required on first call)
474
+ data_path: Path for data storage (optional)
475
+
476
+ Returns:
477
+ The MessageScheduler singleton
478
+ """
479
+ global _scheduler
480
+
481
+ if _scheduler is None:
482
+ _scheduler = MessageScheduler(nervous, data_path)
483
+ elif nervous is not None and _scheduler.nervous is None:
484
+ _scheduler.nervous = nervous
485
+
486
+ return _scheduler
487
+
488
+
489
+ def get_scheduler_prompt_section() -> str:
490
+ """
491
+ Get a prompt section describing scheduled messages for LLM context.
492
+
493
+ Returns:
494
+ Formatted string with scheduled message info
495
+ """
496
+ global _scheduler
497
+
498
+ if _scheduler is None:
499
+ return ""
500
+
501
+ pending = _scheduler.get_pending()
502
+ if not pending:
503
+ return ""
504
+
505
+ lines = ["[Your Scheduled Messages:]"]
506
+ for msg in pending[:5]: # Show next 5
507
+ time_str = msg.scheduled_datetime.strftime("%H:%M")
508
+ lines.append(f"- At {time_str}: '{msg.message[:40]}...' (to user {msg.user_id})")
509
+
510
+ return "\n".join(lines)
@@ -0,0 +1 @@
1
+ """Photo Manager"""