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,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
|