alive-ai 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/Dockerfile +24 -0
- package/LICENSE +21 -0
- package/README.md +143 -0
- package/alive_ai/__init__.py +3 -0
- package/brain/__init__.py +59 -0
- package/brain/almost_said.py +154 -0
- package/brain/bid_detector.py +636 -0
- package/brain/conversation_flow.py +135 -0
- package/brain/curiosity.py +328 -0
- package/brain/default_mode.py +1438 -0
- package/brain/dreams.py +220 -0
- package/brain/embeddings/__init__.py +82 -0
- package/brain/emotional_memory.py +949 -0
- package/brain/global_activity.py +173 -0
- package/brain/group_dynamics.py +63 -0
- package/brain/linguistic.py +235 -0
- package/brain/llm/__init__.py +63 -0
- package/brain/llm/base.py +33 -0
- package/brain/llm/fallback_router.py +309 -0
- package/brain/llm/manifest.md +30 -0
- package/brain/llm/ollama.py +218 -0
- package/brain/llm/openrouter.py +151 -0
- package/brain/llm/provider.py +205 -0
- package/brain/llm/unified.py +423 -0
- package/brain/llm/zai.py +169 -0
- package/brain/manifest.md +23 -0
- package/brain/memory/__init__.py +123 -0
- package/brain/memory/episodic.py +92 -0
- package/brain/memory/fact_extractor.py +209 -0
- package/brain/memory/index.py +54 -0
- package/brain/memory/manager.py +151 -0
- package/brain/memory/summarizer.py +102 -0
- package/brain/memory/vector_store.py +297 -0
- package/brain/memory/working.py +43 -0
- package/brain/narrative.py +343 -0
- package/brain/stt/__init__.py +4 -0
- package/brain/stt/google_stt.py +83 -0
- package/brain/stt/whisper_stt.py +82 -0
- package/brain/subconscious/__init__.py +33 -0
- package/brain/subconscious/actions.py +136 -0
- package/brain/subconscious/evaluation.py +166 -0
- package/brain/subconscious/goal_system.py +90 -0
- package/brain/subconscious/goals.py +41 -0
- package/brain/subconscious/impulse_generator.py +200 -0
- package/brain/subconscious/impulses.py +48 -0
- package/brain/subconscious/learning.py +24 -0
- package/brain/subconscious/learning_system.py +79 -0
- package/brain/subconscious/loop.py +398 -0
- package/brain/subconscious/manifest.md +32 -0
- package/brain/subconscious/relationship.py +47 -0
- package/brain/subconscious/relationship_memory.py +83 -0
- package/brain/subconscious/response_analyzer.py +74 -0
- package/brain/subconscious/templates.py +70 -0
- package/brain/subconscious/thought.py +37 -0
- package/brain/subconscious/working_memory.py +97 -0
- package/cli/index.js +371 -0
- package/config/directives.example.json +28 -0
- package/config/instructions.example.md +16 -0
- package/config/self.example.json +74 -0
- package/config/settings.example.json +95 -0
- package/core/__init__.py +1 -0
- package/core/config.py +54 -0
- package/core/directives.py +198 -0
- package/core/events.py +50 -0
- package/core/follow_up.py +267 -0
- package/core/hot_reload.py +174 -0
- package/core/initialization.py +253 -0
- package/core/manifest.md +28 -0
- package/core/media_handler.py +241 -0
- package/core/memory_monitor.py +200 -0
- package/core/message_handler.py +1440 -0
- package/core/proactive_generator.py +277 -0
- package/core/self.py +188 -0
- package/core/settings.py +169 -0
- package/core/skills_registry.py +357 -0
- package/core/state.py +27 -0
- package/core/subconscious_bridge.py +93 -0
- package/core/thinking.py +175 -0
- package/core/user_manager.py +306 -0
- package/core/user_tracker.py +144 -0
- package/demo/index.html +144 -0
- package/docker-compose.yml +28 -0
- package/docs/assets/logo.svg +15 -0
- package/docs/index.html +355 -0
- package/heart/__init__.py +93 -0
- package/heart/afterglow.py +215 -0
- package/heart/attachment.py +186 -0
- package/heart/circadian.py +251 -0
- package/heart/complex_emotions.py +114 -0
- package/heart/conflicts.py +589 -0
- package/heart/core.py +387 -0
- package/heart/emotional_decay.py +59 -0
- package/heart/emotional_memory.py +261 -0
- package/heart/emotional_state.py +146 -0
- package/heart/emotional_variability.py +156 -0
- package/heart/hormonal.py +424 -0
- package/heart/inconsistency.py +1222 -0
- package/heart/integrity.py +469 -0
- package/heart/interoception.py +997 -0
- package/heart/love.py +120 -0
- package/heart/manifest.md +25 -0
- package/heart/mood_shifts.py +169 -0
- package/heart/phantom_somatic.py +259 -0
- package/heart/predictive.py +374 -0
- package/heart/scars.py +474 -0
- package/heart/somatic.py +482 -0
- package/heart/soul.py +633 -0
- package/heart/telemetry.py +942 -0
- package/heart/triggers.py +119 -0
- package/heart/unconscious.py +443 -0
- package/input/__init__.py +1 -0
- package/input/manifest.md +24 -0
- package/input/telegram/__init__.py +1 -0
- package/input/telegram/commands.py +762 -0
- package/input/telegram/listener.py +532 -0
- package/main.py +90 -0
- package/manifest.md +28 -0
- package/mypics/.gitkeep +1 -0
- package/myvids/.gitkeep +1 -0
- package/output/__init__.py +1 -0
- package/output/images/__init__.py +1 -0
- package/output/images/fal_gen.py +43 -0
- package/output/manifest.md +26 -0
- package/output/text/__init__.py +1 -0
- package/output/text/sender.py +22 -0
- package/output/voice/__init__.py +64 -0
- package/output/voice/google_tts.py +252 -0
- package/output/voice/gtts_tts.py +214 -0
- package/output/voice/vibe_tts.py +190 -0
- package/package.json +58 -0
- package/pyproject.toml +23 -0
- package/requirements.txt +21 -0
- package/skills/__init__.py +1 -0
- package/skills/anticipation_engine/__init__.py +8 -0
- package/skills/anticipation_engine/engine.py +618 -0
- package/skills/anticipation_engine/manifest.md +192 -0
- package/skills/calendar/__init__.py +1 -0
- package/skills/content_unlocks/__init__.py +8 -0
- package/skills/content_unlocks/manifest.md +231 -0
- package/skills/content_unlocks/unlocks.py +945 -0
- package/skills/exclusive_moments/__init__.py +8 -0
- package/skills/exclusive_moments/manifest.md +145 -0
- package/skills/exclusive_moments/moments.py +506 -0
- package/skills/intimacy_layers/__init__.py +8 -0
- package/skills/intimacy_layers/layers.py +703 -0
- package/skills/intimacy_layers/manifest.md +203 -0
- package/skills/manifest.md +67 -0
- package/skills/memory_callbacks/__init__.py +9 -0
- package/skills/memory_callbacks/callbacks.py +748 -0
- package/skills/memory_callbacks/manifest.md +170 -0
- package/skills/message_scheduler/__init__.py +19 -0
- package/skills/message_scheduler/manifest.md +107 -0
- package/skills/message_scheduler/scheduler.py +510 -0
- package/skills/photo_manager/__init__.py +1 -0
- package/skills/photo_manager/scanner.py +296 -0
- package/skills/relationship_milestones/__init__.py +8 -0
- package/skills/relationship_milestones/manifest.md +206 -0
- package/skills/relationship_milestones/tracker.py +494 -0
- package/skills/self_authorship/__init__.py +23 -0
- package/skills/self_authorship/author.py +331 -0
- package/skills/self_authorship/manifest.md +24 -0
- package/skills/video_manager/__init__.py +5 -0
- package/skills/video_manager/manifest.md +37 -0
- package/skills/video_manager/scanner.py +229 -0
- package/webui/__init__.py +3 -0
- package/webui/app.py +936 -0
- package/webui/bridge.py +366 -0
- package/webui/static/index.html +2070 -0
|
@@ -0,0 +1,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"""
|