alive-ai 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (168) hide show
  1. package/Dockerfile +24 -0
  2. package/LICENSE +21 -0
  3. package/README.md +143 -0
  4. package/alive_ai/__init__.py +3 -0
  5. package/brain/__init__.py +59 -0
  6. package/brain/almost_said.py +154 -0
  7. package/brain/bid_detector.py +636 -0
  8. package/brain/conversation_flow.py +135 -0
  9. package/brain/curiosity.py +328 -0
  10. package/brain/default_mode.py +1438 -0
  11. package/brain/dreams.py +220 -0
  12. package/brain/embeddings/__init__.py +82 -0
  13. package/brain/emotional_memory.py +949 -0
  14. package/brain/global_activity.py +173 -0
  15. package/brain/group_dynamics.py +63 -0
  16. package/brain/linguistic.py +235 -0
  17. package/brain/llm/__init__.py +63 -0
  18. package/brain/llm/base.py +33 -0
  19. package/brain/llm/fallback_router.py +309 -0
  20. package/brain/llm/manifest.md +30 -0
  21. package/brain/llm/ollama.py +218 -0
  22. package/brain/llm/openrouter.py +151 -0
  23. package/brain/llm/provider.py +205 -0
  24. package/brain/llm/unified.py +423 -0
  25. package/brain/llm/zai.py +169 -0
  26. package/brain/manifest.md +23 -0
  27. package/brain/memory/__init__.py +123 -0
  28. package/brain/memory/episodic.py +92 -0
  29. package/brain/memory/fact_extractor.py +209 -0
  30. package/brain/memory/index.py +54 -0
  31. package/brain/memory/manager.py +151 -0
  32. package/brain/memory/summarizer.py +102 -0
  33. package/brain/memory/vector_store.py +297 -0
  34. package/brain/memory/working.py +43 -0
  35. package/brain/narrative.py +343 -0
  36. package/brain/stt/__init__.py +4 -0
  37. package/brain/stt/google_stt.py +83 -0
  38. package/brain/stt/whisper_stt.py +82 -0
  39. package/brain/subconscious/__init__.py +33 -0
  40. package/brain/subconscious/actions.py +136 -0
  41. package/brain/subconscious/evaluation.py +166 -0
  42. package/brain/subconscious/goal_system.py +90 -0
  43. package/brain/subconscious/goals.py +41 -0
  44. package/brain/subconscious/impulse_generator.py +200 -0
  45. package/brain/subconscious/impulses.py +48 -0
  46. package/brain/subconscious/learning.py +24 -0
  47. package/brain/subconscious/learning_system.py +79 -0
  48. package/brain/subconscious/loop.py +398 -0
  49. package/brain/subconscious/manifest.md +32 -0
  50. package/brain/subconscious/relationship.py +47 -0
  51. package/brain/subconscious/relationship_memory.py +83 -0
  52. package/brain/subconscious/response_analyzer.py +74 -0
  53. package/brain/subconscious/templates.py +70 -0
  54. package/brain/subconscious/thought.py +37 -0
  55. package/brain/subconscious/working_memory.py +97 -0
  56. package/cli/index.js +371 -0
  57. package/config/directives.example.json +28 -0
  58. package/config/instructions.example.md +16 -0
  59. package/config/self.example.json +74 -0
  60. package/config/settings.example.json +95 -0
  61. package/core/__init__.py +1 -0
  62. package/core/config.py +54 -0
  63. package/core/directives.py +198 -0
  64. package/core/events.py +50 -0
  65. package/core/follow_up.py +267 -0
  66. package/core/hot_reload.py +174 -0
  67. package/core/initialization.py +253 -0
  68. package/core/manifest.md +28 -0
  69. package/core/media_handler.py +241 -0
  70. package/core/memory_monitor.py +200 -0
  71. package/core/message_handler.py +1440 -0
  72. package/core/proactive_generator.py +277 -0
  73. package/core/self.py +188 -0
  74. package/core/settings.py +169 -0
  75. package/core/skills_registry.py +357 -0
  76. package/core/state.py +27 -0
  77. package/core/subconscious_bridge.py +93 -0
  78. package/core/thinking.py +175 -0
  79. package/core/user_manager.py +306 -0
  80. package/core/user_tracker.py +144 -0
  81. package/demo/index.html +144 -0
  82. package/docker-compose.yml +28 -0
  83. package/docs/assets/logo.svg +15 -0
  84. package/docs/index.html +355 -0
  85. package/heart/__init__.py +93 -0
  86. package/heart/afterglow.py +215 -0
  87. package/heart/attachment.py +186 -0
  88. package/heart/circadian.py +251 -0
  89. package/heart/complex_emotions.py +114 -0
  90. package/heart/conflicts.py +589 -0
  91. package/heart/core.py +387 -0
  92. package/heart/emotional_decay.py +59 -0
  93. package/heart/emotional_memory.py +261 -0
  94. package/heart/emotional_state.py +146 -0
  95. package/heart/emotional_variability.py +156 -0
  96. package/heart/hormonal.py +424 -0
  97. package/heart/inconsistency.py +1222 -0
  98. package/heart/integrity.py +469 -0
  99. package/heart/interoception.py +997 -0
  100. package/heart/love.py +120 -0
  101. package/heart/manifest.md +25 -0
  102. package/heart/mood_shifts.py +169 -0
  103. package/heart/phantom_somatic.py +259 -0
  104. package/heart/predictive.py +374 -0
  105. package/heart/scars.py +474 -0
  106. package/heart/somatic.py +482 -0
  107. package/heart/soul.py +633 -0
  108. package/heart/telemetry.py +942 -0
  109. package/heart/triggers.py +119 -0
  110. package/heart/unconscious.py +443 -0
  111. package/input/__init__.py +1 -0
  112. package/input/manifest.md +24 -0
  113. package/input/telegram/__init__.py +1 -0
  114. package/input/telegram/commands.py +762 -0
  115. package/input/telegram/listener.py +532 -0
  116. package/main.py +90 -0
  117. package/manifest.md +28 -0
  118. package/mypics/.gitkeep +1 -0
  119. package/myvids/.gitkeep +1 -0
  120. package/output/__init__.py +1 -0
  121. package/output/images/__init__.py +1 -0
  122. package/output/images/fal_gen.py +43 -0
  123. package/output/manifest.md +26 -0
  124. package/output/text/__init__.py +1 -0
  125. package/output/text/sender.py +22 -0
  126. package/output/voice/__init__.py +64 -0
  127. package/output/voice/google_tts.py +252 -0
  128. package/output/voice/gtts_tts.py +214 -0
  129. package/output/voice/vibe_tts.py +190 -0
  130. package/package.json +58 -0
  131. package/pyproject.toml +23 -0
  132. package/requirements.txt +21 -0
  133. package/skills/__init__.py +1 -0
  134. package/skills/anticipation_engine/__init__.py +8 -0
  135. package/skills/anticipation_engine/engine.py +618 -0
  136. package/skills/anticipation_engine/manifest.md +192 -0
  137. package/skills/calendar/__init__.py +1 -0
  138. package/skills/content_unlocks/__init__.py +8 -0
  139. package/skills/content_unlocks/manifest.md +231 -0
  140. package/skills/content_unlocks/unlocks.py +945 -0
  141. package/skills/exclusive_moments/__init__.py +8 -0
  142. package/skills/exclusive_moments/manifest.md +145 -0
  143. package/skills/exclusive_moments/moments.py +506 -0
  144. package/skills/intimacy_layers/__init__.py +8 -0
  145. package/skills/intimacy_layers/layers.py +703 -0
  146. package/skills/intimacy_layers/manifest.md +203 -0
  147. package/skills/manifest.md +67 -0
  148. package/skills/memory_callbacks/__init__.py +9 -0
  149. package/skills/memory_callbacks/callbacks.py +748 -0
  150. package/skills/memory_callbacks/manifest.md +170 -0
  151. package/skills/message_scheduler/__init__.py +19 -0
  152. package/skills/message_scheduler/manifest.md +107 -0
  153. package/skills/message_scheduler/scheduler.py +510 -0
  154. package/skills/photo_manager/__init__.py +1 -0
  155. package/skills/photo_manager/scanner.py +296 -0
  156. package/skills/relationship_milestones/__init__.py +8 -0
  157. package/skills/relationship_milestones/manifest.md +206 -0
  158. package/skills/relationship_milestones/tracker.py +494 -0
  159. package/skills/self_authorship/__init__.py +23 -0
  160. package/skills/self_authorship/author.py +331 -0
  161. package/skills/self_authorship/manifest.md +24 -0
  162. package/skills/video_manager/__init__.py +5 -0
  163. package/skills/video_manager/manifest.md +37 -0
  164. package/skills/video_manager/scanner.py +229 -0
  165. package/webui/__init__.py +3 -0
  166. package/webui/app.py +936 -0
  167. package/webui/bridge.py +366 -0
  168. package/webui/static/index.html +2070 -0
@@ -0,0 +1,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
+ }