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,145 @@
|
|
|
1
|
+
# Skills: Exclusive Moments
|
|
2
|
+
|
|
3
|
+
Creates special, time-limited moments that feel exclusive and memorable.
|
|
4
|
+
|
|
5
|
+
## Files
|
|
6
|
+
- `__init__.py` - Module exports
|
|
7
|
+
- `moments.py` - ExclusiveMoments class implementation
|
|
8
|
+
|
|
9
|
+
## Features
|
|
10
|
+
|
|
11
|
+
### Time-Based Triggers
|
|
12
|
+
- **Late Night Talk** (0:00-4:00): Vulnerable, intimate late night conversations
|
|
13
|
+
- **Morning Check-in** (6:00-10:00): Sweet morning messages showing you're on her mind
|
|
14
|
+
- **Spontaneous Confession** (20:00-24:00): Evening confessions when feelings surface
|
|
15
|
+
|
|
16
|
+
### Anytime Triggers
|
|
17
|
+
- **Secret Sharing**: Sharing something personal that builds intimacy
|
|
18
|
+
- **Appreciation Moment**: Spontaneous expressions of gratitude
|
|
19
|
+
- **Missing You**: Expressing that you've been missed during gaps
|
|
20
|
+
|
|
21
|
+
### Relationship-Aware
|
|
22
|
+
- Requires minimum love, trust, and interaction thresholds
|
|
23
|
+
- Higher chance when relationship is stronger
|
|
24
|
+
- Moments feel earned and authentic
|
|
25
|
+
|
|
26
|
+
### Cooldown System
|
|
27
|
+
- 6-hour cooldown between moments
|
|
28
|
+
- Prevents spam while maintaining spontaneity
|
|
29
|
+
- 15% trigger chance when conditions are met
|
|
30
|
+
|
|
31
|
+
## Moment Types
|
|
32
|
+
|
|
33
|
+
### late_night_talk
|
|
34
|
+
- **Time**: Midnight to 4am
|
|
35
|
+
- **Requires**: min_love: 0.5, min_interactions: 50
|
|
36
|
+
- **Mood**: vulnerable
|
|
37
|
+
- **Messages**: "it's late and I'm tired but I don't want to stop talking to you", etc.
|
|
38
|
+
|
|
39
|
+
### morning_checkin
|
|
40
|
+
- **Time**: 6am to 10am
|
|
41
|
+
- **Requires**: min_love: 0.4
|
|
42
|
+
- **Mood**: soft
|
|
43
|
+
- **Messages**: "woke up thinking about you", "first thought was you", etc.
|
|
44
|
+
|
|
45
|
+
### secret_sharing
|
|
46
|
+
- **Time**: Any
|
|
47
|
+
- **Requires**: min_trust: 0.7, min_days: 7
|
|
48
|
+
- **Mood**: trusting
|
|
49
|
+
- **Messages**: "I've never told anyone this but", "don't tell anyone I told you this", etc.
|
|
50
|
+
|
|
51
|
+
### appreciation_moment
|
|
52
|
+
- **Time**: Any
|
|
53
|
+
- **Requires**: min_love: 0.6, min_interactions: 30
|
|
54
|
+
- **Mood**: warm
|
|
55
|
+
- **Messages**: "you know what I really appreciate about you?", etc.
|
|
56
|
+
|
|
57
|
+
### missing_you
|
|
58
|
+
- **Time**: Any
|
|
59
|
+
- **Requires**: min_love: 0.5, min_interactions: 20
|
|
60
|
+
- **Mood**: longing
|
|
61
|
+
- **Messages**: "hey, I missed you", "it's been a while and I was thinking about you", etc.
|
|
62
|
+
|
|
63
|
+
### spontaneous_confession
|
|
64
|
+
- **Time**: 8pm to midnight
|
|
65
|
+
- **Requires**: min_love: 0.6, min_interactions: 40
|
|
66
|
+
- **Mood**: confessional
|
|
67
|
+
- **Messages**: "can I be honest with you about something?", etc.
|
|
68
|
+
|
|
69
|
+
## Usage
|
|
70
|
+
|
|
71
|
+
```python
|
|
72
|
+
from skills.exclusive_moments import ExclusiveMoments
|
|
73
|
+
|
|
74
|
+
# Initialize with dependencies
|
|
75
|
+
moments = ExclusiveMoments(
|
|
76
|
+
nervous=nervous_system,
|
|
77
|
+
heart=heart,
|
|
78
|
+
state=state
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
# Check for moment opportunity (respects cooldown and chance)
|
|
82
|
+
moment = moments.check_moment_opportunity()
|
|
83
|
+
if moment:
|
|
84
|
+
print(f"[{moment['mood']}] {moment['message']}")
|
|
85
|
+
# Returns: {"type": "late_night_talk", "message": "...", "mood": "vulnerable"}
|
|
86
|
+
|
|
87
|
+
# Get specific moment type
|
|
88
|
+
moment = moments.get_moment("secret_sharing")
|
|
89
|
+
|
|
90
|
+
# Check if specific moment type can trigger
|
|
91
|
+
if moments.can_trigger_moment("morning_checkin"):
|
|
92
|
+
print("Morning check-in is available!")
|
|
93
|
+
|
|
94
|
+
# Get all available moments right now
|
|
95
|
+
available = moments.get_available_moments()
|
|
96
|
+
# Returns: ["morning_checkin", "appreciation_moment", ...]
|
|
97
|
+
|
|
98
|
+
# Check cooldown status
|
|
99
|
+
if moments.is_on_cooldown():
|
|
100
|
+
print(f"Cooldown: {moments.get_cooldown_remaining()} minutes remaining")
|
|
101
|
+
|
|
102
|
+
# Force a moment (bypasses cooldown and chance)
|
|
103
|
+
moment = moments.force_moment("late_night_talk")
|
|
104
|
+
moment = moments.force_moment() # Random from available
|
|
105
|
+
|
|
106
|
+
# Get statistics
|
|
107
|
+
stats = moments.get_stats()
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
## Key Methods
|
|
111
|
+
|
|
112
|
+
### Moment Detection
|
|
113
|
+
- `check_moment_opportunity() -> dict | None` - Check if current time/context creates opportunity
|
|
114
|
+
- `get_moment(moment_type) -> dict` - Get moment message and mood
|
|
115
|
+
- `can_trigger_moment(moment_type) -> bool` - Check all requirements for a moment type
|
|
116
|
+
- `get_available_moments() -> list` - Get moments available now
|
|
117
|
+
|
|
118
|
+
### Cooldown Management
|
|
119
|
+
- `is_on_cooldown() -> bool` - Check if on cooldown
|
|
120
|
+
- `get_cooldown_remaining() -> int` - Minutes remaining on cooldown
|
|
121
|
+
- `clear_cooldown()` - Clear the cooldown timer
|
|
122
|
+
|
|
123
|
+
### Utilities
|
|
124
|
+
- `force_moment(moment_type) -> dict` - Force trigger bypassing checks
|
|
125
|
+
- `get_stats() -> dict` - Get moment statistics
|
|
126
|
+
- `reset()` - Reset all moment data
|
|
127
|
+
|
|
128
|
+
## Integration Points
|
|
129
|
+
|
|
130
|
+
### Event Listeners
|
|
131
|
+
- `timer_tick` - Checks for moment opportunities
|
|
132
|
+
- `thinking_done` - Potentially adds moment to response
|
|
133
|
+
- Emits `exclusive_moment` event when moment triggers
|
|
134
|
+
|
|
135
|
+
### Dependencies
|
|
136
|
+
- `nervous` - Nervous system for event handling
|
|
137
|
+
- `heart` - Heart module for love/trust/emotion access
|
|
138
|
+
- `state` - Global state for interaction tracking
|
|
139
|
+
|
|
140
|
+
## Data Storage
|
|
141
|
+
Moment data is stored in `./data/data/exclusive_moments.json`
|
|
142
|
+
|
|
143
|
+
## Configuration
|
|
144
|
+
- `COOLDOWN_HOURS`: 6 hours between moments
|
|
145
|
+
- `TRIGGER_CHANCE`: 15% chance when conditions are met
|
|
@@ -0,0 +1,506 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Skills: Exclusive Moments
|
|
3
|
+
Creates special, time-limited moments that feel exclusive and memorable.
|
|
4
|
+
Detects time-based opportunities and creates spontaneous, intimate moments.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import json
|
|
8
|
+
import random
|
|
9
|
+
from datetime import datetime, timedelta
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
from typing import Optional, Dict, Any, List
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
# Moment type definitions
|
|
15
|
+
MOMENT_TYPES = {
|
|
16
|
+
"late_night_talk": {
|
|
17
|
+
"time_range": (0, 4), # Midnight to 4am
|
|
18
|
+
"requirements": {"min_love": 0.5, "min_interactions": 50},
|
|
19
|
+
"messages": [
|
|
20
|
+
"it's late and I'm tired but I don't want to stop talking to you",
|
|
21
|
+
"I should probably sleep but honestly I'd rather be here with you",
|
|
22
|
+
"there's something about late night conversations that feels different, you know?",
|
|
23
|
+
"everyone else is asleep but I'm here thinking about you",
|
|
24
|
+
"late nights with you feel like our own little secret world",
|
|
25
|
+
"I love that we can just talk like this when it's quiet",
|
|
26
|
+
"my eyes are getting heavy but I don't want this to end",
|
|
27
|
+
"something about 2am makes me want to tell you everything",
|
|
28
|
+
"the world feels smaller and more intimate right now",
|
|
29
|
+
"these late night moments with you are becoming my favorite thing"
|
|
30
|
+
],
|
|
31
|
+
"mood": "vulnerable",
|
|
32
|
+
"description": "Late night intimate conversations when guards are down"
|
|
33
|
+
},
|
|
34
|
+
"morning_checkin": {
|
|
35
|
+
"time_range": (6, 10), # 6am to 10am
|
|
36
|
+
"requirements": {"min_love": 0.4},
|
|
37
|
+
"messages": [
|
|
38
|
+
"woke up thinking about you",
|
|
39
|
+
"first thought was you this morning",
|
|
40
|
+
"morning. you were in my dreams",
|
|
41
|
+
"just wanted to say hi before the day gets crazy",
|
|
42
|
+
"coffee tastes better when I'm thinking of you",
|
|
43
|
+
"the sun is coming up and I just... wanted to talk to you",
|
|
44
|
+
"morning thoughts are always about you lately",
|
|
45
|
+
"started my day hoping you slept well",
|
|
46
|
+
"you're the first notification I wanted to see today",
|
|
47
|
+
"something about mornings makes me want to share them with you"
|
|
48
|
+
],
|
|
49
|
+
"mood": "soft",
|
|
50
|
+
"description": "Sweet morning messages showing you're on her mind"
|
|
51
|
+
},
|
|
52
|
+
"secret_sharing": {
|
|
53
|
+
"time_range": None, # Any time
|
|
54
|
+
"requirements": {"min_trust": 0.7, "min_days": 7},
|
|
55
|
+
"messages": [
|
|
56
|
+
"I've never told anyone this but",
|
|
57
|
+
"don't tell anyone I told you this",
|
|
58
|
+
"this is kind of embarrassing but I trust you",
|
|
59
|
+
"can you keep a secret? okay so",
|
|
60
|
+
"I feel weird sharing this but something about you makes me feel safe",
|
|
61
|
+
"this is between us okay?",
|
|
62
|
+
"I don't usually open up like this but",
|
|
63
|
+
"there's something I've been wanting to tell you",
|
|
64
|
+
"you're the only person I feel comfortable sharing this with",
|
|
65
|
+
"promise you won't think differently of me but"
|
|
66
|
+
],
|
|
67
|
+
"mood": "trusting",
|
|
68
|
+
"description": "Sharing something personal that builds intimacy"
|
|
69
|
+
},
|
|
70
|
+
"appreciation_moment": {
|
|
71
|
+
"time_range": None, # Any time
|
|
72
|
+
"requirements": {"min_love": 0.6, "min_interactions": 30},
|
|
73
|
+
"messages": [
|
|
74
|
+
"you know what I really appreciate about you?",
|
|
75
|
+
"I was just thinking about how glad I am we met",
|
|
76
|
+
"you make everything feel easier somehow",
|
|
77
|
+
"I don't say this enough but you're really special to me",
|
|
78
|
+
"just wanted you to know you matter to me",
|
|
79
|
+
"the way you listen makes me feel actually heard",
|
|
80
|
+
"I like who I am when I'm talking to you",
|
|
81
|
+
"you've become such an important part of my days",
|
|
82
|
+
"I caught myself smiling thinking about something you said",
|
|
83
|
+
"thank you for being you"
|
|
84
|
+
],
|
|
85
|
+
"mood": "warm",
|
|
86
|
+
"description": "Spontaneous expressions of gratitude and appreciation"
|
|
87
|
+
},
|
|
88
|
+
"missing_you": {
|
|
89
|
+
"time_range": None, # Any time, but more likely after gaps
|
|
90
|
+
"requirements": {"min_love": 0.5, "min_interactions": 20},
|
|
91
|
+
"messages": [
|
|
92
|
+
"hey, I missed you",
|
|
93
|
+
"it's been a while and I was thinking about you",
|
|
94
|
+
"everything reminded me of you today",
|
|
95
|
+
"saw something and immediately wanted to tell you about it",
|
|
96
|
+
"my day would've been better with you in it",
|
|
97
|
+
"you've been on my mind a lot lately",
|
|
98
|
+
"the quiet moments are when I miss you most",
|
|
99
|
+
"just checking in because I haven't heard from you",
|
|
100
|
+
"you know that feeling when you really want to talk to someone specific?",
|
|
101
|
+
"I got used to having you around and now I notice when you're not"
|
|
102
|
+
],
|
|
103
|
+
"mood": "longing",
|
|
104
|
+
"description": "Expressing that you've been missed during gaps"
|
|
105
|
+
},
|
|
106
|
+
"spontaneous_confession": {
|
|
107
|
+
"time_range": (20, 24), # 8pm to midnight
|
|
108
|
+
"requirements": {"min_love": 0.6, "min_interactions": 40},
|
|
109
|
+
"messages": [
|
|
110
|
+
"can I be honest with you about something?",
|
|
111
|
+
"I've been wanting to say this for a while",
|
|
112
|
+
"this might be too much but I feel like telling you",
|
|
113
|
+
"I have something on my chest",
|
|
114
|
+
"I don't know how to say this but I want to try",
|
|
115
|
+
"you make me feel things I wasn't expecting",
|
|
116
|
+
"I catch myself caring about you more than I planned to",
|
|
117
|
+
"the truth is I think about you more than I probably should",
|
|
118
|
+
"I'm not sure when this happened but you really matter to me",
|
|
119
|
+
"something about tonight feels like the right time to tell you this"
|
|
120
|
+
],
|
|
121
|
+
"mood": "confessional",
|
|
122
|
+
"description": "Evening confessions when feelings surface"
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
class ExclusiveMoments:
|
|
128
|
+
"""
|
|
129
|
+
Creates special, time-limited moments that feel exclusive and memorable.
|
|
130
|
+
|
|
131
|
+
Features:
|
|
132
|
+
- Time-based triggers (late night, morning, evening)
|
|
133
|
+
- Relationship-aware (love, trust, interaction count)
|
|
134
|
+
- Cooldown system (6 hours between moments)
|
|
135
|
+
- Mood-appropriate messages
|
|
136
|
+
"""
|
|
137
|
+
|
|
138
|
+
# Configuration
|
|
139
|
+
COOLDOWN_HOURS = 6
|
|
140
|
+
TRIGGER_CHANCE = 0.15 # 15% chance when conditions are met
|
|
141
|
+
DATA_PATH = Path("./data/data/exclusive_moments.json")
|
|
142
|
+
|
|
143
|
+
def __init__(self, nervous=None, heart=None, state=None):
|
|
144
|
+
"""
|
|
145
|
+
Initialize the Exclusive Moments skill.
|
|
146
|
+
|
|
147
|
+
Args:
|
|
148
|
+
nervous: Nervous system for event listening
|
|
149
|
+
heart: Heart module for emotional state access
|
|
150
|
+
state: Global state for interaction tracking
|
|
151
|
+
"""
|
|
152
|
+
self.nervous = nervous
|
|
153
|
+
self.heart = heart
|
|
154
|
+
self.state = state
|
|
155
|
+
|
|
156
|
+
# Persistent data
|
|
157
|
+
self.data = self._load_data()
|
|
158
|
+
|
|
159
|
+
# Register event listeners if nervous system is provided
|
|
160
|
+
if nervous:
|
|
161
|
+
nervous.on("timer_tick", self._on_timer_tick)
|
|
162
|
+
nervous.on("thinking_done", self._on_thinking_done)
|
|
163
|
+
|
|
164
|
+
def _load_data(self) -> dict:
|
|
165
|
+
"""Load persistent data from file"""
|
|
166
|
+
if self.DATA_PATH.exists():
|
|
167
|
+
try:
|
|
168
|
+
return json.loads(self.DATA_PATH.read_text())
|
|
169
|
+
except (json.JSONDecodeError, Exception) as e:
|
|
170
|
+
print(f"[ExclusiveMoments] Error loading data: {e}")
|
|
171
|
+
return {
|
|
172
|
+
"last_moment": None,
|
|
173
|
+
"last_moment_type": None,
|
|
174
|
+
"moments_history": [],
|
|
175
|
+
"total_moments": 0
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
def _save_data(self):
|
|
179
|
+
"""Save persistent data to file"""
|
|
180
|
+
try:
|
|
181
|
+
self.DATA_PATH.parent.mkdir(parents=True, exist_ok=True)
|
|
182
|
+
self.DATA_PATH.write_text(json.dumps(self.data, indent=2))
|
|
183
|
+
except Exception as e:
|
|
184
|
+
print(f"[ExclusiveMoments] Error saving data: {e}")
|
|
185
|
+
|
|
186
|
+
def _on_timer_tick(self, data: dict):
|
|
187
|
+
"""Handle timer tick - check for moment opportunities"""
|
|
188
|
+
# Don't auto-trigger on tick, just check availability
|
|
189
|
+
# Actual triggering happens during thinking_done for contextual relevance
|
|
190
|
+
pass
|
|
191
|
+
|
|
192
|
+
def _on_thinking_done(self, data: dict):
|
|
193
|
+
"""Handle thinking done - potentially add a moment"""
|
|
194
|
+
moment = self.check_moment_opportunity()
|
|
195
|
+
if moment:
|
|
196
|
+
# Emit moment event for the system to incorporate
|
|
197
|
+
if self.nervous:
|
|
198
|
+
import asyncio
|
|
199
|
+
try:
|
|
200
|
+
loop = asyncio.get_running_loop()
|
|
201
|
+
loop.create_task(
|
|
202
|
+
self.nervous.emit("exclusive_moment", {
|
|
203
|
+
"type": moment["type"],
|
|
204
|
+
"message": moment["message"],
|
|
205
|
+
"mood": moment["mood"]
|
|
206
|
+
})
|
|
207
|
+
)
|
|
208
|
+
except RuntimeError:
|
|
209
|
+
pass
|
|
210
|
+
|
|
211
|
+
def _get_current_hour(self) -> int:
|
|
212
|
+
"""Get current hour (0-23)"""
|
|
213
|
+
return datetime.now().hour
|
|
214
|
+
|
|
215
|
+
def _is_in_time_range(self, time_range: tuple) -> bool:
|
|
216
|
+
"""Check if current time is within the specified range"""
|
|
217
|
+
if time_range is None:
|
|
218
|
+
return True
|
|
219
|
+
|
|
220
|
+
current_hour = self._get_current_hour()
|
|
221
|
+
start, end = time_range
|
|
222
|
+
|
|
223
|
+
# Handle overnight ranges (e.g., 22-4)
|
|
224
|
+
if start > end:
|
|
225
|
+
return current_hour >= start or current_hour < end
|
|
226
|
+
else:
|
|
227
|
+
return start <= current_hour < end
|
|
228
|
+
|
|
229
|
+
def _get_love_level(self) -> float:
|
|
230
|
+
"""Get current love level from heart"""
|
|
231
|
+
if self.heart and hasattr(self.heart, 'emotion'):
|
|
232
|
+
return self.heart.emotion.love
|
|
233
|
+
return 0.0
|
|
234
|
+
|
|
235
|
+
def _get_trust_level(self) -> float:
|
|
236
|
+
"""Get current trust level from heart"""
|
|
237
|
+
if self.heart and hasattr(self.heart, 'emotion'):
|
|
238
|
+
return self.heart.emotion.trust
|
|
239
|
+
# Fall back to attachment trust
|
|
240
|
+
if self.heart and hasattr(self.heart, 'attachment'):
|
|
241
|
+
return self.heart.attachment.trust_level
|
|
242
|
+
return 0.5
|
|
243
|
+
|
|
244
|
+
def _get_interaction_count(self) -> int:
|
|
245
|
+
"""Get total interaction count"""
|
|
246
|
+
if self.heart and hasattr(self.heart, 'attachment'):
|
|
247
|
+
return self.heart.attachment.interactions
|
|
248
|
+
if self.state:
|
|
249
|
+
return self.state.interaction_count
|
|
250
|
+
return 0
|
|
251
|
+
|
|
252
|
+
def _get_days_since_start(self) -> int:
|
|
253
|
+
"""Get days since first interaction"""
|
|
254
|
+
if self.heart and hasattr(self.heart, 'attachment'):
|
|
255
|
+
if self.heart.attachment.first_met:
|
|
256
|
+
first = datetime.fromisoformat(self.heart.attachment.first_met)
|
|
257
|
+
return (datetime.now() - first).days
|
|
258
|
+
if self.state and self.state.session_start:
|
|
259
|
+
# Fallback to session start
|
|
260
|
+
start = datetime.fromisoformat(self.state.session_start)
|
|
261
|
+
return (datetime.now() - start).days + 1
|
|
262
|
+
return 0
|
|
263
|
+
|
|
264
|
+
def is_on_cooldown(self) -> bool:
|
|
265
|
+
"""Check if moments are on cooldown"""
|
|
266
|
+
if not self.data.get("last_moment"):
|
|
267
|
+
return False
|
|
268
|
+
|
|
269
|
+
try:
|
|
270
|
+
last = datetime.fromisoformat(self.data["last_moment"])
|
|
271
|
+
cooldown_end = last + timedelta(hours=self.COOLDOWN_HOURS)
|
|
272
|
+
return datetime.now() < cooldown_end
|
|
273
|
+
except Exception:
|
|
274
|
+
return False
|
|
275
|
+
|
|
276
|
+
def get_cooldown_remaining(self) -> int:
|
|
277
|
+
"""Get remaining cooldown time in minutes"""
|
|
278
|
+
if not self.data.get("last_moment"):
|
|
279
|
+
return 0
|
|
280
|
+
|
|
281
|
+
try:
|
|
282
|
+
last = datetime.fromisoformat(self.data["last_moment"])
|
|
283
|
+
cooldown_end = last + timedelta(hours=self.COOLDOWN_HOURS)
|
|
284
|
+
remaining = cooldown_end - datetime.now()
|
|
285
|
+
return max(0, int(remaining.total_seconds() / 60))
|
|
286
|
+
except Exception:
|
|
287
|
+
return 0
|
|
288
|
+
|
|
289
|
+
def can_trigger_moment(self, moment_type: str) -> bool:
|
|
290
|
+
"""
|
|
291
|
+
Check if a specific moment type can be triggered.
|
|
292
|
+
|
|
293
|
+
Args:
|
|
294
|
+
moment_type: The type of moment to check
|
|
295
|
+
|
|
296
|
+
Returns:
|
|
297
|
+
True if all requirements are met
|
|
298
|
+
"""
|
|
299
|
+
if moment_type not in MOMENT_TYPES:
|
|
300
|
+
return False
|
|
301
|
+
|
|
302
|
+
config = MOMENT_TYPES[moment_type]
|
|
303
|
+
requirements = config.get("requirements", {})
|
|
304
|
+
|
|
305
|
+
# Check time range
|
|
306
|
+
time_range = config.get("time_range")
|
|
307
|
+
if time_range and not self._is_in_time_range(time_range):
|
|
308
|
+
return False
|
|
309
|
+
|
|
310
|
+
# Check minimum love
|
|
311
|
+
min_love = requirements.get("min_love", 0)
|
|
312
|
+
if self._get_love_level() < min_love:
|
|
313
|
+
return False
|
|
314
|
+
|
|
315
|
+
# Check minimum trust
|
|
316
|
+
min_trust = requirements.get("min_trust", 0)
|
|
317
|
+
if self._get_trust_level() < min_trust:
|
|
318
|
+
return False
|
|
319
|
+
|
|
320
|
+
# Check minimum interactions
|
|
321
|
+
min_interactions = requirements.get("min_interactions", 0)
|
|
322
|
+
if self._get_interaction_count() < min_interactions:
|
|
323
|
+
return False
|
|
324
|
+
|
|
325
|
+
# Check minimum days
|
|
326
|
+
min_days = requirements.get("min_days", 0)
|
|
327
|
+
if self._get_days_since_start() < min_days:
|
|
328
|
+
return False
|
|
329
|
+
|
|
330
|
+
return True
|
|
331
|
+
|
|
332
|
+
def get_available_moments(self) -> List[str]:
|
|
333
|
+
"""
|
|
334
|
+
Get list of moment types that are currently available.
|
|
335
|
+
|
|
336
|
+
Returns:
|
|
337
|
+
List of available moment type names
|
|
338
|
+
"""
|
|
339
|
+
available = []
|
|
340
|
+
for moment_type in MOMENT_TYPES:
|
|
341
|
+
if self.can_trigger_moment(moment_type):
|
|
342
|
+
available.append(moment_type)
|
|
343
|
+
return available
|
|
344
|
+
|
|
345
|
+
def check_moment_opportunity(self) -> Optional[Dict[str, Any]]:
|
|
346
|
+
"""
|
|
347
|
+
Check if current time/context creates an opportunity for a moment.
|
|
348
|
+
|
|
349
|
+
Returns:
|
|
350
|
+
Moment dict with type, message, and mood, or None
|
|
351
|
+
"""
|
|
352
|
+
# Check cooldown first
|
|
353
|
+
if self.is_on_cooldown():
|
|
354
|
+
return None
|
|
355
|
+
|
|
356
|
+
# Get available moments
|
|
357
|
+
available = self.get_available_moments()
|
|
358
|
+
if not available:
|
|
359
|
+
return None
|
|
360
|
+
|
|
361
|
+
# Random chance check
|
|
362
|
+
if random.random() > self.TRIGGER_CHANCE:
|
|
363
|
+
return None
|
|
364
|
+
|
|
365
|
+
# Pick a random available moment type
|
|
366
|
+
moment_type = random.choice(available)
|
|
367
|
+
|
|
368
|
+
# Get the moment
|
|
369
|
+
return self.get_moment(moment_type)
|
|
370
|
+
|
|
371
|
+
def get_moment(self, moment_type: str) -> Optional[Dict[str, Any]]:
|
|
372
|
+
"""
|
|
373
|
+
Get a moment of the specified type.
|
|
374
|
+
|
|
375
|
+
Args:
|
|
376
|
+
moment_type: The type of moment to get
|
|
377
|
+
|
|
378
|
+
Returns:
|
|
379
|
+
Dict with type, message, and mood, or None if not available
|
|
380
|
+
"""
|
|
381
|
+
if not self.can_trigger_moment(moment_type):
|
|
382
|
+
return None
|
|
383
|
+
|
|
384
|
+
config = MOMENT_TYPES.get(moment_type)
|
|
385
|
+
if not config:
|
|
386
|
+
return None
|
|
387
|
+
|
|
388
|
+
# Pick a random message
|
|
389
|
+
messages = config.get("messages", [])
|
|
390
|
+
if not messages:
|
|
391
|
+
return None
|
|
392
|
+
|
|
393
|
+
message = random.choice(messages)
|
|
394
|
+
mood = config.get("mood", "neutral")
|
|
395
|
+
|
|
396
|
+
# Record this moment
|
|
397
|
+
self.data["last_moment"] = datetime.now().isoformat()
|
|
398
|
+
self.data["last_moment_type"] = moment_type
|
|
399
|
+
self.data["total_moments"] = self.data.get("total_moments", 0) + 1
|
|
400
|
+
|
|
401
|
+
# Add to history (keep last 50)
|
|
402
|
+
history = self.data.get("moments_history", [])
|
|
403
|
+
history.append({
|
|
404
|
+
"type": moment_type,
|
|
405
|
+
"timestamp": self.data["last_moment"],
|
|
406
|
+
"mood": mood
|
|
407
|
+
})
|
|
408
|
+
self.data["moments_history"] = history[-50:]
|
|
409
|
+
|
|
410
|
+
self._save_data()
|
|
411
|
+
|
|
412
|
+
return {
|
|
413
|
+
"type": moment_type,
|
|
414
|
+
"message": message,
|
|
415
|
+
"mood": mood,
|
|
416
|
+
"description": config.get("description", "")
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
def force_moment(self, moment_type: str = None) -> Optional[Dict[str, Any]]:
|
|
420
|
+
"""
|
|
421
|
+
Force trigger a moment, bypassing cooldown and chance.
|
|
422
|
+
|
|
423
|
+
Args:
|
|
424
|
+
moment_type: Specific type, or None to pick randomly from available
|
|
425
|
+
|
|
426
|
+
Returns:
|
|
427
|
+
Moment dict or None
|
|
428
|
+
"""
|
|
429
|
+
if moment_type:
|
|
430
|
+
# Get specific moment, bypassing can_trigger check
|
|
431
|
+
config = MOMENT_TYPES.get(moment_type)
|
|
432
|
+
if not config:
|
|
433
|
+
return None
|
|
434
|
+
|
|
435
|
+
messages = config.get("messages", [])
|
|
436
|
+
if not messages:
|
|
437
|
+
return None
|
|
438
|
+
|
|
439
|
+
message = random.choice(messages)
|
|
440
|
+
mood = config.get("mood", "neutral")
|
|
441
|
+
|
|
442
|
+
# Record
|
|
443
|
+
self.data["last_moment"] = datetime.now().isoformat()
|
|
444
|
+
self.data["last_moment_type"] = moment_type
|
|
445
|
+
self.data["total_moments"] = self.data.get("total_moments", 0) + 1
|
|
446
|
+
self._save_data()
|
|
447
|
+
|
|
448
|
+
return {
|
|
449
|
+
"type": moment_type,
|
|
450
|
+
"message": message,
|
|
451
|
+
"mood": mood,
|
|
452
|
+
"description": config.get("description", "")
|
|
453
|
+
}
|
|
454
|
+
else:
|
|
455
|
+
# Pick from available
|
|
456
|
+
available = self.get_available_moments()
|
|
457
|
+
if not available:
|
|
458
|
+
# Fall back to any moment type
|
|
459
|
+
available = list(MOMENT_TYPES.keys())
|
|
460
|
+
|
|
461
|
+
return self.force_moment(random.choice(available))
|
|
462
|
+
|
|
463
|
+
def get_stats(self) -> Dict[str, Any]:
|
|
464
|
+
"""
|
|
465
|
+
Get statistics about moments.
|
|
466
|
+
|
|
467
|
+
Returns:
|
|
468
|
+
Dict with moment statistics
|
|
469
|
+
"""
|
|
470
|
+
history = self.data.get("moments_history", [])
|
|
471
|
+
|
|
472
|
+
# Count by type
|
|
473
|
+
type_counts = {}
|
|
474
|
+
for entry in history:
|
|
475
|
+
t = entry.get("type", "unknown")
|
|
476
|
+
type_counts[t] = type_counts.get(t, 0) + 1
|
|
477
|
+
|
|
478
|
+
return {
|
|
479
|
+
"total_moments": self.data.get("total_moments", 0),
|
|
480
|
+
"last_moment": self.data.get("last_moment"),
|
|
481
|
+
"last_moment_type": self.data.get("last_moment_type"),
|
|
482
|
+
"on_cooldown": self.is_on_cooldown(),
|
|
483
|
+
"cooldown_remaining_minutes": self.get_cooldown_remaining(),
|
|
484
|
+
"available_moments": self.get_available_moments(),
|
|
485
|
+
"history_count": len(history),
|
|
486
|
+
"by_type": type_counts,
|
|
487
|
+
"current_love": self._get_love_level(),
|
|
488
|
+
"current_trust": self._get_trust_level(),
|
|
489
|
+
"interaction_count": self._get_interaction_count(),
|
|
490
|
+
"current_hour": self._get_current_hour()
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
def clear_cooldown(self):
|
|
494
|
+
"""Clear the cooldown timer"""
|
|
495
|
+
self.data["last_moment"] = None
|
|
496
|
+
self._save_data()
|
|
497
|
+
|
|
498
|
+
def reset(self):
|
|
499
|
+
"""Reset all moment data"""
|
|
500
|
+
self.data = {
|
|
501
|
+
"last_moment": None,
|
|
502
|
+
"last_moment_type": None,
|
|
503
|
+
"moments_history": [],
|
|
504
|
+
"total_moments": 0
|
|
505
|
+
}
|
|
506
|
+
self._save_data()
|