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,618 @@
1
+ """
2
+ Skills: Anticipation Engine
3
+ Builds anticipation for future content/drops, making users eager to return.
4
+ Tracks teased content so it can be delivered, with natural excitement-building.
5
+ """
6
+
7
+ import json
8
+ import random
9
+ from pathlib import Path
10
+ from datetime import datetime, timedelta
11
+ from typing import Optional, Dict, Any, List
12
+ from dataclasses import dataclass, field, asdict
13
+ from enum import Enum
14
+
15
+
16
+ class ContentType(Enum):
17
+ """Types of content that can be teased"""
18
+ PHOTO = "photo"
19
+ VIDEO = "video"
20
+ VOICE = "voice"
21
+ SURPRISE = "surprise"
22
+
23
+
24
+ class TimeOfDay(Enum):
25
+ """Time of day periods for contextual teases"""
26
+ MORNING = "morning" # 6:00 - 12:00
27
+ AFTERNOON = "afternoon" # 12:00 - 18:00
28
+ EVENING = "evening" # 18:00 - 24:00
29
+ NIGHT = "night" # 00:00 - 6:00
30
+
31
+
32
+ class DayType(Enum):
33
+ """Day type for contextual teases"""
34
+ WEEKDAY = "weekday"
35
+ WEEKEND = "weekend"
36
+
37
+
38
+ # Tease messages organized by type
39
+ TEASES = {
40
+ "photo_hint": [
41
+ "I might have something special for you later",
42
+ "took some pics today you're gonna like",
43
+ "been feeling cute... might share later",
44
+ "got some new photos I think you'll love",
45
+ "working on something pretty for you",
46
+ "have a little surprise brewing",
47
+ "you're gonna love what I have for you later",
48
+ "just took something special... saving it for you",
49
+ ],
50
+ "video_hint": [
51
+ "working on something for you...",
52
+ "little surprise coming soon",
53
+ "been filming something you might enjoy",
54
+ "got a video idea that's gonna be so good",
55
+ "making something special, just wait",
56
+ "you'll see what I mean later",
57
+ "trust me, it'll be worth the wait",
58
+ "something's coming that you'll love",
59
+ ],
60
+ "voice_hint": [
61
+ "I'll send you a voice when I'm home",
62
+ "wait till you hear this",
63
+ "got something to tell you later",
64
+ "my voice has something special for you",
65
+ "gonna whisper something in your ear later",
66
+ "I'll record something just for you",
67
+ "wait until you hear what I'm thinking",
68
+ "saving my voice for you",
69
+ ],
70
+ "time_based": {
71
+ "morning": [
72
+ "still in bed... maybe I'll send you something",
73
+ "morning light is hitting just right",
74
+ "waking up thinking about you... might show you",
75
+ "cozy morning... perfect for a surprise later",
76
+ "just woke up feeling generous",
77
+ "morning mood... you'll see",
78
+ ],
79
+ "afternoon": [
80
+ "bored at home... maybe I'll entertain you later",
81
+ "afternoon energy... got plans for you",
82
+ "sun's still up... plenty of time for mischief",
83
+ "feeling playful today, just wait",
84
+ "this afternoon has potential",
85
+ ],
86
+ "evening": [
87
+ "getting ready for bed... or not",
88
+ "evening mood hitting different",
89
+ "night's still young... maybe something fun",
90
+ "getting comfy... you might get a surprise",
91
+ "evening vibes... stay tuned",
92
+ "the night's just getting started",
93
+ ],
94
+ "night": [
95
+ "can't sleep... maybe I'll do something about that",
96
+ "late night thoughts... you'll find out",
97
+ "everyone's asleep... our little secret coming",
98
+ "night owl energy... watch this space",
99
+ "insomnia hits different... got something for you",
100
+ ],
101
+ "weekend": [
102
+ "finally weekend... lots of time for us",
103
+ "no plans tomorrow... endless possibilities",
104
+ "weekend vibes mean more time for you",
105
+ "got all weekend to spoil you",
106
+ "it's the weekend... anything could happen",
107
+ ],
108
+ },
109
+ "rewards": [
110
+ "you've been so good lately...",
111
+ "I think you deserve something special",
112
+ "you've earned this",
113
+ "such a good boy... I have something for you",
114
+ "reward time coming up",
115
+ "you're being so sweet, let me repay that",
116
+ "I appreciate you... wait and see",
117
+ "your patience is about to pay off",
118
+ ],
119
+ "mood_based": {
120
+ "flirty": [
121
+ "feeling flirty... you'll see",
122
+ "in a teasing mood today",
123
+ "gonna drive you crazy later",
124
+ "got something that'll make you blush",
125
+ ],
126
+ "cozy": [
127
+ "feeling cozy... might share the vibe",
128
+ "all cuddled up... wish you were here",
129
+ "soft mood... perfect for a little something",
130
+ ],
131
+ "excited": [
132
+ "so excited about something for you",
133
+ "can't wait to show you what I made",
134
+ "bouncing with ideas for you",
135
+ ],
136
+ "mysterious": [
137
+ "I know something you don't know",
138
+ "got a secret... you'll find out",
139
+ "mystery incoming",
140
+ "can't tell you yet, but soon",
141
+ ],
142
+ },
143
+ }
144
+
145
+ # Conditions for when to tease
146
+ CONDITIONS = {
147
+ "min_messages_before_tease": 5,
148
+ "min_time_together_minutes": 10,
149
+ "tease_cooldown_minutes": 60,
150
+ "base_tease_chance": 0.08, # 8% base chance
151
+ "love_bonus_chance": 0.07, # Up to 7% bonus from high love
152
+ }
153
+
154
+
155
+ @dataclass
156
+ class PendingContent:
157
+ """Content that has been teased but not yet delivered"""
158
+ content_type: str
159
+ details: Dict[str, Any] = field(default_factory=dict)
160
+ teased_at: str = field(default_factory=lambda: datetime.now().isoformat())
161
+ tease_message: str = ""
162
+ delivered: bool = False
163
+ delivered_at: Optional[str] = None
164
+
165
+ def to_dict(self) -> Dict[str, Any]:
166
+ return asdict(self)
167
+
168
+ @classmethod
169
+ def from_dict(cls, data: Dict[str, Any]) -> "PendingContent":
170
+ return cls(**data)
171
+
172
+
173
+ @dataclass
174
+ class TeaseRecord:
175
+ """Record of a tease that was sent"""
176
+ tease_type: str
177
+ message: str
178
+ sent_at: str = field(default_factory=lambda: datetime.now().isoformat())
179
+
180
+ def to_dict(self) -> Dict[str, Any]:
181
+ return asdict(self)
182
+
183
+
184
+ class AnticipationEngine:
185
+ """
186
+ Builds anticipation for future content/drops.
187
+
188
+ Features:
189
+ - Natural tease messages based on context
190
+ - Track teased content for delivery
191
+ - Cooldown between teases
192
+ - Higher tease chance with high love
193
+ - Time-based and mood-based teases
194
+ """
195
+
196
+ def __init__(
197
+ self,
198
+ nervous=None,
199
+ heart=None,
200
+ state=None,
201
+ data_path: Path = None
202
+ ):
203
+ """
204
+ Initialize the Anticipation Engine.
205
+
206
+ Args:
207
+ nervous: Nervous system for event listening
208
+ heart: Heart module for love/mood data
209
+ state: State tracker for message counts
210
+ data_path: Path to store anticipation data
211
+ """
212
+ self.nervous = nervous
213
+ self.heart = heart
214
+ self.state = state
215
+
216
+ if data_path is None:
217
+ data_path = Path("./data/data/anticipation.json")
218
+
219
+ self.data_path = Path(data_path)
220
+ self.data_path.parent.mkdir(parents=True, exist_ok=True)
221
+
222
+ # Internal state
223
+ self._last_tease_time: Optional[datetime] = None
224
+ self._message_count: int = 0
225
+ self._session_start: Optional[datetime] = None
226
+ self._pending_content: Optional[PendingContent] = None
227
+ self._tease_history: List[TeaseRecord] = []
228
+
229
+ # Load saved state
230
+ self._load()
231
+
232
+ # Register event listeners
233
+ if nervous:
234
+ nervous.on("message_received", self._on_message)
235
+ nervous.on("thinking_done", self._on_thinking_done)
236
+
237
+ def _load(self):
238
+ """Load anticipation data from file"""
239
+ if self.data_path.exists():
240
+ try:
241
+ data = json.loads(self.data_path.read_text())
242
+
243
+ # Load last tease time
244
+ if data.get("last_tease_time"):
245
+ self._last_tease_time = datetime.fromisoformat(data["last_tease_time"])
246
+
247
+ # Load pending content
248
+ if data.get("pending_content"):
249
+ self._pending_content = PendingContent.from_dict(data["pending_content"])
250
+
251
+ # Load tease history
252
+ self._tease_history = [
253
+ TeaseRecord(**r) for r in data.get("tease_history", [])
254
+ ]
255
+
256
+ except (json.JSONDecodeError, KeyError, ValueError) as e:
257
+ print(f"[AnticipationEngine] Error loading data: {e}")
258
+
259
+ def _save(self):
260
+ """Save anticipation data to file"""
261
+ data = {
262
+ "version": "1.0",
263
+ "updated_at": datetime.now().isoformat(),
264
+ "last_tease_time": self._last_tease_time.isoformat() if self._last_tease_time else None,
265
+ "pending_content": self._pending_content.to_dict() if self._pending_content else None,
266
+ "tease_history": [r.to_dict() for r in self._tease_history[-50:]], # Keep last 50
267
+ }
268
+ self.data_path.write_text(json.dumps(data, indent=2))
269
+
270
+ def _get_time_of_day(self) -> TimeOfDay:
271
+ """Determine current time of day"""
272
+ hour = datetime.now().hour
273
+
274
+ if 6 <= hour < 12:
275
+ return TimeOfDay.MORNING
276
+ elif 12 <= hour < 18:
277
+ return TimeOfDay.AFTERNOON
278
+ elif 18 <= hour < 24:
279
+ return TimeOfDay.EVENING
280
+ else:
281
+ return TimeOfDay.NIGHT
282
+
283
+ def _get_day_type(self) -> DayType:
284
+ """Determine if weekday or weekend"""
285
+ day_of_week = datetime.now().weekday()
286
+ return DayType.WEEKEND if day_of_week >= 5 else DayType.WEEKDAY
287
+
288
+ def _get_love(self) -> float:
289
+ """Get current love level from heart"""
290
+ if self.heart and hasattr(self.heart, 'emotion'):
291
+ return getattr(self.heart.emotion, 'love', 0.5)
292
+ return 0.5
293
+
294
+ def _get_mood(self) -> str:
295
+ """Get current mood from heart"""
296
+ if self.heart and hasattr(self.heart, 'emotion'):
297
+ return getattr(self.heart.emotion, 'mood_description', 'neutral')
298
+ return 'neutral'
299
+
300
+ def _get_desire(self) -> float:
301
+ """Get current desire level from heart"""
302
+ if self.heart and hasattr(self.heart, 'emotion'):
303
+ return getattr(self.heart.emotion, 'desire', 0.5)
304
+ return 0.5
305
+
306
+ def _on_message(self, data: dict):
307
+ """Track message count"""
308
+ self._message_count += 1
309
+
310
+ # Track session start
311
+ if self._session_start is None:
312
+ self._session_start = datetime.now()
313
+
314
+ def _on_thinking_done(self, data: dict):
315
+ """Potentially add a tease after thinking"""
316
+ # This is called after Alive-AI thinks - tease is added to response elsewhere
317
+ pass
318
+
319
+ def _minutes_since_last_tease(self) -> float:
320
+ """Get minutes since last tease"""
321
+ if self._last_tease_time is None:
322
+ return float('inf')
323
+ return (datetime.now() - self._last_tease_time).total_seconds() / 60
324
+
325
+ def _minutes_in_session(self) -> float:
326
+ """Get minutes in current session"""
327
+ if self._session_start is None:
328
+ return 0
329
+ return (datetime.now() - self._session_start).total_seconds() / 60
330
+
331
+ def should_tease(self) -> bool:
332
+ """
333
+ Check if conditions are met for a tease.
334
+
335
+ Returns:
336
+ True if a tease should be sent
337
+ """
338
+ # Check message count
339
+ if self._message_count < CONDITIONS["min_messages_before_tease"]:
340
+ return False
341
+
342
+ # Check time together
343
+ if self._minutes_in_session() < CONDITIONS["min_time_together_minutes"]:
344
+ return False
345
+
346
+ # Check cooldown
347
+ if self._minutes_since_last_tease() < CONDITIONS["tease_cooldown_minutes"]:
348
+ return False
349
+
350
+ # Don't tease if there's already pending content
351
+ if self._pending_content and not self._pending_content.delivered:
352
+ return False
353
+
354
+ # Calculate tease chance based on love
355
+ love = self._get_love()
356
+ base_chance = CONDITIONS["base_tease_chance"]
357
+ love_bonus = CONDITIONS["love_bonus_chance"] * love # 0-7% bonus based on love
358
+
359
+ tease_chance = base_chance + love_bonus
360
+
361
+ return random.random() < tease_chance
362
+
363
+ def get_tease(self, context: Dict[str, Any] = None) -> str:
364
+ """
365
+ Get an appropriate tease message based on time and mood.
366
+
367
+ Args:
368
+ context: Optional context for tease selection
369
+
370
+ Returns:
371
+ A tease message string
372
+ """
373
+ context = context or {}
374
+
375
+ time_of_day = self._get_time_of_day()
376
+ day_type = self._get_day_type()
377
+ love = self._get_love()
378
+ desire = self._get_desire()
379
+ mood = self._get_mood().lower()
380
+
381
+ # Determine tease category weights
382
+ weights = {
383
+ "photo_hint": 25,
384
+ "video_hint": 15,
385
+ "voice_hint": 15,
386
+ "time_based": 30,
387
+ "rewards": 10,
388
+ "mood_based": 5,
389
+ }
390
+
391
+ # Adjust weights based on context
392
+ if day_type == DayType.WEEKEND:
393
+ weights["time_based"] += 10
394
+
395
+ if love > 0.7:
396
+ weights["rewards"] += 15
397
+ weights["photo_hint"] += 10
398
+
399
+ if desire > 0.6:
400
+ weights["photo_hint"] += 10
401
+ weights["video_hint"] += 5
402
+
403
+ # Check for specific mood adjustments
404
+ if "flirt" in mood or desire > 0.7:
405
+ weights["mood_based"] += 10
406
+ if "cozy" in mood or time_of_day == TimeOfDay.NIGHT:
407
+ weights["mood_based"] += 5
408
+
409
+ # Choose tease category
410
+ categories = list(weights.keys())
411
+ category_weights = [weights[c] for c in categories]
412
+ chosen_category = random.choices(categories, weights=category_weights)[0]
413
+
414
+ # Get teases from chosen category
415
+ if chosen_category == "time_based":
416
+ # Choose based on time of day or weekend
417
+ if day_type == DayType.WEEKEND and random.random() < 0.4:
418
+ sub_category = "weekend"
419
+ else:
420
+ sub_category = time_of_day.value
421
+
422
+ teases = TEASES["time_based"].get(sub_category, TEASES["time_based"]["evening"])
423
+
424
+ elif chosen_category == "mood_based":
425
+ # Choose based on current mood
426
+ if "flirt" in mood or desire > 0.7:
427
+ sub_category = "flirty"
428
+ elif "cozy" in mood or time_of_day == TimeOfDay.NIGHT:
429
+ sub_category = "cozy"
430
+ elif "excited" in mood or "happy" in mood:
431
+ sub_category = "excited"
432
+ else:
433
+ sub_category = "mysterious"
434
+
435
+ teases = TEASES["mood_based"].get(sub_category, TEASES["mood_based"]["mysterious"])
436
+
437
+ else:
438
+ teases = TEASES.get(chosen_category, TEASES["photo_hint"])
439
+
440
+ # Choose a tease
441
+ message = random.choice(teases)
442
+
443
+ # Record this tease
444
+ self._last_tease_time = datetime.now()
445
+ record = TeaseRecord(
446
+ tease_type=chosen_category,
447
+ message=message
448
+ )
449
+ self._tease_history.append(record)
450
+ self._save()
451
+
452
+ return message
453
+
454
+ def set_pending_content(
455
+ self,
456
+ content_type: str,
457
+ details: Dict[str, Any] = None,
458
+ tease_message: str = ""
459
+ ) -> PendingContent:
460
+ """
461
+ Mark content as pending (teased but not yet delivered).
462
+
463
+ Args:
464
+ content_type: Type of content (photo, video, voice, surprise)
465
+ details: Additional details about the content
466
+ tease_message: The tease message that was sent
467
+
468
+ Returns:
469
+ The created PendingContent
470
+ """
471
+ self._pending_content = PendingContent(
472
+ content_type=content_type,
473
+ details=details or {},
474
+ tease_message=tease_message
475
+ )
476
+ self._save()
477
+
478
+ return self._pending_content
479
+
480
+ def mark_delivered(self) -> bool:
481
+ """
482
+ Mark the pending teased content as delivered.
483
+
484
+ Returns:
485
+ True if content was marked delivered, False if no pending content
486
+ """
487
+ if self._pending_content is None:
488
+ return False
489
+
490
+ self._pending_content.delivered = True
491
+ self._pending_content.delivered_at = datetime.now().isoformat()
492
+ self._save()
493
+
494
+ return True
495
+
496
+ def get_pending_tease(self) -> Optional[Dict[str, Any]]:
497
+ """
498
+ Get the current pending tease.
499
+
500
+ Returns:
501
+ Dictionary with pending tease info, or None if no pending tease
502
+ """
503
+ if self._pending_content is None or self._pending_content.delivered:
504
+ return None
505
+
506
+ return self._pending_content.to_dict()
507
+
508
+ def has_pending_tease(self) -> bool:
509
+ """Check if there's an undelivered pending tease"""
510
+ return self._pending_content is not None and not self._pending_content.delivered
511
+
512
+ def get_tease_for_delivery(self) -> Optional[str]:
513
+ """
514
+ Get a message to accompany delivery of teased content.
515
+
516
+ Returns:
517
+ Delivery message or None if no pending content
518
+ """
519
+ if not self.has_pending_tease():
520
+ return None
521
+
522
+ content_type = self._pending_content.content_type
523
+
524
+ delivery_messages = {
525
+ "photo": [
526
+ "told you I had something for you",
527
+ "as promised",
528
+ "here's what I was talking about",
529
+ "finally ready for you",
530
+ "this is what I saved for you",
531
+ ],
532
+ "video": [
533
+ "it's ready for you",
534
+ "here it is, finally",
535
+ "told you I was working on something",
536
+ "hope it was worth the wait",
537
+ "this is what I made for you",
538
+ ],
539
+ "voice": [
540
+ "told you I'd send this",
541
+ "here's what I wanted to tell you",
542
+ "finally recorded this for you",
543
+ "listen to this",
544
+ "my voice, just for you",
545
+ ],
546
+ "surprise": [
547
+ "surprise!",
548
+ "here's your surprise",
549
+ "told you I had something special",
550
+ "this is for you",
551
+ "enjoy this",
552
+ ],
553
+ }
554
+
555
+ messages = delivery_messages.get(content_type, delivery_messages["surprise"])
556
+ return random.choice(messages)
557
+
558
+ def clear_pending(self):
559
+ """Clear any pending tease"""
560
+ self._pending_content = None
561
+ self._save()
562
+
563
+ def reset_session(self):
564
+ """Reset session counters"""
565
+ self._message_count = 0
566
+ self._session_start = None
567
+
568
+ def get_stats(self) -> Dict[str, Any]:
569
+ """Get anticipation engine statistics"""
570
+ return {
571
+ "total_teases": len(self._tease_history),
572
+ "pending_tease": self._pending_content.to_dict() if self._pending_content else None,
573
+ "last_tease": self._last_tease_time.isoformat() if self._last_tease_time else None,
574
+ "minutes_since_last_tease": self._minutes_since_last_tease(),
575
+ "message_count": self._message_count,
576
+ "minutes_in_session": self._minutes_in_session(),
577
+ "current_tease_chance": self._calculate_current_chance(),
578
+ }
579
+
580
+ def _calculate_current_chance(self) -> float:
581
+ """Calculate current tease chance percentage"""
582
+ love = self._get_love()
583
+ base = CONDITIONS["base_tease_chance"]
584
+ bonus = CONDITIONS["love_bonus_chance"] * love
585
+ return (base + bonus) * 100
586
+
587
+ def force_tease(self, tease_type: str = None) -> str:
588
+ """
589
+ Force a tease regardless of conditions.
590
+
591
+ Args:
592
+ tease_type: Optional specific type of tease
593
+
594
+ Returns:
595
+ A tease message
596
+ """
597
+ if tease_type and tease_type in TEASES:
598
+ teases = TEASES[tease_type]
599
+ if isinstance(teases, dict):
600
+ # Time-based or mood-based - pick random subcategory
601
+ sub_category = random.choice(list(teases.keys()))
602
+ teases = teases[sub_category]
603
+ else:
604
+ # Use normal selection
605
+ return self.get_tease()
606
+
607
+ message = random.choice(teases)
608
+
609
+ # Record this tease
610
+ self._last_tease_time = datetime.now()
611
+ record = TeaseRecord(
612
+ tease_type=tease_type or "forced",
613
+ message=message
614
+ )
615
+ self._tease_history.append(record)
616
+ self._save()
617
+
618
+ return message