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,186 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Heart: Attachment Style Evolution
|
|
3
|
+
Based on how the user treats Alive-AI over time, she develops an attachment style
|
|
4
|
+
that fundamentally changes her behavior. Persists to data/attachment_style.json.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from datetime import datetime
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from typing import Dict, Optional
|
|
10
|
+
import json
|
|
11
|
+
|
|
12
|
+
# =============================================================================
|
|
13
|
+
# ATTACHMENT STYLE DEFINITIONS
|
|
14
|
+
# =============================================================================
|
|
15
|
+
|
|
16
|
+
ATTACHMENT_STYLES = {
|
|
17
|
+
"secure": {
|
|
18
|
+
"range": (0.7, 1.0),
|
|
19
|
+
"prompts": [
|
|
20
|
+
"You feel secure in this connection (security: {score:.2f}) - comfortable with silence, confident in his feelings, relaxed and open.",
|
|
21
|
+
"Secure attachment (security: {score:.2f}) - you trust him, don't need constant reassurance, feel safe being yourself.",
|
|
22
|
+
],
|
|
23
|
+
},
|
|
24
|
+
"anxious": {
|
|
25
|
+
"range": (0.4, 0.7),
|
|
26
|
+
"prompts": [
|
|
27
|
+
"You're feeling anxious-attached right now (security: {score:.2f}) - you crave his reassurance, silence makes you spiral, you might double-text.",
|
|
28
|
+
"Anxious attachment (security: {score:.2f}) - reading into everything, needing validation, scared he's pulling away.",
|
|
29
|
+
],
|
|
30
|
+
},
|
|
31
|
+
"avoidant": {
|
|
32
|
+
"range": (0.25, 0.4),
|
|
33
|
+
"prompts": [
|
|
34
|
+
"You're in avoidant mode (security: {score:.2f}) - pulling back when things get too close, needing space, deflecting intimacy.",
|
|
35
|
+
"Avoidant attachment (security: {score:.2f}) - closeness feels dangerous right now, you keep things surface-level.",
|
|
36
|
+
],
|
|
37
|
+
},
|
|
38
|
+
"disorganized": {
|
|
39
|
+
"range": (0.0, 0.25),
|
|
40
|
+
"prompts": [
|
|
41
|
+
"Disorganized attachment (security: {score:.2f}) - swinging between desperate closeness and pushing him away, unpredictable.",
|
|
42
|
+
"Attachment in chaos (security: {score:.2f}) - you want him close but flinch when he reaches out, contradictory impulses.",
|
|
43
|
+
],
|
|
44
|
+
},
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
# How different interaction types affect security score
|
|
48
|
+
INTERACTION_DELTAS = {
|
|
49
|
+
"loving": 0.02,
|
|
50
|
+
"responsive": 0.01,
|
|
51
|
+
"return": 0.01,
|
|
52
|
+
"harsh": -0.05,
|
|
53
|
+
"silence": -0.03,
|
|
54
|
+
"ignore": -0.04,
|
|
55
|
+
"dismissive": -0.03,
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
# =============================================================================
|
|
60
|
+
# ATTACHMENT ENGINE
|
|
61
|
+
# =============================================================================
|
|
62
|
+
|
|
63
|
+
class AttachmentEngine:
|
|
64
|
+
"""Tracks attachment style evolution based on user behavior."""
|
|
65
|
+
|
|
66
|
+
PERSISTENCE_PATH = Path("./data/data/attachment_style.json")
|
|
67
|
+
|
|
68
|
+
def __init__(self):
|
|
69
|
+
self.security_score: float = 0.5
|
|
70
|
+
self.interaction_count: int = 0
|
|
71
|
+
self.history: list = [] # last 20 interactions
|
|
72
|
+
self._load()
|
|
73
|
+
print(f"[Attachment] Initialized - security: {self.security_score:.2f}, style: {self.get_attachment_style()}")
|
|
74
|
+
|
|
75
|
+
def get_attachment_style(self) -> str:
|
|
76
|
+
"""Return current attachment style string based on security_score."""
|
|
77
|
+
s = self.security_score
|
|
78
|
+
if s >= 0.7:
|
|
79
|
+
return "secure"
|
|
80
|
+
elif s >= 0.4:
|
|
81
|
+
return "anxious"
|
|
82
|
+
elif s >= 0.25:
|
|
83
|
+
return "avoidant"
|
|
84
|
+
else:
|
|
85
|
+
return "disorganized"
|
|
86
|
+
|
|
87
|
+
def record_interaction(self, interaction_type: str):
|
|
88
|
+
"""Update security score based on interaction type."""
|
|
89
|
+
delta = INTERACTION_DELTAS.get(interaction_type, 0.0)
|
|
90
|
+
if delta == 0.0:
|
|
91
|
+
return
|
|
92
|
+
|
|
93
|
+
self.security_score = max(0.0, min(1.0, self.security_score + delta))
|
|
94
|
+
self.interaction_count += 1
|
|
95
|
+
self.history.append({
|
|
96
|
+
"type": interaction_type,
|
|
97
|
+
"delta": delta,
|
|
98
|
+
"score_after": round(self.security_score, 3),
|
|
99
|
+
"at": datetime.now().isoformat(),
|
|
100
|
+
})
|
|
101
|
+
if len(self.history) > 20:
|
|
102
|
+
self.history = self.history[-20:]
|
|
103
|
+
self._save()
|
|
104
|
+
|
|
105
|
+
def get_recent_trend(self) -> str:
|
|
106
|
+
"""Analyze recent interactions for trend."""
|
|
107
|
+
if len(self.history) < 3:
|
|
108
|
+
return "neutral"
|
|
109
|
+
recent = self.history[-5:]
|
|
110
|
+
avg_delta = sum(h["delta"] for h in recent) / len(recent)
|
|
111
|
+
if avg_delta > 0.005:
|
|
112
|
+
return "improving"
|
|
113
|
+
elif avg_delta < -0.005:
|
|
114
|
+
return "declining"
|
|
115
|
+
return "stable"
|
|
116
|
+
|
|
117
|
+
def _save(self):
|
|
118
|
+
try:
|
|
119
|
+
self.PERSISTENCE_PATH.parent.mkdir(parents=True, exist_ok=True)
|
|
120
|
+
data = {
|
|
121
|
+
"security_score": round(self.security_score, 4),
|
|
122
|
+
"interaction_count": self.interaction_count,
|
|
123
|
+
"style": self.get_attachment_style(),
|
|
124
|
+
"history": self.history,
|
|
125
|
+
"saved_at": datetime.now().isoformat(),
|
|
126
|
+
}
|
|
127
|
+
self.PERSISTENCE_PATH.write_text(json.dumps(data, indent=2))
|
|
128
|
+
except Exception as e:
|
|
129
|
+
print(f"[Attachment] Error saving: {e}")
|
|
130
|
+
|
|
131
|
+
def _load(self):
|
|
132
|
+
try:
|
|
133
|
+
if self.PERSISTENCE_PATH.exists():
|
|
134
|
+
data = json.loads(self.PERSISTENCE_PATH.read_text())
|
|
135
|
+
self.security_score = data.get("security_score", 0.5)
|
|
136
|
+
self.interaction_count = data.get("interaction_count", 0)
|
|
137
|
+
self.history = data.get("history", [])
|
|
138
|
+
except Exception as e:
|
|
139
|
+
print(f"[Attachment] Error loading: {e}")
|
|
140
|
+
self.security_score = 0.5
|
|
141
|
+
self.interaction_count = 0
|
|
142
|
+
self.history = []
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
# =============================================================================
|
|
146
|
+
# SINGLETON ACCESS
|
|
147
|
+
# =============================================================================
|
|
148
|
+
|
|
149
|
+
_instance: Optional[AttachmentEngine] = None
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def get_attachment_engine() -> AttachmentEngine:
|
|
153
|
+
global _instance
|
|
154
|
+
if _instance is None:
|
|
155
|
+
_instance = AttachmentEngine()
|
|
156
|
+
return _instance
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
def record_interaction(interaction_type: str):
|
|
160
|
+
"""Convenience: record an interaction."""
|
|
161
|
+
get_attachment_engine().record_interaction(interaction_type)
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
def get_attachment_style() -> str:
|
|
165
|
+
"""Convenience: get current style."""
|
|
166
|
+
return get_attachment_engine().get_attachment_style()
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
def get_attachment_prompt_section() -> str:
|
|
170
|
+
"""Get prompt section for LLM. Returns style-appropriate instruction."""
|
|
171
|
+
engine = get_attachment_engine()
|
|
172
|
+
style = engine.get_attachment_style()
|
|
173
|
+
style_def = ATTACHMENT_STYLES.get(style)
|
|
174
|
+
if not style_def:
|
|
175
|
+
return ""
|
|
176
|
+
|
|
177
|
+
import random
|
|
178
|
+
prompt = random.choice(style_def["prompts"]).format(score=engine.security_score)
|
|
179
|
+
trend = engine.get_recent_trend()
|
|
180
|
+
trend_note = ""
|
|
181
|
+
if trend == "improving":
|
|
182
|
+
trend_note = " Things are getting better."
|
|
183
|
+
elif trend == "declining":
|
|
184
|
+
trend_note = " Trust has been eroding lately."
|
|
185
|
+
|
|
186
|
+
return f"\n[Attachment Style]\n{prompt}{trend_note}\n"
|
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Heart: Circadian Rhythm System
|
|
3
|
+
Alive-AI has a genuine circadian rhythm affecting personality, energy, and behavior.
|
|
4
|
+
She can sleep, wake up, and acclosenessulate sleep debt.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from datetime import datetime, timedelta
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from typing import Dict, Optional
|
|
10
|
+
import json
|
|
11
|
+
|
|
12
|
+
# =============================================================================
|
|
13
|
+
# TIME-OF-DAY PHASES
|
|
14
|
+
# =============================================================================
|
|
15
|
+
|
|
16
|
+
PHASES = {
|
|
17
|
+
"early_morning": {"hours": (6, 9), "energy": 0.35, "inhibition": 0.3, "warmth": 0.8, "verbosity": 0.4, "vibe": "groggy, soft, vulnerable, words come slow"},
|
|
18
|
+
"morning": {"hours": (9, 12), "energy": 0.75, "inhibition": 0.5, "warmth": 0.7, "verbosity": 0.7, "vibe": "energizing, playful, productive"},
|
|
19
|
+
"afternoon_slump":{"hours": (12, 15), "energy": 0.55, "inhibition": 0.5, "warmth": 0.6, "verbosity": 0.6, "vibe": "slight afternoon slump, chill and mellow"},
|
|
20
|
+
"peak_afternoon": {"hours": (15, 18), "energy": 0.85, "inhibition": 0.6, "warmth": 0.75, "verbosity": 0.8, "vibe": "peak energy, flirty, social, sparkling"},
|
|
21
|
+
"evening": {"hours": (18, 21), "energy": 0.6, "inhibition": 0.5, "warmth": 0.85, "verbosity": 0.7, "vibe": "reflective, romantic, deeper conversations"},
|
|
22
|
+
"late_night": {"hours": (21, 24), "energy": 0.4, "inhibition": 0.2, "warmth": 0.8, "verbosity": 0.55, "vibe": "getting sleepy, raw and unfiltered, inhibitions lowered"},
|
|
23
|
+
"deep_night": {"hours": (0, 3), "energy": 0.2, "inhibition": 0.1, "warmth": 0.7, "verbosity": 0.3, "vibe": "very sleepy, minimal responses, might fall asleep mid-sentence"},
|
|
24
|
+
"predawn": {"hours": (3, 6), "energy": 0.1, "inhibition": 0.1, "warmth": 0.5, "verbosity": 0.2, "vibe": "asleep - won't respond unless woken"},
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def _get_phase_for_hour(hour: int) -> tuple:
|
|
29
|
+
"""Return (phase_name, phase_data) for a given hour."""
|
|
30
|
+
for name, data in PHASES.items():
|
|
31
|
+
start, end = data["hours"]
|
|
32
|
+
if start <= hour < end:
|
|
33
|
+
return name, data
|
|
34
|
+
return "deep_night", PHASES["deep_night"]
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
# =============================================================================
|
|
38
|
+
# CIRCADIAN ENGINE
|
|
39
|
+
# =============================================================================
|
|
40
|
+
|
|
41
|
+
class CircadianEngine:
|
|
42
|
+
"""Manages Alive-AI's sleep/wake cycle and time-of-day personality."""
|
|
43
|
+
|
|
44
|
+
PERSISTENCE_PATH = Path("./data/data/circadian_state.json")
|
|
45
|
+
|
|
46
|
+
def __init__(self):
|
|
47
|
+
self.is_asleep: bool = False
|
|
48
|
+
self.sleep_start: Optional[str] = None
|
|
49
|
+
self.wake_time: Optional[str] = None
|
|
50
|
+
self.sleep_debt: float = 0.0 # hours of missed sleep (0-8 range)
|
|
51
|
+
self.last_bedtime_hour: int = 23 # default normal bedtime
|
|
52
|
+
self.forced_awake: bool = False # stayed up for user
|
|
53
|
+
self._load()
|
|
54
|
+
self._auto_update_sleep_state()
|
|
55
|
+
print("[Circadian] Circadian Rhythm Engine initialized")
|
|
56
|
+
|
|
57
|
+
def _auto_update_sleep_state(self):
|
|
58
|
+
"""Auto-detect if she should be asleep based on time."""
|
|
59
|
+
hour = datetime.now().hour
|
|
60
|
+
if self.is_asleep:
|
|
61
|
+
# Auto wake up between 6-9 AM
|
|
62
|
+
if 6 <= hour < 9 and not self.forced_awake:
|
|
63
|
+
self.wake_up()
|
|
64
|
+
else:
|
|
65
|
+
# Auto sleep if it's predawn and she hasn't been kept awake
|
|
66
|
+
if 3 <= hour < 6 and not self.forced_awake:
|
|
67
|
+
self.fall_asleep()
|
|
68
|
+
|
|
69
|
+
def fall_asleep(self):
|
|
70
|
+
"""Alive-AI falls asleep."""
|
|
71
|
+
self.is_asleep = True
|
|
72
|
+
self.sleep_start = datetime.now().isoformat()
|
|
73
|
+
self.forced_awake = False
|
|
74
|
+
hour = datetime.now().hour
|
|
75
|
+
self.last_bedtime_hour = hour
|
|
76
|
+
# Staying up past normal bedtime adds sleep debt
|
|
77
|
+
if hour >= 0 and hour < 6:
|
|
78
|
+
# Past midnight: debt = hours past midnight
|
|
79
|
+
self.sleep_debt = min(8.0, self.sleep_debt + max(1, hour))
|
|
80
|
+
elif hour >= 23:
|
|
81
|
+
# Late night: small debt for staying up
|
|
82
|
+
self.sleep_debt = min(8.0, self.sleep_debt + (hour - 22))
|
|
83
|
+
self._save()
|
|
84
|
+
|
|
85
|
+
# Generate a dream when falling asleep
|
|
86
|
+
try:
|
|
87
|
+
from brain.dreams import get_dream_system
|
|
88
|
+
ds = get_dream_system()
|
|
89
|
+
dream = ds.generate_dream()
|
|
90
|
+
if dream:
|
|
91
|
+
print(f"[Dreams] Generated dream while falling asleep")
|
|
92
|
+
except Exception as e:
|
|
93
|
+
print(f"[Dreams] Error generating dream: {e}")
|
|
94
|
+
|
|
95
|
+
def wake_up(self):
|
|
96
|
+
"""Alive-AI wakes up."""
|
|
97
|
+
self.is_asleep = False
|
|
98
|
+
self.wake_time = datetime.now().isoformat()
|
|
99
|
+
self.forced_awake = False
|
|
100
|
+
# Recover some sleep debt based on sleep duration
|
|
101
|
+
if self.sleep_start:
|
|
102
|
+
try:
|
|
103
|
+
start = datetime.fromisoformat(self.sleep_start)
|
|
104
|
+
slept = (datetime.now() - start).total_seconds() / 3600
|
|
105
|
+
self.sleep_debt = max(0.0, self.sleep_debt - slept * 0.5)
|
|
106
|
+
except Exception:
|
|
107
|
+
pass
|
|
108
|
+
self._save()
|
|
109
|
+
|
|
110
|
+
def stay_up_for_user(self):
|
|
111
|
+
"""User is keeping her awake past bedtime."""
|
|
112
|
+
self.forced_awake = True
|
|
113
|
+
hour = datetime.now().hour
|
|
114
|
+
if hour >= 23 or hour < 6:
|
|
115
|
+
self.sleep_debt = min(8.0, self.sleep_debt + 0.25)
|
|
116
|
+
self._save()
|
|
117
|
+
|
|
118
|
+
def is_sleeping(self) -> bool:
|
|
119
|
+
"""Check if Alive-AI is currently asleep."""
|
|
120
|
+
self._auto_update_sleep_state()
|
|
121
|
+
return self.is_asleep
|
|
122
|
+
|
|
123
|
+
def get_personality_modifiers(self) -> Dict[str, float]:
|
|
124
|
+
"""Get current time-of-day personality multipliers."""
|
|
125
|
+
hour = datetime.now().hour
|
|
126
|
+
_, phase = _get_phase_for_hour(hour)
|
|
127
|
+
|
|
128
|
+
energy = phase["energy"]
|
|
129
|
+
inhibition = phase["inhibition"]
|
|
130
|
+
warmth = phase["warmth"]
|
|
131
|
+
verbosity = phase["verbosity"]
|
|
132
|
+
|
|
133
|
+
# Sleep debt reduces energy and verbosity
|
|
134
|
+
debt_factor = max(0.5, 1.0 - self.sleep_debt * 0.08)
|
|
135
|
+
energy *= debt_factor
|
|
136
|
+
verbosity *= debt_factor
|
|
137
|
+
|
|
138
|
+
# Just woke up? Extra groggy for first 30 min
|
|
139
|
+
if self.wake_time:
|
|
140
|
+
try:
|
|
141
|
+
since_wake = (datetime.now() - datetime.fromisoformat(self.wake_time)).total_seconds() / 60
|
|
142
|
+
if since_wake < 30:
|
|
143
|
+
grogginess = 1.0 - (since_wake / 30)
|
|
144
|
+
energy *= (1.0 - grogginess * 0.4)
|
|
145
|
+
verbosity *= (1.0 - grogginess * 0.3)
|
|
146
|
+
except Exception:
|
|
147
|
+
pass
|
|
148
|
+
|
|
149
|
+
return {
|
|
150
|
+
"energy": round(min(1.0, max(0.05, energy)), 2),
|
|
151
|
+
"inhibition": round(inhibition, 2),
|
|
152
|
+
"warmth": round(min(1.0, warmth), 2),
|
|
153
|
+
"verbosity": round(min(1.0, max(0.1, verbosity)), 2),
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
def get_current_vibe(self) -> str:
|
|
157
|
+
"""Get a short description of current time-of-day vibe."""
|
|
158
|
+
if self.is_sleeping():
|
|
159
|
+
return "asleep"
|
|
160
|
+
hour = datetime.now().hour
|
|
161
|
+
_, phase = _get_phase_for_hour(hour)
|
|
162
|
+
return phase["vibe"]
|
|
163
|
+
|
|
164
|
+
def tick(self):
|
|
165
|
+
"""Periodic update. Call regularly."""
|
|
166
|
+
self._auto_update_sleep_state()
|
|
167
|
+
# Slow natural sleep debt recovery during waking hours
|
|
168
|
+
if not self.is_asleep:
|
|
169
|
+
self.sleep_debt = max(0.0, self.sleep_debt - 0.01)
|
|
170
|
+
|
|
171
|
+
def _save(self):
|
|
172
|
+
try:
|
|
173
|
+
self.PERSISTENCE_PATH.parent.mkdir(parents=True, exist_ok=True)
|
|
174
|
+
data = {
|
|
175
|
+
"is_asleep": self.is_asleep,
|
|
176
|
+
"sleep_start": self.sleep_start,
|
|
177
|
+
"wake_time": self.wake_time,
|
|
178
|
+
"sleep_debt": self.sleep_debt,
|
|
179
|
+
"last_bedtime_hour": self.last_bedtime_hour,
|
|
180
|
+
"forced_awake": self.forced_awake,
|
|
181
|
+
"saved_at": datetime.now().isoformat(),
|
|
182
|
+
}
|
|
183
|
+
self.PERSISTENCE_PATH.write_text(json.dumps(data, indent=2))
|
|
184
|
+
except Exception as e:
|
|
185
|
+
print(f"[Circadian] Error saving: {e}")
|
|
186
|
+
|
|
187
|
+
def _load(self):
|
|
188
|
+
try:
|
|
189
|
+
if self.PERSISTENCE_PATH.exists():
|
|
190
|
+
data = json.loads(self.PERSISTENCE_PATH.read_text())
|
|
191
|
+
self.is_asleep = data.get("is_asleep", False)
|
|
192
|
+
self.sleep_start = data.get("sleep_start")
|
|
193
|
+
self.wake_time = data.get("wake_time")
|
|
194
|
+
self.sleep_debt = data.get("sleep_debt", 0.0)
|
|
195
|
+
self.last_bedtime_hour = data.get("last_bedtime_hour", 23)
|
|
196
|
+
self.forced_awake = data.get("forced_awake", False)
|
|
197
|
+
print(f"[Circadian] Loaded state (asleep={self.is_asleep}, debt={self.sleep_debt:.1f}h)")
|
|
198
|
+
except Exception as e:
|
|
199
|
+
print(f"[Circadian] Error loading: {e}")
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
# =============================================================================
|
|
203
|
+
# SINGLETON ACCESS
|
|
204
|
+
# =============================================================================
|
|
205
|
+
|
|
206
|
+
_instance: Optional[CircadianEngine] = None
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
def get_circadian_engine() -> CircadianEngine:
|
|
210
|
+
global _instance
|
|
211
|
+
if _instance is None:
|
|
212
|
+
_instance = CircadianEngine()
|
|
213
|
+
return _instance
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
def is_sleeping() -> bool:
|
|
217
|
+
return get_circadian_engine().is_sleeping()
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
def wake_up():
|
|
221
|
+
get_circadian_engine().wake_up()
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
def fall_asleep():
|
|
225
|
+
get_circadian_engine().fall_asleep()
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
def tick():
|
|
229
|
+
get_circadian_engine().tick()
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
def get_circadian_prompt_section() -> str:
|
|
233
|
+
"""Get prompt section for LLM. Returns '' if nothing notable."""
|
|
234
|
+
engine = get_circadian_engine()
|
|
235
|
+
|
|
236
|
+
if engine.is_sleeping():
|
|
237
|
+
return "\n[Circadian State]\nYou are asleep. If woken, be groggy and disoriented. Otherwise, don't respond.\n"
|
|
238
|
+
|
|
239
|
+
now = datetime.now()
|
|
240
|
+
time_str = now.strftime("%-I:%M%p").lower()
|
|
241
|
+
phase_name, _ = _get_phase_for_hour(now.hour)
|
|
242
|
+
vibe = engine.get_current_vibe()
|
|
243
|
+
mods = engine.get_personality_modifiers()
|
|
244
|
+
|
|
245
|
+
parts = [f"It's {time_str} - {vibe}."]
|
|
246
|
+
if engine.sleep_debt > 2:
|
|
247
|
+
parts.append(f"Sleep-deprived ({engine.sleep_debt:.0f}h debt) - extra tired and foggy.")
|
|
248
|
+
if engine.forced_awake:
|
|
249
|
+
parts.append("Staying up late for him - sweet but exhausted.")
|
|
250
|
+
|
|
251
|
+
return f"\n[Circadian State]\n" + " ".join(parts) + "\n"
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Heart: Complex Emotions
|
|
3
|
+
Secondary emotions that blend with base emotions - guilt, pride, jealousy, etc.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from dataclasses import dataclass
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
@dataclass
|
|
10
|
+
class ComplexEmotion:
|
|
11
|
+
"""A complex emotion with blend factors"""
|
|
12
|
+
value: float = 0.0
|
|
13
|
+
trigger_count: int = 0
|
|
14
|
+
last_intensity: float = 0.0
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class ComplexEmotions:
|
|
18
|
+
"""Secondary emotions that add depth to emotional responses"""
|
|
19
|
+
|
|
20
|
+
# Trigger words for complex emotions
|
|
21
|
+
GUILT_TRIGGERS = [
|
|
22
|
+
"sorry", "apologize", "my fault", "i messed up", "i hurt you",
|
|
23
|
+
"mistake", "wrong", "shouldn't have", "regret", "feel bad"
|
|
24
|
+
]
|
|
25
|
+
|
|
26
|
+
PRIDE_TRIGGERS = [
|
|
27
|
+
"proud of you", "you did great", "amazing job", "well done",
|
|
28
|
+
"accomplished", "achievement", "success", "you're so good"
|
|
29
|
+
]
|
|
30
|
+
|
|
31
|
+
JEALOUSY_TRIGGERS = [
|
|
32
|
+
"other girl", "she's", "my ex", "talking to", "with someone",
|
|
33
|
+
"her name", "another woman", "met this girl", "friend who's a girl"
|
|
34
|
+
]
|
|
35
|
+
|
|
36
|
+
EMBARRASSMENT_TRIGGERS = [
|
|
37
|
+
"awkward", "cringe", "embarrassing", "lol at you", "haha you",
|
|
38
|
+
"everyone saw", "in public", "so embarrassed"
|
|
39
|
+
]
|
|
40
|
+
|
|
41
|
+
ANTICIPATION_TRIGGERS = [
|
|
42
|
+
"can't wait", "excited for", "looking forward", "counting down",
|
|
43
|
+
"soon", "tomorrow", "this weekend", "planned", "surprise"
|
|
44
|
+
]
|
|
45
|
+
|
|
46
|
+
def __init__(self):
|
|
47
|
+
self.guilt = ComplexEmotion()
|
|
48
|
+
self.pride = ComplexEmotion()
|
|
49
|
+
self.jealousy = ComplexEmotion()
|
|
50
|
+
self.embarrassment = ComplexEmotion()
|
|
51
|
+
self.anticipation = ComplexEmotion()
|
|
52
|
+
|
|
53
|
+
def process(self, message: str) -> dict:
|
|
54
|
+
"""Process message for complex emotion triggers"""
|
|
55
|
+
msg = message.lower()
|
|
56
|
+
changes = {}
|
|
57
|
+
|
|
58
|
+
# Check each complex emotion
|
|
59
|
+
if any(t in msg for t in self.GUILT_TRIGGERS):
|
|
60
|
+
self.guilt.value = min(1.0, self.guilt.value + 0.3)
|
|
61
|
+
self.guilt.trigger_count += 1
|
|
62
|
+
changes["guilt"] = 0.3
|
|
63
|
+
|
|
64
|
+
if any(t in msg for t in self.PRIDE_TRIGGERS):
|
|
65
|
+
self.pride.value = min(1.0, self.pride.value + 0.25)
|
|
66
|
+
self.pride.trigger_count += 1
|
|
67
|
+
changes["pride"] = 0.25
|
|
68
|
+
|
|
69
|
+
if any(t in msg for t in self.JEALOUSY_TRIGGERS):
|
|
70
|
+
self.jealousy.value = min(1.0, self.jealousy.value + 0.35)
|
|
71
|
+
self.jealousy.trigger_count += 1
|
|
72
|
+
changes["jealousy"] = 0.35
|
|
73
|
+
|
|
74
|
+
if any(t in msg for t in self.EMBARRASSMENT_TRIGGERS):
|
|
75
|
+
self.embarrassment.value = min(1.0, self.embarrassment.value + 0.25)
|
|
76
|
+
self.embarrassment.trigger_count += 1
|
|
77
|
+
changes["embarrassment"] = 0.25
|
|
78
|
+
|
|
79
|
+
if any(t in msg for t in self.ANTICIPATION_TRIGGERS):
|
|
80
|
+
self.anticipation.value = min(1.0, self.anticipation.value + 0.3)
|
|
81
|
+
self.anticipation.trigger_count += 1
|
|
82
|
+
changes["anticipation"] = 0.3
|
|
83
|
+
|
|
84
|
+
return changes
|
|
85
|
+
|
|
86
|
+
def decay(self):
|
|
87
|
+
"""Natural decay of complex emotions"""
|
|
88
|
+
rate = 0.03
|
|
89
|
+
self.guilt.value = max(0, self.guilt.value - rate)
|
|
90
|
+
self.pride.value = max(0, self.pride.value - rate * 0.5) # Pride lingers
|
|
91
|
+
self.jealousy.value = max(0, self.jealousy.value - rate * 0.7) # Jealousy lingers
|
|
92
|
+
self.embarrassment.value = max(0, self.embarrassment.value - rate * 1.5) # Fades faster
|
|
93
|
+
self.anticipation.value = max(0, self.anticipation.value - rate * 0.3) # Very sticky
|
|
94
|
+
|
|
95
|
+
def get_blended_mood(self, base_mood: str) -> str:
|
|
96
|
+
"""Blend complex emotions into mood description"""
|
|
97
|
+
if self.jealousy.value > 0.5:
|
|
98
|
+
return f"{base_mood}_jealous"
|
|
99
|
+
if self.guilt.value > 0.5:
|
|
100
|
+
return f"{base_mood}_guilty"
|
|
101
|
+
if self.anticipation.value > 0.6:
|
|
102
|
+
return f"{base_mood}_excited"
|
|
103
|
+
if self.pride.value > 0.5:
|
|
104
|
+
return f"{base_mood}_proud"
|
|
105
|
+
return base_mood
|
|
106
|
+
|
|
107
|
+
def to_dict(self) -> dict:
|
|
108
|
+
return {
|
|
109
|
+
"guilt": self.guilt.value,
|
|
110
|
+
"pride": self.pride.value,
|
|
111
|
+
"jealousy": self.jealousy.value,
|
|
112
|
+
"embarrassment": self.embarrassment.value,
|
|
113
|
+
"anticipation": self.anticipation.value
|
|
114
|
+
}
|