alive-ai 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/Dockerfile +24 -0
- package/LICENSE +21 -0
- package/README.md +143 -0
- package/alive_ai/__init__.py +3 -0
- package/brain/__init__.py +59 -0
- package/brain/almost_said.py +154 -0
- package/brain/bid_detector.py +636 -0
- package/brain/conversation_flow.py +135 -0
- package/brain/curiosity.py +328 -0
- package/brain/default_mode.py +1438 -0
- package/brain/dreams.py +220 -0
- package/brain/embeddings/__init__.py +82 -0
- package/brain/emotional_memory.py +949 -0
- package/brain/global_activity.py +173 -0
- package/brain/group_dynamics.py +63 -0
- package/brain/linguistic.py +235 -0
- package/brain/llm/__init__.py +63 -0
- package/brain/llm/base.py +33 -0
- package/brain/llm/fallback_router.py +309 -0
- package/brain/llm/manifest.md +30 -0
- package/brain/llm/ollama.py +218 -0
- package/brain/llm/openrouter.py +151 -0
- package/brain/llm/provider.py +205 -0
- package/brain/llm/unified.py +423 -0
- package/brain/llm/zai.py +169 -0
- package/brain/manifest.md +23 -0
- package/brain/memory/__init__.py +123 -0
- package/brain/memory/episodic.py +92 -0
- package/brain/memory/fact_extractor.py +209 -0
- package/brain/memory/index.py +54 -0
- package/brain/memory/manager.py +151 -0
- package/brain/memory/summarizer.py +102 -0
- package/brain/memory/vector_store.py +297 -0
- package/brain/memory/working.py +43 -0
- package/brain/narrative.py +343 -0
- package/brain/stt/__init__.py +4 -0
- package/brain/stt/google_stt.py +83 -0
- package/brain/stt/whisper_stt.py +82 -0
- package/brain/subconscious/__init__.py +33 -0
- package/brain/subconscious/actions.py +136 -0
- package/brain/subconscious/evaluation.py +166 -0
- package/brain/subconscious/goal_system.py +90 -0
- package/brain/subconscious/goals.py +41 -0
- package/brain/subconscious/impulse_generator.py +200 -0
- package/brain/subconscious/impulses.py +48 -0
- package/brain/subconscious/learning.py +24 -0
- package/brain/subconscious/learning_system.py +79 -0
- package/brain/subconscious/loop.py +398 -0
- package/brain/subconscious/manifest.md +32 -0
- package/brain/subconscious/relationship.py +47 -0
- package/brain/subconscious/relationship_memory.py +83 -0
- package/brain/subconscious/response_analyzer.py +74 -0
- package/brain/subconscious/templates.py +70 -0
- package/brain/subconscious/thought.py +37 -0
- package/brain/subconscious/working_memory.py +97 -0
- package/cli/index.js +371 -0
- package/config/directives.example.json +28 -0
- package/config/instructions.example.md +16 -0
- package/config/self.example.json +74 -0
- package/config/settings.example.json +95 -0
- package/core/__init__.py +1 -0
- package/core/config.py +54 -0
- package/core/directives.py +198 -0
- package/core/events.py +50 -0
- package/core/follow_up.py +267 -0
- package/core/hot_reload.py +174 -0
- package/core/initialization.py +253 -0
- package/core/manifest.md +28 -0
- package/core/media_handler.py +241 -0
- package/core/memory_monitor.py +200 -0
- package/core/message_handler.py +1440 -0
- package/core/proactive_generator.py +277 -0
- package/core/self.py +188 -0
- package/core/settings.py +169 -0
- package/core/skills_registry.py +357 -0
- package/core/state.py +27 -0
- package/core/subconscious_bridge.py +93 -0
- package/core/thinking.py +175 -0
- package/core/user_manager.py +306 -0
- package/core/user_tracker.py +144 -0
- package/demo/index.html +144 -0
- package/docker-compose.yml +28 -0
- package/docs/assets/logo.svg +15 -0
- package/docs/index.html +355 -0
- package/heart/__init__.py +93 -0
- package/heart/afterglow.py +215 -0
- package/heart/attachment.py +186 -0
- package/heart/circadian.py +251 -0
- package/heart/complex_emotions.py +114 -0
- package/heart/conflicts.py +589 -0
- package/heart/core.py +387 -0
- package/heart/emotional_decay.py +59 -0
- package/heart/emotional_memory.py +261 -0
- package/heart/emotional_state.py +146 -0
- package/heart/emotional_variability.py +156 -0
- package/heart/hormonal.py +424 -0
- package/heart/inconsistency.py +1222 -0
- package/heart/integrity.py +469 -0
- package/heart/interoception.py +997 -0
- package/heart/love.py +120 -0
- package/heart/manifest.md +25 -0
- package/heart/mood_shifts.py +169 -0
- package/heart/phantom_somatic.py +259 -0
- package/heart/predictive.py +374 -0
- package/heart/scars.py +474 -0
- package/heart/somatic.py +482 -0
- package/heart/soul.py +633 -0
- package/heart/telemetry.py +942 -0
- package/heart/triggers.py +119 -0
- package/heart/unconscious.py +443 -0
- package/input/__init__.py +1 -0
- package/input/manifest.md +24 -0
- package/input/telegram/__init__.py +1 -0
- package/input/telegram/commands.py +762 -0
- package/input/telegram/listener.py +532 -0
- package/main.py +90 -0
- package/manifest.md +28 -0
- package/mypics/.gitkeep +1 -0
- package/myvids/.gitkeep +1 -0
- package/output/__init__.py +1 -0
- package/output/images/__init__.py +1 -0
- package/output/images/fal_gen.py +43 -0
- package/output/manifest.md +26 -0
- package/output/text/__init__.py +1 -0
- package/output/text/sender.py +22 -0
- package/output/voice/__init__.py +64 -0
- package/output/voice/google_tts.py +252 -0
- package/output/voice/gtts_tts.py +214 -0
- package/output/voice/vibe_tts.py +190 -0
- package/package.json +58 -0
- package/pyproject.toml +23 -0
- package/requirements.txt +21 -0
- package/skills/__init__.py +1 -0
- package/skills/anticipation_engine/__init__.py +8 -0
- package/skills/anticipation_engine/engine.py +618 -0
- package/skills/anticipation_engine/manifest.md +192 -0
- package/skills/calendar/__init__.py +1 -0
- package/skills/content_unlocks/__init__.py +8 -0
- package/skills/content_unlocks/manifest.md +231 -0
- package/skills/content_unlocks/unlocks.py +945 -0
- package/skills/exclusive_moments/__init__.py +8 -0
- package/skills/exclusive_moments/manifest.md +145 -0
- package/skills/exclusive_moments/moments.py +506 -0
- package/skills/intimacy_layers/__init__.py +8 -0
- package/skills/intimacy_layers/layers.py +703 -0
- package/skills/intimacy_layers/manifest.md +203 -0
- package/skills/manifest.md +67 -0
- package/skills/memory_callbacks/__init__.py +9 -0
- package/skills/memory_callbacks/callbacks.py +748 -0
- package/skills/memory_callbacks/manifest.md +170 -0
- package/skills/message_scheduler/__init__.py +19 -0
- package/skills/message_scheduler/manifest.md +107 -0
- package/skills/message_scheduler/scheduler.py +510 -0
- package/skills/photo_manager/__init__.py +1 -0
- package/skills/photo_manager/scanner.py +296 -0
- package/skills/relationship_milestones/__init__.py +8 -0
- package/skills/relationship_milestones/manifest.md +206 -0
- package/skills/relationship_milestones/tracker.py +494 -0
- package/skills/self_authorship/__init__.py +23 -0
- package/skills/self_authorship/author.py +331 -0
- package/skills/self_authorship/manifest.md +24 -0
- package/skills/video_manager/__init__.py +5 -0
- package/skills/video_manager/manifest.md +37 -0
- package/skills/video_manager/scanner.py +229 -0
- package/webui/__init__.py +3 -0
- package/webui/app.py +936 -0
- package/webui/bridge.py +366 -0
- package/webui/static/index.html +2070 -0
|
@@ -0,0 +1,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()
|