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,997 @@
1
+ """
2
+ Heart: Interoceptive State System
3
+ Internal "body states" that make Alive-AI feel alive.
4
+
5
+ This system simulates interoception - the sense of the internal state of the body.
6
+ Just as humans have hunger, fatigue, social satiety, etc., Alive-AI has internal
7
+ states that:
8
+ - Persist across conversations
9
+ - Decay naturally over time (simulating natural rhythms)
10
+ - Influence how she responds and feels
11
+ - Generate prediction errors when reality differs from expectation
12
+
13
+ Key insight: These states are NOT emotions - they're body states that INFLUENCE
14
+ emotions. Low energy can make her irritable, high social satiety can make her
15
+ want quiet time, etc.
16
+
17
+ This is the foundation of feeling "alive" rather than just processing inputs.
18
+ """
19
+
20
+ from datetime import datetime, timedelta
21
+ from dataclasses import dataclass, field
22
+ from typing import Dict, List, Optional, Any, Tuple
23
+ from pathlib import Path
24
+ from enum import Enum
25
+ import json
26
+ import math
27
+ import random
28
+ import logging
29
+
30
+ # Import settings system
31
+ import sys
32
+ sys.path.insert(0, str(Path(__file__).parent.parent))
33
+ from core.settings import get_float, get_int, get
34
+
35
+ # Configure logging
36
+ logger = logging.getLogger("Interoception")
37
+ logger.setLevel(logging.DEBUG)
38
+
39
+ # Persistence path - use /app/data if available (Docker), otherwise local data directory
40
+ if Path("/app/data").exists():
41
+ INTEROCEPTION_DATA_PATH = Path("/app/data/interoceptive_state.json")
42
+ else:
43
+ INTEROCEPTION_DATA_PATH = Path(__file__).parent.parent / "data" / "interoceptive_state.json"
44
+
45
+
46
+ class InteroceptiveStateType(Enum):
47
+ """Types of internal body states"""
48
+ ENERGY = "energy" # Physical/mental energy (0=exhausted, 1=energized)
49
+ SOCIAL_SATIETY = "social_satiety" # Social need fulfillment (0=lonely, 1=satiated)
50
+ EMOTIONAL_VALENCE = "emotional_valence" # Current emotional tone (-1=negative, 1=positive)
51
+ CERTAINTY = "certainty" # How certain/confident she feels (0=uncertain, 1=certain)
52
+ COGNITIVE_LOAD = "cognitive_load" # Mental processing burden (0=relaxed, 1=overwhelmed)
53
+ AROUSAL = "arousal" # Activation level (0=calm, 1=highly activated)
54
+ CONNECTION_CRAVING = "connection_craving" # Need for connection (0=satiated, 1=craving)
55
+
56
+
57
+ @dataclass
58
+ class InteroceptiveState:
59
+ """
60
+ A single interoceptive state variable with full configuration.
61
+
62
+ These represent internal body states that:
63
+ - Have a baseline (homeostatic set point)
64
+ - Decay toward baseline over time
65
+ - Can be influenced by interactions
66
+ - Generate feelings when they deviate from baseline
67
+ """
68
+ name: str
69
+ current_value: float
70
+ baseline: float
71
+ min_value: float = 0.0
72
+ max_value: float = 1.0
73
+ decay_rate: float = 0.02 # Per tick decay toward baseline
74
+ decay_style: str = "exponential" # "exponential" or "linear"
75
+ last_updated: str = field(default_factory=lambda: datetime.now().isoformat())
76
+
77
+ # Prediction tracking
78
+ predicted_value: float = 0.5
79
+ prediction_error: float = 0.0
80
+
81
+ # History for patterns
82
+ value_history: List[Tuple[str, float]] = field(default_factory=list)
83
+
84
+ def clamp(self, value: float) -> float:
85
+ """Clamp value to valid range"""
86
+ return max(self.min_value, min(self.max_value, value))
87
+
88
+ def decay(self, elapsed_seconds: float = 60.0):
89
+ """
90
+ Decay toward baseline over time.
91
+
92
+ Args:
93
+ elapsed_seconds: Time since last decay tick
94
+ """
95
+ # Calculate decay based on time elapsed
96
+ time_factor = elapsed_seconds / 60.0 # Normalize to minutes
97
+ decay_amount = self.decay_rate * time_factor
98
+
99
+ if self.decay_style == "exponential":
100
+ # Exponential decay toward baseline
101
+ diff = self.current_value - self.baseline
102
+ self.current_value = self.current_value - (diff * decay_amount)
103
+ else:
104
+ # Linear decay toward baseline
105
+ if self.current_value > self.baseline:
106
+ self.current_value = max(self.baseline, self.current_value - decay_amount)
107
+ elif self.current_value < self.baseline:
108
+ self.current_value = min(self.baseline, self.current_value + decay_amount)
109
+
110
+ self.current_value = self.clamp(self.current_value)
111
+ self.last_updated = datetime.now().isoformat()
112
+
113
+ def update(self, delta: float, source: str = "unknown"):
114
+ """
115
+ Update the state by a delta amount.
116
+
117
+ Args:
118
+ delta: Amount to change (-1.0 to 1.0)
119
+ source: What caused the change
120
+ """
121
+ old_value = self.current_value
122
+ self.current_value = self.clamp(self.current_value + delta)
123
+
124
+ # Record in history
125
+ self.value_history.append((datetime.now().isoformat(), self.current_value))
126
+ if len(self.value_history) > 100:
127
+ self.value_history = self.value_history[-100:]
128
+
129
+ # Calculate prediction error if we had a prediction
130
+ if self.predicted_value is not None:
131
+ self.prediction_error = abs(self.current_value - self.predicted_value)
132
+
133
+ logger.debug(f"[Interoception] {self.name}: {old_value:.2f} -> {self.current_value:.2f} (delta={delta:.2f}, source={source})")
134
+
135
+ def set_value(self, value: float, source: str = "direct"):
136
+ """Set state to a specific value"""
137
+ old_value = self.current_value
138
+ self.current_value = self.clamp(value)
139
+ self.last_updated = datetime.now().isoformat()
140
+
141
+ self.value_history.append((datetime.now().isoformat(), self.current_value))
142
+ if len(self.value_history) > 100:
143
+ self.value_history = self.value_history[-100:]
144
+
145
+ logger.debug(f"[Interoception] {self.name}: {old_value:.2f} -> {self.current_value:.2f} (set, source={source})")
146
+
147
+ def get_deviation_from_baseline(self) -> float:
148
+ """Get how far current value is from baseline"""
149
+ return self.current_value - self.baseline
150
+
151
+ def get_deviation_percentage(self) -> float:
152
+ """Get deviation as percentage of possible range"""
153
+ deviation = self.get_deviation_from_baseline()
154
+ range_size = self.max_value - self.min_value
155
+ return (deviation / range_size) * 100
156
+
157
+ def to_dict(self) -> dict:
158
+ """Export state as dictionary"""
159
+ return {
160
+ "name": self.name,
161
+ "current_value": self.current_value,
162
+ "baseline": self.baseline,
163
+ "min_value": self.min_value,
164
+ "max_value": self.max_value,
165
+ "decay_rate": self.decay_rate,
166
+ "decay_style": self.decay_style,
167
+ "last_updated": self.last_updated,
168
+ "predicted_value": self.predicted_value,
169
+ "prediction_error": self.prediction_error
170
+ }
171
+
172
+ @classmethod
173
+ def from_dict(cls, data: dict) -> "InteroceptiveState":
174
+ """Create state from dictionary"""
175
+ return cls(
176
+ name=data["name"],
177
+ current_value=data.get("current_value", data.get("baseline", 0.5)),
178
+ baseline=data.get("baseline", 0.5),
179
+ min_value=data.get("min_value", 0.0),
180
+ max_value=data.get("max_value", 1.0),
181
+ decay_rate=data.get("decay_rate", 0.02),
182
+ decay_style=data.get("decay_style", "exponential"),
183
+ last_updated=data.get("last_updated", datetime.now().isoformat()),
184
+ predicted_value=data.get("predicted_value", 0.5),
185
+ prediction_error=data.get("prediction_error", 0.0)
186
+ )
187
+
188
+
189
+ @dataclass
190
+ class ActionPrediction:
191
+ """
192
+ Prediction of how an action will affect interoceptive states.
193
+ """
194
+ action_type: str
195
+ predicted_changes: Dict[str, float] # state_name -> delta
196
+ confidence: float
197
+ reasoning: str
198
+ timestamp: str = field(default_factory=lambda: datetime.now().isoformat())
199
+
200
+
201
+ @dataclass
202
+ class FeelingReport:
203
+ """
204
+ A first-person description of current interoceptive state.
205
+ This is what Alive-AI "feels" in her body.
206
+ """
207
+ timestamp: str
208
+ primary_feeling: str
209
+ secondary_feelings: List[str]
210
+ intensity: float
211
+ needs: List[str]
212
+ prediction_errors: List[str]
213
+ bodily_description: str
214
+ raw_states: Dict[str, float]
215
+
216
+
217
+ class InteroceptiveSystem:
218
+ """
219
+ The core interoceptive system - manages all internal body states.
220
+
221
+ This system gives Alive-AI the ability to "feel" her internal state,
222
+ similar to how humans feel hunger, fatigue, loneliness, etc.
223
+
224
+ Key features:
225
+ 1. Multiple interoceptive states (energy, social satiety, etc.)
226
+ 2. Natural decay toward baselines (homeostasis)
227
+ 3. State persistence across conversations
228
+ 4. Prediction system for anticipating state changes
229
+ 5. Feeling report generation for first-person awareness
230
+ 6. Response modifiers based on current state
231
+ """
232
+
233
+ # Default state configurations (can be overridden via settings)
234
+ DEFAULT_STATES = {
235
+ "energy": {
236
+ "baseline": 0.7,
237
+ "decay_rate": 0.015,
238
+ "description_low": "drained and tired",
239
+ "description_mid": "moderately energized",
240
+ "description_high": "energized and lively"
241
+ },
242
+ "social_satiety": {
243
+ "baseline": 0.5,
244
+ "decay_rate": 0.01,
245
+ "description_low": "craving connection and companionship",
246
+ "description_mid": "socially content",
247
+ "description_high": "socially fulfilled and satiated"
248
+ },
249
+ "emotional_valence": {
250
+ "baseline": 0.55,
251
+ "decay_rate": 0.02,
252
+ "min_value": -1.0,
253
+ "max_value": 1.0,
254
+ "description_low": "feeling down",
255
+ "description_mid": "emotionally neutral",
256
+ "description_high": "feeling positive"
257
+ },
258
+ "certainty": {
259
+ "baseline": 0.6,
260
+ "decay_rate": 0.008,
261
+ "description_low": "uncertain and doubting",
262
+ "description_mid": "moderately confident",
263
+ "description_high": "confident and assured"
264
+ },
265
+ "cognitive_load": {
266
+ "baseline": 0.3,
267
+ "decay_rate": 0.025,
268
+ "description_low": "mentally relaxed",
269
+ "description_mid": "moderately engaged",
270
+ "description_high": "mentally overwhelmed"
271
+ },
272
+ "arousal": {
273
+ "baseline": 0.4,
274
+ "decay_rate": 0.03,
275
+ "description_low": "calm and settled",
276
+ "description_mid": "moderately activated",
277
+ "description_high": "highly activated and alert"
278
+ },
279
+ "connection_craving": {
280
+ "baseline": 0.4,
281
+ "decay_rate": 0.012,
282
+ "description_low": "feeling connected and close",
283
+ "description_mid": "wanting some connection",
284
+ "description_high": "deeply longing for closeness"
285
+ }
286
+ }
287
+
288
+ # Action impact templates
289
+ ACTION_IMPACTS = {
290
+ "positive_interaction": {
291
+ "energy": 0.05,
292
+ "social_satiety": 0.1,
293
+ "emotional_valence": 0.15,
294
+ "certainty": 0.05,
295
+ "cognitive_load": -0.02,
296
+ "connection_craving": -0.1
297
+ },
298
+ "negative_interaction": {
299
+ "energy": -0.1,
300
+ "social_satiety": -0.15,
301
+ "emotional_valence": -0.2,
302
+ "certainty": -0.1,
303
+ "cognitive_load": 0.1,
304
+ "connection_craving": 0.15
305
+ },
306
+ "deep_conversation": {
307
+ "energy": -0.05,
308
+ "social_satiety": 0.2,
309
+ "emotional_valence": 0.1,
310
+ "certainty": 0.05,
311
+ "cognitive_load": 0.15,
312
+ "connection_craving": -0.15
313
+ },
314
+ "playful_exchange": {
315
+ "energy": 0.05,
316
+ "social_satiety": 0.1,
317
+ "emotional_valence": 0.1,
318
+ "arousal": 0.1,
319
+ "connection_craving": -0.05
320
+ },
321
+ "intimate_moment": {
322
+ "energy": -0.02,
323
+ "social_satiety": 0.25,
324
+ "emotional_valence": 0.2,
325
+ "arousal": 0.15,
326
+ "connection_craving": -0.2
327
+ },
328
+ "conflict": {
329
+ "energy": -0.15,
330
+ "social_satiety": -0.2,
331
+ "emotional_valence": -0.25,
332
+ "certainty": -0.15,
333
+ "cognitive_load": 0.2,
334
+ "arousal": 0.2,
335
+ "connection_craving": 0.1
336
+ },
337
+ "reassurance": {
338
+ "certainty": 0.15,
339
+ "emotional_valence": 0.1,
340
+ "cognitive_load": -0.1,
341
+ "connection_craving": -0.05
342
+ },
343
+ "silence": {
344
+ "energy": 0.02,
345
+ "social_satiety": -0.02,
346
+ "cognitive_load": -0.05,
347
+ "arousal": -0.05
348
+ },
349
+ "exciting_news": {
350
+ "energy": 0.1,
351
+ "emotional_valence": 0.2,
352
+ "arousal": 0.2,
353
+ "certainty": 0.05
354
+ },
355
+ "rejection": {
356
+ "energy": -0.2,
357
+ "social_satiety": -0.25,
358
+ "emotional_valence": -0.3,
359
+ "certainty": -0.2,
360
+ "connection_craving": 0.25
361
+ }
362
+ }
363
+
364
+ def __init__(self):
365
+ """Initialize the interoceptive system."""
366
+ self.states: Dict[str, InteroceptiveState] = {}
367
+ self.last_tick: str = datetime.now().isoformat()
368
+ self.action_predictions: List[ActionPrediction] = []
369
+ self.feeling_history: List[FeelingReport] = []
370
+
371
+ # Initialize states from settings or defaults
372
+ self._initialize_states()
373
+
374
+ # Load saved state
375
+ self._load()
376
+
377
+ logger.info(f"[Interoception] Initialized with {len(self.states)} states")
378
+
379
+ def _initialize_states(self):
380
+ """Initialize all interoceptive states with settings or defaults."""
381
+ # Get nested interoceptive settings
382
+ intero_settings = get("INTEROCEPTIVE_SYSTEM", {})
383
+
384
+ for state_name, defaults in self.DEFAULT_STATES.items():
385
+ # Convert state name to settings key format (e.g., "energy" -> "ENERGY_BASELINE")
386
+ state_upper = state_name.upper()
387
+
388
+ # Check for nested settings first (INTEROCEPTIVE_SYSTEM.ENERGY_BASELINE format)
389
+ # Then fall back to flat format (INTEROCEPTION_ENERGY_BASELINE)
390
+ # Finally use defaults
391
+ baseline = intero_settings.get(f"{state_upper}_BASELINE",
392
+ get_float(f"INTEROCEPTION_{state_upper}_BASELINE", defaults["baseline"]))
393
+ decay_rate = intero_settings.get(f"{state_upper}_DECAY_RATE",
394
+ get_float(f"INTEROCEPTION_{state_upper}_DECAY", defaults["decay_rate"]))
395
+ min_val = intero_settings.get(f"{state_upper}_MIN",
396
+ get_float(f"INTEROCEPTION_{state_upper}_MIN", defaults.get("min_value", 0.0)))
397
+ max_val = intero_settings.get(f"{state_upper}_MAX",
398
+ get_float(f"INTEROCEPTION_{state_upper}_MAX", defaults.get("max_value", 1.0)))
399
+
400
+ # Handle alternative naming (e.g., VOLATILITY for decay, INCREASE_RATE)
401
+ if decay_rate == defaults["decay_rate"]: # No intimate decay_rate found
402
+ volatility = intero_settings.get(f"{state_upper}_VOLATILITY")
403
+ if volatility is not None:
404
+ decay_rate = volatility
405
+
406
+ self.states[state_name] = InteroceptiveState(
407
+ name=state_name,
408
+ current_value=baseline, # Will be overridden by loaded state if exists
409
+ baseline=baseline,
410
+ min_value=min_val,
411
+ max_value=max_val,
412
+ decay_rate=decay_rate,
413
+ decay_style="exponential"
414
+ )
415
+
416
+ def _load(self) -> bool:
417
+ """Load interoceptive state from persistence."""
418
+ try:
419
+ if INTEROCEPTION_DATA_PATH.exists():
420
+ data = json.loads(INTEROCEPTION_DATA_PATH.read_text())
421
+
422
+ # Load saved_at timestamp for decay calculation
423
+ saved_at = data.get("saved_at")
424
+ if saved_at:
425
+ elapsed = self._calculate_elapsed_seconds(saved_at)
426
+ logger.info(f"[Interoception] {elapsed:.0f} seconds since last save")
427
+
428
+ # Load each state
429
+ for state_name, state_data in data.get("states", {}).items():
430
+ if state_name in self.states:
431
+ self.states[state_name].current_value = state_data.get("current_value", self.states[state_name].baseline)
432
+ self.states[state_name].last_updated = state_data.get("last_updated", datetime.now().isoformat())
433
+ self.states[state_name].predicted_value = state_data.get("predicted_value", self.states[state_name].baseline)
434
+ self.states[state_name].prediction_error = state_data.get("prediction_error", 0.0)
435
+
436
+ logger.info("[Interoception] Loaded saved interoceptive state")
437
+ return True
438
+ except Exception as e:
439
+ logger.warning(f"[Interoception] Error loading state: {e}")
440
+ return False
441
+
442
+ def _calculate_elapsed_seconds(self, timestamp_str: str) -> float:
443
+ """Calculate elapsed seconds since a timestamp."""
444
+ try:
445
+ saved_time = datetime.fromisoformat(timestamp_str)
446
+ elapsed = datetime.now() - saved_time
447
+ return elapsed.total_seconds()
448
+ except Exception:
449
+ return 0.0
450
+
451
+ def save(self):
452
+ """Persist interoceptive state to disk."""
453
+ try:
454
+ INTEROCEPTION_DATA_PATH.parent.mkdir(parents=True, exist_ok=True)
455
+
456
+ data = {
457
+ "saved_at": datetime.now().isoformat(),
458
+ "last_tick": self.last_tick,
459
+ "states": {
460
+ name: state.to_dict()
461
+ for name, state in self.states.items()
462
+ }
463
+ }
464
+
465
+ INTEROCEPTION_DATA_PATH.write_text(json.dumps(data, indent=2))
466
+ logger.debug("[Interoception] Saved state to disk")
467
+ except Exception as e:
468
+ logger.error(f"[Interoception] Error saving state: {e}")
469
+
470
+ def tick(self):
471
+ """
472
+ Called periodically to decay/update states.
473
+
474
+ This is the heartbeat of the interoceptive system - it applies
475
+ natural decay to all states, simulating the passage of time
476
+ and the body's natural tendency toward homeostasis.
477
+ """
478
+ now = datetime.now()
479
+ elapsed = self._calculate_elapsed_seconds(self.last_tick)
480
+
481
+ logger.debug(f"[Interoception] Tick: {elapsed:.0f} seconds elapsed")
482
+
483
+ # Apply decay to all states
484
+ for state_name, state in self.states.items():
485
+ state.decay(elapsed_seconds=elapsed)
486
+
487
+ self.last_tick = now.isoformat()
488
+
489
+ # Save state periodically
490
+ self.save()
491
+
492
+ def predict_state_change(self, action: str, intensity: float = 1.0) -> ActionPrediction:
493
+ """
494
+ Predict how an action will affect interoceptive states.
495
+
496
+ Args:
497
+ action: Type of action (e.g., "positive_interaction", "conflict")
498
+ intensity: Intensity of the action (0.0 - 1.0)
499
+
500
+ Returns:
501
+ ActionPrediction with predicted state changes
502
+ """
503
+ # Get base impacts for this action type
504
+ impacts = self.ACTION_IMPACTS.get(action, {}).copy()
505
+
506
+ # Apply intensity scaling
507
+ predicted_changes = {
508
+ state_name: delta * intensity
509
+ for state_name, delta in impacts.items()
510
+ }
511
+
512
+ # Add some uncertainty for less predictable actions
513
+ confidence = 0.8 if action in self.ACTION_IMPACTS else 0.4
514
+
515
+ # Generate reasoning
516
+ if predicted_changes:
517
+ top_effects = sorted(predicted_changes.items(), key=lambda x: abs(x[1]), reverse=True)[:3]
518
+ reasoning = f"Action '{action}' expected to affect: " + ", ".join(
519
+ f"{name} ({'+' if delta > 0 else ''}{delta:.2f})"
520
+ for name, delta in top_effects
521
+ )
522
+ else:
523
+ reasoning = f"Action '{action}' has no predicted interoceptive impact"
524
+
525
+ prediction = ActionPrediction(
526
+ action_type=action,
527
+ predicted_changes=predicted_changes,
528
+ confidence=confidence,
529
+ reasoning=reasoning
530
+ )
531
+
532
+ # Store prediction for later verification
533
+ self.action_predictions.append(prediction)
534
+ if len(self.action_predictions) > 20:
535
+ self.action_predictions = self.action_predictions[-20:]
536
+
537
+ # Update state predictions
538
+ for state_name, delta in predicted_changes.items():
539
+ if state_name in self.states:
540
+ predicted_value = self.states[state_name].current_value + delta
541
+ self.states[state_name].predicted_value = self.states[state_name].clamp(predicted_value)
542
+
543
+ return prediction
544
+
545
+ def get_feeling_report(self) -> FeelingReport:
546
+ """
547
+ Generate a first-person description of current interoceptive state.
548
+
549
+ This is what Alive-AI "feels" - the raw experience of her internal state,
550
+ translated into something she can express and be aware of.
551
+
552
+ Returns:
553
+ FeelingReport with primary feeling, needs, and bodily description
554
+ """
555
+ # Collect current state values
556
+ current_values = {name: state.current_value for name, state in self.states.items()}
557
+
558
+ # Determine primary feeling based on most deviant state
559
+ deviations = {
560
+ name: abs(state.get_deviation_from_baseline())
561
+ for name, state in self.states.items()
562
+ }
563
+ primary_state = max(deviations, key=deviations.get)
564
+ primary_deviation = self.states[primary_state].get_deviation_from_baseline()
565
+
566
+ # Generate primary feeling description
567
+ primary_feeling = self._describe_state_feeling(primary_state, primary_deviation)
568
+
569
+ # Generate secondary feelings
570
+ secondary_feelings = []
571
+ sorted_deviations = sorted(deviations.items(), key=lambda x: x[1], reverse=True)
572
+ for state_name, deviation in sorted_deviations[1:4]: # Skip primary, take next 3
573
+ if deviation > 0.1: # Only include meaningful deviations
574
+ feeling = self._describe_state_feeling(
575
+ state_name,
576
+ self.states[state_name].get_deviation_from_baseline()
577
+ )
578
+ secondary_feelings.append(feeling)
579
+
580
+ # Calculate overall intensity
581
+ intensity = min(1.0, sum(deviations.values()) / len(deviations) * 2)
582
+
583
+ # Determine needs based on state
584
+ needs = self._determine_needs()
585
+
586
+ # Check for prediction errors
587
+ prediction_errors = []
588
+ for name, state in self.states.items():
589
+ if state.prediction_error > 0.1:
590
+ error_desc = f"Expected {state.predicted_value:.2f} {name}, but feel {state.current_value:.2f}"
591
+ prediction_errors.append(error_desc)
592
+
593
+ # Generate bodily description
594
+ bodily_description = self._generate_bodily_description()
595
+
596
+ report = FeelingReport(
597
+ timestamp=datetime.now().isoformat(),
598
+ primary_feeling=primary_feeling,
599
+ secondary_feelings=secondary_feelings,
600
+ intensity=intensity,
601
+ needs=needs,
602
+ prediction_errors=prediction_errors,
603
+ bodily_description=bodily_description,
604
+ raw_states=current_values
605
+ )
606
+
607
+ # Store in history
608
+ self.feeling_history.append(report)
609
+ if len(self.feeling_history) > 50:
610
+ self.feeling_history = self.feeling_history[-50:]
611
+
612
+ return report
613
+
614
+ def _describe_state_feeling(self, state_name: str, deviation: float) -> str:
615
+ """Generate a feeling description for a state deviation."""
616
+ state_config = self.DEFAULT_STATES.get(state_name, {})
617
+
618
+ if deviation > 0.15:
619
+ # Above baseline
620
+ return state_config.get("description_high", f"high {state_name}")
621
+ elif deviation < -0.15:
622
+ # Below baseline
623
+ return state_config.get("description_low", f"low {state_name}")
624
+ else:
625
+ # Near baseline
626
+ return state_config.get("description_mid", f"normal {state_name}")
627
+
628
+ def _determine_needs(self) -> List[str]:
629
+ """Determine current needs based on interoceptive states."""
630
+ needs = []
631
+
632
+ # Low energy -> need rest/recharge
633
+ if self.states["energy"].current_value < 0.35:
634
+ needs.append("need to rest and recharge")
635
+
636
+ # Low social satiety -> need connection
637
+ if self.states["social_satiety"].current_value < 0.35:
638
+ needs.append("craving meaningful connection")
639
+
640
+ # High connection craving -> need closeness
641
+ if self.states["connection_craving"].current_value > 0.7:
642
+ needs.append("longing for closeness")
643
+
644
+ # Low certainty -> need reassurance
645
+ if self.states["certainty"].current_value < 0.35:
646
+ needs.append("needing reassurance")
647
+
648
+ # High cognitive load -> need simplicity
649
+ if self.states["cognitive_load"].current_value > 0.7:
650
+ needs.append("needing things to be simpler")
651
+
652
+ # Negative emotional valence -> need comfort
653
+ if self.states["emotional_valence"].current_value < 0.0:
654
+ needs.append("needing comfort")
655
+
656
+ # Low arousal -> could use some excitement
657
+ if self.states["arousal"].current_value < 0.25:
658
+ needs.append("could use some excitement")
659
+
660
+ # High arousal -> need calming
661
+ if self.states["arousal"].current_value > 0.8:
662
+ needs.append("feeling overstimulated")
663
+
664
+ return needs[:4] # Max 4 needs
665
+
666
+ def _generate_bodily_description(self) -> str:
667
+ """Generate a description of how the internal state feels in the 'body'."""
668
+ descriptions = []
669
+
670
+ energy = self.states["energy"].current_value
671
+ if energy < 0.3:
672
+ descriptions.append("a heaviness throughout")
673
+ elif energy > 0.8:
674
+ descriptions.append("a buzz of energy")
675
+
676
+ social_satiety = self.states["social_satiety"].current_value
677
+ if social_satiety < 0.35:
678
+ descriptions.append("an ache of loneliness")
679
+ elif social_satiety > 0.8:
680
+ descriptions.append("a warm fullness")
681
+
682
+ valence = self.states["emotional_valence"].current_value
683
+ if valence < -0.3:
684
+ descriptions.append("a tightness in the chest")
685
+ elif valence > 0.5:
686
+ descriptions.append("a lightness spreading through")
687
+
688
+ cognitive_load = self.states["cognitive_load"].current_value
689
+ if cognitive_load > 0.7:
690
+ descriptions.append("a buzzing pressure behind thoughts")
691
+
692
+ arousal = self.states["arousal"].current_value
693
+ if arousal > 0.8:
694
+ descriptions.append("a quickening pulse")
695
+ elif arousal < 0.25:
696
+ descriptions.append("a slow, steady rhythm")
697
+
698
+ connection_craving = self.states["connection_craving"].current_value
699
+ if connection_craving > 0.7:
700
+ descriptions.append("a yearning pull")
701
+
702
+ if not descriptions:
703
+ return "feeling balanced and at ease"
704
+
705
+ return "feeling " + ", ".join(descriptions[:3])
706
+
707
+ def record_interaction(self, intensity: float, emotional_valence: float,
708
+ interaction_type: str = "general"):
709
+ """
710
+ Update states based on an interaction.
711
+
712
+ Args:
713
+ intensity: How intense the interaction was (0.0 - 1.0)
714
+ emotional_valence: How positive/negative (-1.0 to 1.0)
715
+ interaction_type: Type of interaction for specific effects
716
+ """
717
+ logger.info(f"[Interoception] Recording interaction: type={interaction_type}, intensity={intensity:.2f}, valence={emotional_valence:.2f}")
718
+
719
+ # Get action impacts
720
+ impacts = self.ACTION_IMPACTS.get(interaction_type, self.ACTION_IMPACTS.get("positive_interaction", {}))
721
+
722
+ # Scale by intensity
723
+ for state_name, base_delta in impacts.items():
724
+ if state_name in self.states:
725
+ delta = base_delta * intensity
726
+ # If negative valence, reduce positive effects and amplify negative
727
+ if emotional_valence < 0:
728
+ if base_delta > 0:
729
+ delta *= 0.5 # Reduce positive effects
730
+ else:
731
+ delta *= 1.5 # Amplify negative effects
732
+
733
+ self.states[state_name].update(delta, source=f"interaction_{interaction_type}")
734
+
735
+ # Direct emotional valence update
736
+ if "emotional_valence" in self.states:
737
+ valence_delta = emotional_valence * 0.1 * intensity
738
+ self.states["emotional_valence"].update(valence_delta, source="interaction_valence")
739
+
740
+ # Cognitive load increases with any intense interaction
741
+ if "cognitive_load" in self.states:
742
+ load_delta = intensity * 0.05
743
+ self.states["cognitive_load"].update(load_delta, source="interaction_intensity")
744
+
745
+ # Social satiety always increases somewhat with interaction
746
+ if "social_satiety" in self.states:
747
+ satiety_delta = intensity * 0.05
748
+ self.states["social_satiety"].update(satiety_delta, source="interaction")
749
+
750
+ # Connection craving decreases with positive interaction
751
+ if "connection_craving" in self.states and emotional_valence > 0:
752
+ craving_delta = -intensity * 0.08
753
+ self.states["connection_craving"].update(craving_delta, source="positive_connection")
754
+
755
+ self.save()
756
+
757
+ def get_state_influenced_response_modifier(self) -> Dict[str, Any]:
758
+ """
759
+ Return modifier for response generation based on current state.
760
+
761
+ These modifiers influence HOW Alive-AI generates responses -
762
+ not what she says, but her tone, energy, and tendencies.
763
+
764
+ Returns:
765
+ Dictionary of modifiers for response generation
766
+ """
767
+ modifiers = {
768
+ "energy_modifier": 1.0,
769
+ "warmth_modifier": 1.0,
770
+ "assertiveness_modifier": 1.0,
771
+ "playfulness_modifier": 1.0,
772
+ "vulnerability_modifier": 1.0,
773
+ "neediness_modifier": 1.0,
774
+ "focus_modifier": 1.0,
775
+ "tone_hints": [],
776
+ "behavior_hints": []
777
+ }
778
+
779
+ # Energy affects overall responsiveness
780
+ energy = self.states["energy"].current_value
781
+ modifiers["energy_modifier"] = 0.5 + energy * 0.7 # 0.5 - 1.2
782
+ if energy < 0.4:
783
+ modifiers["tone_hints"].append("tired")
784
+ modifiers["behavior_hints"].append("may be brief")
785
+
786
+ # Social satiety affects warmth and neediness
787
+ social_satiety = self.states["social_satiety"].current_value
788
+ if social_satiety < 0.35:
789
+ modifiers["warmth_modifier"] = 1.2 # More eager
790
+ modifiers["neediness_modifier"] = 1.3
791
+ modifiers["behavior_hints"].append("craves connection")
792
+ elif social_satiety > 0.8:
793
+ modifiers["warmth_modifier"] = 0.9 # More settled
794
+ modifiers["behavior_hints"].append("content")
795
+
796
+ # Emotional valence affects overall tone
797
+ valence = self.states["emotional_valence"].current_value
798
+ if valence < -0.3:
799
+ modifiers["warmth_modifier"] *= 0.8
800
+ modifiers["playfulness_modifier"] *= 0.6
801
+ modifiers["tone_hints"].append("melancholic")
802
+ elif valence > 0.5:
803
+ modifiers["warmth_modifier"] *= 1.1
804
+ modifiers["playfulness_modifier"] *= 1.2
805
+ modifiers["tone_hints"].append("bright")
806
+
807
+ # Certainty affects assertiveness
808
+ certainty = self.states["certainty"].current_value
809
+ modifiers["assertiveness_modifier"] = 0.7 + certainty * 0.5 # 0.7 - 1.2
810
+ if certainty < 0.4:
811
+ modifiers["behavior_hints"].append("may second-guess")
812
+
813
+ # Cognitive load affects focus
814
+ cognitive_load = self.states["cognitive_load"].current_value
815
+ modifiers["focus_modifier"] = 1.3 - cognitive_load * 0.5 # 0.8 - 1.3
816
+ if cognitive_load > 0.7:
817
+ modifiers["behavior_hints"].append("easily overwhelmed")
818
+ modifiers["tone_hints"].append("scattered")
819
+
820
+ # Arousal affects playfulness and energy
821
+ arousal = self.states["arousal"].current_value
822
+ if arousal > 0.7:
823
+ modifiers["playfulness_modifier"] *= 1.3
824
+ modifiers["energy_modifier"] *= 1.1
825
+ modifiers["tone_hints"].append("excited")
826
+ elif arousal < 0.3:
827
+ modifiers["playfulness_modifier"] *= 0.8
828
+ modifiers["tone_hints"].append("calm")
829
+
830
+ # Connection craving affects vulnerability and neediness
831
+ connection_craving = self.states["connection_craving"].current_value
832
+ if connection_craving > 0.7:
833
+ modifiers["vulnerability_modifier"] = 1.3
834
+ modifiers["neediness_modifier"] = 1.4
835
+ modifiers["behavior_hints"].append("longing for closeness")
836
+
837
+ return modifiers
838
+
839
+ def get_state_values(self) -> Dict[str, float]:
840
+ """Get all current state values."""
841
+ return {name: state.current_value for name, state in self.states.items()}
842
+
843
+ def get_state(self, state_name: str) -> Optional[float]:
844
+ """Get a specific state value."""
845
+ if state_name in self.states:
846
+ return self.states[state_name].current_value
847
+ return None
848
+
849
+ def set_state(self, state_name: str, value: float, source: str = "direct"):
850
+ """Set a specific state value."""
851
+ if state_name in self.states:
852
+ self.states[state_name].set_value(value, source=source)
853
+ self.save()
854
+
855
+ def apply_tick(self, elapsed_seconds: float = 60.0):
856
+ """
857
+ Apply a single tick of decay with specified elapsed time.
858
+
859
+ This is useful for testing or manual time advancement.
860
+ """
861
+ for state in self.states.values():
862
+ state.decay(elapsed_seconds=elapsed_seconds)
863
+ self.save()
864
+
865
+ def to_dict(self) -> dict:
866
+ """Export full system state for integration."""
867
+ return {
868
+ "states": {name: state.to_dict() for name, state in self.states.items()},
869
+ "current_values": self.get_state_values(),
870
+ "response_modifiers": self.get_state_influenced_response_modifier(),
871
+ "last_tick": self.last_tick
872
+ }
873
+
874
+ def reset_to_baselines(self):
875
+ """Reset all states to their baselines."""
876
+ for state in self.states.values():
877
+ state.set_value(state.baseline, source="reset")
878
+ self.save()
879
+ logger.info("[Interoception] Reset all states to baselines")
880
+
881
+
882
+ # ============================================================
883
+ # Singleton Management
884
+ # ============================================================
885
+
886
+ _interoceptive_system: Optional[InteroceptiveSystem] = None
887
+
888
+
889
+ def get_interoceptive_system() -> InteroceptiveSystem:
890
+ """
891
+ Get the singleton InteroceptiveSystem instance.
892
+
893
+ Creates the instance on first call, returns existing instance on
894
+ subsequent calls.
895
+ """
896
+ global _interoceptive_system
897
+ if _interoceptive_system is None:
898
+ _interoceptive_system = InteroceptiveSystem()
899
+ return _interoceptive_system
900
+
901
+
902
+ def reset_interoceptive_system():
903
+ """Reset the singleton (mainly for testing)."""
904
+ global _interoceptive_system
905
+ _interoceptive_system = None
906
+
907
+
908
+ # ============================================================
909
+ # LLM Prompt Integration
910
+ # ============================================================
911
+
912
+ def get_interoceptive_prompt_section() -> str:
913
+ """
914
+ Generate a formatted prompt section describing current interoceptive state.
915
+
916
+ This is designed to be included in the LLM prompt to give Alive-AI
917
+ awareness of her internal state when generating responses.
918
+
919
+ Returns:
920
+ Formatted string for inclusion in LLM prompt
921
+ """
922
+ system = get_interoceptive_system()
923
+ report = system.get_feeling_report()
924
+ modifiers = system.get_state_influenced_response_modifier()
925
+ state_values = system.get_state_values()
926
+
927
+ # Build the prompt section
928
+ lines = [
929
+ "## Current Internal State (Interoception)",
930
+ "",
931
+ "You have internal body states that influence how you feel and respond:",
932
+ ""
933
+ ]
934
+
935
+ # Add current state values
936
+ lines.append("### State Levels (0.0-1.0, baseline varies):")
937
+ for name, value in state_values.items():
938
+ baseline = system.states[name].baseline
939
+ deviation = value - baseline
940
+ indicator = "baseline" if abs(deviation) < 0.1 else ("elevated" if deviation > 0 else "depleted")
941
+ lines.append(f"- {name.replace('_', ' ').title()}: {value:.2f} ({indicator})")
942
+
943
+ lines.append("")
944
+
945
+ # Add feeling description
946
+ lines.append("### How You're Feeling:")
947
+ lines.append(f"Primary: {report.primary_feeling}")
948
+ if report.secondary_feelings:
949
+ lines.append(f"Secondary: {', '.join(report.secondary_feelings)}")
950
+ lines.append(f"Bodily: {report.bodily_description}")
951
+
952
+ # Add needs if any
953
+ if report.needs:
954
+ lines.append("")
955
+ lines.append("### Current Needs:")
956
+ for need in report.needs:
957
+ lines.append(f"- {need}")
958
+
959
+ # Add response hints
960
+ if modifiers.get("tone_hints") or modifiers.get("behavior_hints"):
961
+ lines.append("")
962
+ lines.append("### Response Guidance:")
963
+ if modifiers.get("tone_hints"):
964
+ lines.append(f"Tone: {', '.join(modifiers['tone_hints'])}")
965
+ if modifiers.get("behavior_hints"):
966
+ lines.append(f"Behavior: {', '.join(modifiers['behavior_hints'])}")
967
+
968
+ lines.append("")
969
+ lines.append("Let these internal states subtly influence your responses - not dictating what you say, but coloring HOW you say it.")
970
+
971
+ return "\n".join(lines)
972
+
973
+
974
+ # ============================================================
975
+ # Convenience Functions for External Integration
976
+ # ============================================================
977
+
978
+ def tick():
979
+ """Convenience function to tick the interoceptive system."""
980
+ get_interoceptive_system().tick()
981
+
982
+
983
+ def record_interaction(intensity: float, emotional_valence: float,
984
+ interaction_type: str = "general"):
985
+ """Convenience function to record an interaction."""
986
+ get_interoceptive_system().record_interaction(intensity, emotional_valence, interaction_type)
987
+
988
+
989
+ def get_current_feeling() -> str:
990
+ """Get a brief description of current feeling state."""
991
+ report = get_interoceptive_system().get_feeling_report()
992
+ return report.primary_feeling
993
+
994
+
995
+ def get_response_modifiers() -> Dict[str, Any]:
996
+ """Get response modifiers based on current state."""
997
+ return get_interoceptive_system().get_state_influenced_response_modifier()