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,261 @@
1
+ """
2
+ Heart: Emotional Memory
3
+ Remember significant emotional events for context and continuity
4
+ Extended for Soul Architecture - embodied memory with somatic markers
5
+ """
6
+
7
+ from datetime import datetime
8
+ from dataclasses import dataclass, field
9
+ from typing import Optional, List, Dict
10
+ import json
11
+
12
+
13
+ @dataclass
14
+ class SomaticMarker:
15
+ """Bodily sensation associated with an emotional memory"""
16
+ region: str # chest, stomach, throat, etc.
17
+ quality: str # tight, warm, heavy, etc.
18
+ intensity: float # 0.0 - 1.0
19
+
20
+
21
+ @dataclass
22
+ class EmotionalEvent:
23
+ """A significant emotional moment worth remembering"""
24
+ timestamp: str
25
+ event_type: str # "peak_love", "peak_desire", "hurt", "joy", "conflict"
26
+ intensity: float
27
+ trigger: str # Brief description of what caused it
28
+ emotions: dict # Snapshot of emotional state
29
+
30
+ # Soul architecture extension - embodied memory
31
+ somatic_marker: Optional[SomaticMarker] = None # How it felt in the body
32
+ integrity_impact: float = 0.0 # How it affected self-integrity
33
+ scar_formed: bool = False # Did this contribute to a scar
34
+ times_recalled: int = 0 # How often this memory has been reactivated
35
+
36
+ def age_minutes(self) -> float:
37
+ """How old is this event in minutes"""
38
+ try:
39
+ event_time = datetime.fromisoformat(self.timestamp)
40
+ delta = datetime.now() - event_time
41
+ return delta.total_seconds() / 60
42
+ except:
43
+ return 9999
44
+
45
+ def recall_sensation(self) -> Optional[str]:
46
+ """Get the somatic sensation description for recall"""
47
+ if self.somatic_marker:
48
+ return f"{self.somatic_marker.quality} feeling in {self.somatic_marker.region}"
49
+ return None
50
+
51
+
52
+ class EmotionalMemory:
53
+ """Track and remember significant emotional events - extended for Soul Architecture"""
54
+
55
+ # Threshold for recording an event
56
+ RECORD_THRESHOLD = 0.7 # Only record high-intensity moments
57
+ MAX_EVENTS = 50 # Keep last 50 significant events
58
+
59
+ def __init__(self):
60
+ self.events: List[EmotionalEvent] = []
61
+ # Somatic patterns for different emotion types
62
+ self._somatic_patterns = {
63
+ "peak_love": SomaticMarker("chest", "warm", 0.6),
64
+ "peak_desire": SomaticMarker("stomach", "fluttery", 0.7),
65
+ "hurt": SomaticMarker("chest", "heavy", 0.6),
66
+ "joy": SomaticMarker("chest", "light", 0.5),
67
+ "conflict": SomaticMarker("stomach", "tight", 0.5),
68
+ "fear": SomaticMarker("stomach", "cold", 0.6),
69
+ "shame": SomaticMarker("face", "hot", 0.5),
70
+ "grief": SomaticMarker("chest", "aching", 0.7),
71
+ }
72
+
73
+ def record(self, event_type: str, intensity: float, trigger: str, emotions: dict,
74
+ integrity_impact: float = 0.0) -> Optional[EmotionalEvent]:
75
+ """Record a significant emotional event if worthy"""
76
+ if intensity < self.RECORD_THRESHOLD:
77
+ return None
78
+
79
+ # Get somatic marker for this emotion type
80
+ somatic_marker = self._somatic_patterns.get(event_type)
81
+
82
+ event = EmotionalEvent(
83
+ timestamp=datetime.now().isoformat(),
84
+ event_type=event_type,
85
+ intensity=min(1.0, intensity),
86
+ trigger=trigger[:100], # Truncate long triggers
87
+ emotions=emotions.copy(),
88
+ somatic_marker=somatic_marker,
89
+ integrity_impact=integrity_impact
90
+ )
91
+ self.events.append(event)
92
+
93
+ # Trim old events
94
+ if len(self.events) > self.MAX_EVENTS:
95
+ self.events = self.events[-self.MAX_EVENTS:]
96
+
97
+ return event
98
+
99
+ def record_embodied(self, event_type: str, intensity: float, trigger: str,
100
+ emotions: dict, somatic: SomaticMarker,
101
+ integrity_impact: float = 0.0) -> Optional[EmotionalEvent]:
102
+ """
103
+ Record an event with intimate somatic marker.
104
+ This is for when the bodily sensation is known.
105
+ """
106
+ if intensity < self.RECORD_THRESHOLD:
107
+ return None
108
+
109
+ event = EmotionalEvent(
110
+ timestamp=datetime.now().isoformat(),
111
+ event_type=event_type,
112
+ intensity=min(1.0, intensity),
113
+ trigger=trigger[:100],
114
+ emotions=emotions.copy(),
115
+ somatic_marker=somatic,
116
+ integrity_impact=integrity_impact
117
+ )
118
+ self.events.append(event)
119
+
120
+ if len(self.events) > self.MAX_EVENTS:
121
+ self.events = self.events[-self.MAX_EVENTS:]
122
+
123
+ return event
124
+
125
+ def check_peaks(self, current: dict, previous: dict, trigger: str) -> list:
126
+ """Check for emotional peaks worth recording"""
127
+ recorded = []
128
+
129
+ # Peak desire
130
+ if current.get("desire", 0) >= 0.9 and previous.get("desire", 0) < 0.9:
131
+ event = self.record("peak_desire", current["desire"], trigger, current)
132
+ if event:
133
+ recorded.append(event)
134
+
135
+ # Peak love
136
+ if current.get("love", 0) >= 0.9 and previous.get("love", 0) < 0.9:
137
+ event = self.record("peak_love", current["love"], trigger, current)
138
+ if event:
139
+ recorded.append(event)
140
+
141
+ # Deep hurt
142
+ if current.get("sadness", 0) >= 0.7 and previous.get("sadness", 0) < 0.5:
143
+ event = self.record("hurt", current["sadness"], trigger, current,
144
+ integrity_impact=-0.2)
145
+ if event:
146
+ recorded.append(event)
147
+
148
+ # Peak joy
149
+ if current.get("joy", 0) >= 0.9 and previous.get("joy", 0) < 0.7:
150
+ event = self.record("joy", current["joy"], trigger, current,
151
+ integrity_impact=0.1)
152
+ if event:
153
+ recorded.append(event)
154
+
155
+ # High vulnerability (soul architecture)
156
+ if current.get("vulnerability", 0) >= 0.7 and previous.get("vulnerability", 0) < 0.5:
157
+ event = self.record("fear", current.get("vulnerability", 0), trigger, current,
158
+ integrity_impact=-0.15)
159
+ if event:
160
+ recorded.append(event)
161
+
162
+ return recorded
163
+
164
+ def recall_with_sensation(self, event_id: str = None, event_type: str = None) -> Optional[dict]:
165
+ """
166
+ Recall a memory and re-trigger its somatic sensation.
167
+ This implements embodied memory - remembering re-feels the bodily sensation.
168
+ """
169
+ event = None
170
+
171
+ if event_id:
172
+ event = next((e for e in self.events if e.timestamp == event_id), None)
173
+ elif event_type:
174
+ # Get most recent event of this type
175
+ matching = [e for e in self.events if e.event_type == event_type]
176
+ if matching:
177
+ event = matching[-1]
178
+
179
+ if not event:
180
+ return None
181
+
182
+ # Increment recall count
183
+ event.times_recalled += 1
184
+
185
+ # Return memory with sensation (diminished by time)
186
+ age_factor = max(0.3, 1.0 - (event.age_minutes() / 1440)) # Fade over 24 hours
187
+
188
+ return {
189
+ "event_type": event.event_type,
190
+ "trigger": event.trigger,
191
+ "intensity": event.intensity * age_factor,
192
+ "sensation": event.recall_sensation(),
193
+ "sensation_intensity": (event.somatic_marker.intensity * age_factor) if event.somatic_marker else 0,
194
+ "times_recalled": event.times_recalled,
195
+ "age_minutes": event.age_minutes()
196
+ }
197
+
198
+ def get_somatic_history(self, event_type: str = None, hours: float = 24) -> List[str]:
199
+ """Get list of somatic sensations from recent events"""
200
+ cutoff = datetime.now() - __import__('datetime').timedelta(hours=hours)
201
+ sensations = []
202
+
203
+ for event in self.events:
204
+ event_time = datetime.fromisoformat(event.timestamp)
205
+ if event_time >= cutoff:
206
+ if event_type is None or event.event_type == event_type:
207
+ if event.somatic_marker:
208
+ sensations.append(f"{event.somatic_marker.quality} in {event.somatic_marker.region}")
209
+
210
+ return sensations
211
+
212
+ def recent(self, event_type: Optional[str] = None, minutes: float = 60) -> list:
213
+ """Get recent events, optionally filtered by type"""
214
+ return [
215
+ e for e in self.events
216
+ if e.age_minutes() <= minutes
217
+ and (event_type is None or e.event_type == event_type)
218
+ ]
219
+
220
+ def get_mood_context(self) -> str:
221
+ """Generate context string from recent significant events"""
222
+ recent_events = self.recent(minutes=120) # Last 2 hours
223
+ if not recent_events:
224
+ return ""
225
+
226
+ summaries = {
227
+ "peak_love": "felt deeply loved",
228
+ "peak_desire": "felt intense desire",
229
+ "hurt": "was hurt",
230
+ "joy": "felt pure joy",
231
+ "conflict": "had a conflict",
232
+ "fear": "felt vulnerable"
233
+ }
234
+
235
+ context_parts = []
236
+ for event in recent_events[-3:]: # Last 3 events
237
+ desc = summaries.get(event.event_type, "felt something")
238
+ age = int(event.age_minutes())
239
+ if age < 5:
240
+ context_parts.append(f"just {desc}")
241
+ elif age < 30:
242
+ context_parts.append(f"{desc} {age} minutes ago")
243
+ else:
244
+ context_parts.append(f"{desc} earlier")
245
+
246
+ return "; ".join(context_parts) if context_parts else ""
247
+
248
+ def to_dict(self) -> dict:
249
+ return {
250
+ "events": [
251
+ {
252
+ "timestamp": e.timestamp,
253
+ "type": e.event_type,
254
+ "intensity": e.intensity,
255
+ "trigger": e.trigger,
256
+ "somatic": e.recall_sensation() if e.somatic_marker else None,
257
+ "times_recalled": e.times_recalled
258
+ }
259
+ for e in self.events[-10:] # Save only last 10
260
+ ]
261
+ }
@@ -0,0 +1,146 @@
1
+ """
2
+ EmotionalState: Data class for emotional state with persistence
3
+ Extended for Soul Architecture dimensions
4
+ """
5
+ import json
6
+ from datetime import datetime
7
+ from pathlib import Path
8
+
9
+ EMOTION_STATE_PATH = Path("/app/data/emotion_state.json") # Use mounted /app directory
10
+ DEFAULTS = {
11
+ "valence": 0.5, "arousal": 0.3, "dominance": 0.5, "desire": 0.0,
12
+ "high_desire_threshold": 0.7, "joy": 0.5, "love": 0.2, "trust": 0.5,
13
+ "fear": 0.1, "anger": 0.1, "sadness": 0.1, "boredom": 0.0,
14
+ "guilt": 0.0, "pride": 0.0, "jealousy": 0.0,
15
+ "embarrassment": 0.0, "anticipation": 0.0,
16
+ # Soul architecture dimensions
17
+ "integrity_overall": 0.65, "vulnerability": 0.2, "hope": 0.5, "dread": 0.1
18
+ }
19
+
20
+
21
+ class EmotionalState:
22
+ """Current emotional state with persistence - extended for Soul Architecture"""
23
+
24
+ def __init__(self, config: dict = None):
25
+ config = config or {}
26
+ if self._load():
27
+ print(f"[Heart] Loaded saved emotional state")
28
+ else:
29
+ for key, val in DEFAULTS.items():
30
+ setattr(self, key, val)
31
+ self.baseline = {"joy": 0.5, "love": 0.2, "trust": 0.5,
32
+ "arousal": 0.3, "desire": 0.0,
33
+ "anger": 0.0, "sadness": 0.1, "fear": 0.1}
34
+
35
+ def _load(self) -> bool:
36
+ """Load state from file"""
37
+ try:
38
+ if EMOTION_STATE_PATH.exists():
39
+ data = json.loads(EMOTION_STATE_PATH.read_text())
40
+ for key in DEFAULTS:
41
+ setattr(self, key, data.get(key, DEFAULTS[key]))
42
+ return True
43
+ except Exception as e:
44
+ print(f"[Heart] Error loading state: {e}")
45
+ return False
46
+
47
+ def save(self):
48
+ """Save state to file"""
49
+ try:
50
+ EMOTION_STATE_PATH.parent.mkdir(parents=True, exist_ok=True)
51
+ data = {key: getattr(self, key) for key in DEFAULTS}
52
+ data["saved_at"] = datetime.now().isoformat()
53
+ EMOTION_STATE_PATH.write_text(json.dumps(data, indent=2))
54
+ except Exception as e:
55
+ print(f"[Heart] Error saving state: {e}")
56
+
57
+ @property
58
+ def is_high_desire(self) -> bool:
59
+ return self.desire >= self.high_desire_threshold
60
+
61
+ @property
62
+ def is_in_love(self) -> bool:
63
+ """True when deeply in love - triggers clingy behavior"""
64
+ return self.love >= 0.8
65
+
66
+ @property
67
+ def is_jealous(self) -> bool:
68
+ return self.jealousy > 0.5
69
+
70
+ @property
71
+ def is_guilty(self) -> bool:
72
+ return self.guilt > 0.5
73
+
74
+ @property
75
+ def is_anticipating(self) -> bool:
76
+ return self.anticipation > 0.6
77
+
78
+ @property
79
+ def _base_mood(self) -> str:
80
+ """Base mood without complex emotion modifiers"""
81
+ if self.is_high_desire: return "high_desire"
82
+ if self.is_in_love: return "in_love"
83
+ if self.desire > 0.5: return "excited"
84
+ if self.boredom > 0.6: return "bored"
85
+ if self.joy > 0.7: return "happy"
86
+ if self.sadness > 0.5: return "sad"
87
+ if self.anger > 0.5: return "angry"
88
+ return "neutral"
89
+
90
+ @property
91
+ def mood_description(self) -> str:
92
+ base = self._base_mood
93
+ if self.jealousy > 0.5: return f"{base}_jealous"
94
+ if self.guilt > 0.5: return f"{base}_guilty"
95
+ if self.anticipation > 0.6: return f"{base}_eager"
96
+ if self.pride > 0.5: return f"{base}_proud"
97
+ if self.embarrassment > 0.4: return f"{base}_shy"
98
+ return base
99
+
100
+ # --- Soul Architecture Properties ---
101
+
102
+ @property
103
+ def is_vulnerable(self) -> bool:
104
+ """True when feeling emotionally exposed or fragile"""
105
+ return self.vulnerability > 0.5
106
+
107
+ @property
108
+ def is_hopeful(self) -> bool:
109
+ """True when feeling hopeful about the future"""
110
+ return self.hope > 0.6 and self.dread < 0.3
111
+
112
+ @property
113
+ def is_dreading(self) -> bool:
114
+ """True when feeling dread about the future"""
115
+ return self.dread > 0.5
116
+
117
+ @property
118
+ def is_in_crisis(self) -> bool:
119
+ """True when integrity is critically low"""
120
+ return self.integrity_overall < 0.25
121
+
122
+ @property
123
+ def is_flourishing(self) -> bool:
124
+ """True when integrity is high and stable"""
125
+ return self.integrity_overall > 0.75 and self.vulnerability < 0.3
126
+
127
+ def update_soul_dimensions(self, integrity: float, vulnerability: float,
128
+ hope: float, dread: float):
129
+ """Update soul architecture dimensions from soul processing"""
130
+ self.integrity_overall = integrity
131
+ self.vulnerability = vulnerability
132
+ self.hope = hope
133
+ self.dread = dread
134
+
135
+ def get_soul_summary(self) -> dict:
136
+ """Get summary of soul-related state dimensions"""
137
+ return {
138
+ "integrity_overall": self.integrity_overall,
139
+ "vulnerability": self.vulnerability,
140
+ "hope": self.hope,
141
+ "dread": self.dread,
142
+ "is_vulnerable": self.is_vulnerable,
143
+ "is_hopeful": self.is_hopeful,
144
+ "is_in_crisis": self.is_in_crisis,
145
+ "is_flourishing": self.is_flourishing
146
+ }
@@ -0,0 +1,156 @@
1
+ """
2
+ Heart: Emotional Variability
3
+ Add organic randomness, inertia, and reaction cooldowns
4
+ """
5
+
6
+ import random
7
+ import time
8
+ from dataclasses import dataclass, field
9
+
10
+
11
+ @dataclass
12
+ class ReactionCooldown:
13
+ """Track when reactions were last used"""
14
+ emoji: str = ""
15
+ last_used: float = 0.0
16
+ count_recent: int = 0
17
+
18
+
19
+ class EmotionalVariability:
20
+ """Adds organic feel to emotional responses"""
21
+
22
+ # Minimum seconds between same emoji reactions
23
+ COOLDOWN_SECONDS = 60
24
+
25
+ # How many recent uses trigger "cooling off"
26
+ SPAM_THRESHOLD = 3
27
+
28
+ def __init__(self):
29
+ self.reaction_history: dict[str, ReactionCooldown] = {}
30
+ self.momentum: dict[str, float] = {} # Emotional inertia
31
+ self.last_tick_randomness = 0.0
32
+
33
+ def can_react(self, emoji: str) -> bool:
34
+ """Check if this emoji is off cooldown"""
35
+ now = time.time()
36
+
37
+ if emoji not in self.reaction_history:
38
+ self.reaction_history[emoji] = ReactionCooldown(emoji=emoji)
39
+ return True
40
+
41
+ history = self.reaction_history[emoji]
42
+
43
+ # Check cooldown
44
+ if now - history.last_used < self.COOLDOWN_SECONDS:
45
+ return False
46
+
47
+ # Reduce spam count over time
48
+ if now - history.last_used > self.COOLDOWN_SECONDS * 2:
49
+ history.count_recent = max(0, history.count_recent - 1)
50
+
51
+ return True
52
+
53
+ def record_reaction(self, emoji: str):
54
+ """Record that a reaction was used"""
55
+ now = time.time()
56
+
57
+ if emoji not in self.reaction_history:
58
+ self.reaction_history[emoji] = ReactionCooldown(emoji=emoji)
59
+
60
+ history = self.reaction_history[emoji]
61
+ history.last_used = now
62
+ history.count_recent += 1
63
+
64
+ def get_reaction_probability(self, emoji: str) -> float:
65
+ """Get probability modifier based on recent usage"""
66
+ if emoji not in self.reaction_history:
67
+ return 1.0
68
+
69
+ history = self.reaction_history[emoji]
70
+
71
+ # Reduce probability if spammed recently
72
+ if history.count_recent >= self.SPAM_THRESHOLD:
73
+ return 0.2 # 20% chance if spammed
74
+
75
+ if history.count_recent >= 2:
76
+ return 0.5 # 50% chance if used a few times
77
+
78
+ return 1.0
79
+
80
+ def add_momentum(self, emotion: str, change: float):
81
+ """Track emotional momentum/inertia"""
82
+ current = self.momentum.get(emotion, 0.0)
83
+ # Weighted average with recent changes
84
+ self.momentum[emotion] = current * 0.6 + change * 0.4
85
+
86
+ def get_inertia_modifier(self, emotion: str, current_value: float) -> float:
87
+ """Get modifier for emotional changes based on momentum"""
88
+ momentum = self.momentum.get(emotion, 0.0)
89
+
90
+ # High momentum in same direction = amplification
91
+ # High momentum in opposite direction = resistance
92
+ if abs(momentum) > 0.1:
93
+ # Amplify changes in momentum direction
94
+ return 1.0 + momentum * 0.3
95
+
96
+ return 1.0
97
+
98
+ def randomize_decay(self, base_rate: float) -> float:
99
+ """Add organic randomness to decay rates"""
100
+ # Randomize between 0.7x and 1.3x base rate
101
+ multiplier = random.uniform(0.7, 1.3)
102
+ return base_rate * multiplier
103
+
104
+ def get_organic_tick(self) -> float:
105
+ """Get a random organic fluctuation for tick updates"""
106
+ # Small random emotional noise (-0.02 to +0.02)
107
+ self.last_tick_randomness = random.gauss(0, 0.01)
108
+ return self.last_tick_randomness
109
+
110
+ def should_skip_decay(self, emotion: str, value: float) -> bool:
111
+ """Sometimes emotions just don't decay (organic feel)"""
112
+ # 10% chance to skip decay entirely
113
+ if random.random() < 0.1:
114
+ return True
115
+
116
+ # High-intensity emotions resist decay more
117
+ if value > 0.8 and random.random() < 0.2:
118
+ return True
119
+
120
+ return False
121
+
122
+ def get_available_emojis(self, preferred: list[str]) -> list[str]:
123
+ """Filter emoji list to only available ones (off cooldown)"""
124
+ return [e for e in preferred if self.can_react(e)]
125
+
126
+ def choose_organic_reaction(self, candidates: list[str]) -> str | None:
127
+ """Choose a reaction with organic variability"""
128
+ available = self.get_available_emojis(candidates)
129
+
130
+ if not available:
131
+ return None
132
+
133
+ # Weight by inverse of recent usage
134
+ weights = []
135
+ for emoji in available:
136
+ prob = self.get_reaction_probability(emoji)
137
+ weights.append(prob)
138
+
139
+ if sum(weights) == 0:
140
+ return None
141
+
142
+ chosen = random.choices(available, weights=weights, k=1)[0]
143
+ self.record_reaction(chosen)
144
+ return chosen
145
+
146
+ def clear_old_history(self, max_age_seconds: float = 3600):
147
+ """Clean up old reaction history"""
148
+ now = time.time()
149
+ to_remove = []
150
+
151
+ for emoji, history in self.reaction_history.items():
152
+ if now - history.last_used > max_age_seconds:
153
+ to_remove.append(emoji)
154
+
155
+ for emoji in to_remove:
156
+ del self.reaction_history[emoji]