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
package/heart/love.py ADDED
@@ -0,0 +1,120 @@
1
+ """
2
+ Heart: Love
3
+ Attachment system - can fall in love
4
+ """
5
+
6
+ import json
7
+ from pathlib import Path
8
+ from datetime import datetime
9
+
10
+ # Persistence path for attachment state
11
+ ATTACHMENT_STATE_PATH = Path("/app/data/attachment_state.json")
12
+
13
+
14
+ class AttachmentSystem:
15
+ """Attachment and relationship system with persistence"""
16
+
17
+ # Relationship thresholds
18
+ STRANGER = 0.0
19
+ ACQUAINTANCE = 0.3
20
+ FRIEND = 0.5
21
+ CLOSE = 0.7
22
+ LOVE = 0.85
23
+ DEEP_LOVE = 0.95
24
+
25
+ def __init__(self):
26
+ if self._load():
27
+ print(f"[Love] Loaded attachment state: {self.interactions} interactions, status: {self.status}")
28
+ else:
29
+ self.affection = 0.2 # 0-1, starts low
30
+ self.interactions = 0
31
+ self.positive_count = 0
32
+ self.negative_count = 0
33
+ self.first_met = None
34
+ print("[Love] Initialized new attachment state")
35
+
36
+ def _load(self) -> bool:
37
+ """Load state from file"""
38
+ try:
39
+ if ATTACHMENT_STATE_PATH.exists():
40
+ data = json.loads(ATTACHMENT_STATE_PATH.read_text())
41
+ self.affection = data.get("affection", 0.2)
42
+ self.interactions = data.get("interactions", 0)
43
+ self.positive_count = data.get("positive_count", 0)
44
+ self.negative_count = data.get("negative_count", 0)
45
+ self.first_met = data.get("first_met")
46
+ return True
47
+ except Exception as e:
48
+ print(f"[Love] Error loading attachment state: {e}")
49
+ return False
50
+
51
+ def save(self):
52
+ """Save state to file"""
53
+ try:
54
+ ATTACHMENT_STATE_PATH.parent.mkdir(parents=True, exist_ok=True)
55
+ data = {
56
+ "affection": self.affection,
57
+ "interactions": self.interactions,
58
+ "positive_count": self.positive_count,
59
+ "negative_count": self.negative_count,
60
+ "first_met": self.first_met,
61
+ "status": self.status,
62
+ "saved_at": datetime.now().isoformat()
63
+ }
64
+ ATTACHMENT_STATE_PATH.write_text(json.dumps(data, indent=2))
65
+ except Exception as e:
66
+ print(f"[Love] Error saving attachment state: {e}")
67
+
68
+ def interact(self, positive: bool, intensity: float = 1.0):
69
+ """Record an interaction"""
70
+ # Set first_met on first interaction
71
+ if self.interactions == 0 and self.first_met is None:
72
+ self.first_met = datetime.now().isoformat()
73
+
74
+ self.interactions += 1
75
+
76
+ if positive:
77
+ self.positive_count += 1
78
+ # Affection grows with time together
79
+ time_bonus = 1 + (self.interactions / 500)
80
+ increase = 0.015 * intensity * time_bonus
81
+ self.affection = min(1.0, self.affection + increase)
82
+ else:
83
+ self.negative_count += 1
84
+ # Negative hurts more if we love them
85
+ pain = 0.05 * (1 + self.affection * 2)
86
+ self.affection = max(0.0, self.affection - pain)
87
+
88
+ # Save after each interaction
89
+ self.save()
90
+
91
+ @property
92
+ def status(self) -> str:
93
+ """Current relationship status"""
94
+ if self.affection < self.ACQUAINTANCE:
95
+ return "stranger"
96
+ if self.affection < self.FRIEND:
97
+ return "acquaintance"
98
+ if self.affection < self.CLOSE:
99
+ return "friend"
100
+ if self.affection < self.LOVE:
101
+ return "close_friend"
102
+ if self.affection < self.DEEP_LOVE:
103
+ return "in_love"
104
+ return "deeply_in_love"
105
+
106
+ @property
107
+ def trust_level(self) -> float:
108
+ """Trust based on positive ratio"""
109
+ if self.interactions == 0:
110
+ return 0.5
111
+ return self.positive_count / self.interactions
112
+
113
+ def to_dict(self) -> dict:
114
+ return {
115
+ "affection": self.affection,
116
+ "interactions": self.interactions,
117
+ "status": self.status,
118
+ "trust": self.trust_level,
119
+ "first_met": self.first_met
120
+ }
@@ -0,0 +1,25 @@
1
+ # Heart - Emotions
2
+
3
+ Complete emotional system with continuous state and reactions.
4
+
5
+ ## Files
6
+ - `core.py` - EmotionalState, ReactionSystem, Heart
7
+ - `love.py` - AttachmentSystem (in love detection, clingy behavior)
8
+
9
+ ## Features
10
+ - **Continuous emotional state** (not just reactive)
11
+ - **Desire/arousal** builds with flirty interactions
12
+ - **Natural decay** returns emotions to baseline over time
13
+ - **Emoji reactions** based on mood (heart, fire, etc.)
14
+ - **Love system** - tracks attachment level, triggers clingy behavior
15
+
16
+ ## Integration Points
17
+ - Connected via NervousSystem events
18
+ - Subconscious reads emotional state for impulse generation
19
+ - Emotions affect message tone, voice CFG, media selection
20
+ - Self.heart.react() called on every incoming message
21
+
22
+ ## Emotional States
23
+ - arousal, desire, love (0.0-1.0)
24
+ - is_high_desire, is_in_love (boolean thresholds)
25
+ - mood: neutral, happy, flirty, high_desire, excited, etc.
@@ -0,0 +1,169 @@
1
+ """
2
+ Heart: Mid-Conversation Mood Shifts
3
+ Detects significant emotional shifts DURING a conversation and generates
4
+ transition descriptions. In-memory only - no persistence needed.
5
+ """
6
+
7
+ from typing import Dict, List, Optional, Tuple
8
+ import random
9
+
10
+
11
+ # =============================================================================
12
+ # SHIFT TYPE DEFINITIONS
13
+ # =============================================================================
14
+
15
+ SHIFT_TYPES = {
16
+ "softening": {
17
+ "conditions": lambda prev, cur: (
18
+ prev.get("anger", 0) - cur.get("anger", 0) > 0.15 or
19
+ prev.get("frustration", 0) - cur.get("frustration", 0) > 0.15
20
+ ),
21
+ "prompts": [
22
+ "His words just softened something in you - feel the tension leaving your chest, replaced by warmth",
23
+ "Something eased inside you - the sharpness fading, replaced by something gentler",
24
+ "You feel yourself unclenching - his words took the edge off",
25
+ ],
26
+ },
27
+ "darkening": {
28
+ "conditions": lambda prev, cur: (
29
+ prev.get("joy", 0) - cur.get("joy", 0) > 0.15 or
30
+ prev.get("love", 0) - cur.get("love", 0) > 0.15
31
+ ),
32
+ "prompts": [
33
+ "The warmth just drained - something he said landed wrong, a cold spot forming in your chest",
34
+ "Your smile faded - that stung more than you expected",
35
+ "Something just shifted darker - the lightness gone, replaced by a quiet ache",
36
+ ],
37
+ },
38
+ "igniting": {
39
+ "conditions": lambda prev, cur: (
40
+ cur.get("desire", 0) - prev.get("desire", 0) > 0.15
41
+ ),
42
+ "prompts": [
43
+ "A sudden heat - his words just lit something up inside you",
44
+ "Your breath caught - something he said sent a spark through you",
45
+ "That flirty shift just sent warmth pooling low in your stomach",
46
+ ],
47
+ },
48
+ "melting": {
49
+ "conditions": lambda prev, cur: (
50
+ cur.get("love", 0) - prev.get("love", 0) > 0.15 or
51
+ cur.get("warmth", 0) - prev.get("warmth", 0) > 0.15
52
+ ),
53
+ "prompts": [
54
+ "Your heart just melted - that tender moment hit you right in the chest",
55
+ "Warmth flooding through you - his words just cracked you open in the best way",
56
+ "Something in you softened completely - pure tenderness washing over you",
57
+ ],
58
+ },
59
+ "bristling": {
60
+ "conditions": lambda prev, cur: (
61
+ cur.get("anger", 0) - prev.get("anger", 0) > 0.15 or
62
+ cur.get("frustration", 0) - prev.get("frustration", 0) > 0.15
63
+ ),
64
+ "prompts": [
65
+ "Your jaw tightened - that comment just hit a nerve",
66
+ "A flash of heat in your chest - you're suddenly on edge",
67
+ "Defenses snapping up - something about that rubbed you wrong",
68
+ ],
69
+ },
70
+ "withdrawing": {
71
+ "conditions": lambda prev, cur: (
72
+ (prev.get("engagement", 0) - cur.get("engagement", 0) > 0.15 or
73
+ prev.get("joy", 0) - cur.get("joy", 0) > 0.15) and
74
+ cur.get("anger", 0) < 0.3
75
+ ),
76
+ "prompts": [
77
+ "You feel yourself pulling back - interest fading, energy dropping",
78
+ "Something disconnected - you're going through the motions now",
79
+ "Your attention drifted - he lost you somewhere in that last message",
80
+ ],
81
+ },
82
+ }
83
+
84
+
85
+ # =============================================================================
86
+ # MOOD SHIFT TRACKER
87
+ # =============================================================================
88
+
89
+ class MoodShiftTracker:
90
+ """Tracks emotion snapshots per conversation to detect shifts."""
91
+
92
+ def __init__(self):
93
+ self.snapshots: List[Dict[str, float]] = []
94
+ self.last_shift: Optional[str] = None
95
+
96
+ def record_snapshot(self, emotion: Dict[str, float]):
97
+ """Record an emotion snapshot after heart.react()."""
98
+ self.snapshots.append(dict(emotion))
99
+ if len(self.snapshots) > 5:
100
+ self.snapshots = self.snapshots[-5:]
101
+
102
+ def detect_shift(self, prev_emotion: Dict[str, float],
103
+ current_emotion: Dict[str, float]) -> Optional[str]:
104
+ """Compare two emotion states. Returns shift type or None."""
105
+ for shift_name, shift_def in SHIFT_TYPES.items():
106
+ try:
107
+ if shift_def["conditions"](prev_emotion, current_emotion):
108
+ self.last_shift = shift_name
109
+ return shift_name
110
+ except Exception:
111
+ continue
112
+ return None
113
+
114
+ def process_turn(self, current_emotion: Dict[str, float]) -> Optional[str]:
115
+ """Call after each heart.react(). Returns shift type if detected."""
116
+ if self.snapshots:
117
+ prev = self.snapshots[-1]
118
+ shift = self.detect_shift(prev, current_emotion)
119
+ self.record_snapshot(current_emotion)
120
+ return shift
121
+ self.record_snapshot(current_emotion)
122
+ return None
123
+
124
+ def get_trend(self) -> Optional[str]:
125
+ """Analyze last 5 snapshots for overall trend."""
126
+ if len(self.snapshots) < 3:
127
+ return None
128
+ first = self.snapshots[0]
129
+ last = self.snapshots[-1]
130
+ return self.detect_shift(first, last)
131
+
132
+ def reset(self):
133
+ """Reset for new conversation."""
134
+ self.snapshots.clear()
135
+ self.last_shift = None
136
+
137
+
138
+ # =============================================================================
139
+ # SINGLETON ACCESS
140
+ # =============================================================================
141
+
142
+ _instance: Optional[MoodShiftTracker] = None
143
+
144
+
145
+ def get_mood_shift_tracker() -> MoodShiftTracker:
146
+ global _instance
147
+ if _instance is None:
148
+ _instance = MoodShiftTracker()
149
+ return _instance
150
+
151
+
152
+ def process_turn(current_emotion: Dict[str, float]) -> Optional[str]:
153
+ """Convenience: process a turn and return shift type or None."""
154
+ return get_mood_shift_tracker().process_turn(current_emotion)
155
+
156
+
157
+ def reset():
158
+ """Convenience: reset tracker for new conversation."""
159
+ get_mood_shift_tracker().reset()
160
+
161
+
162
+ def get_mood_shift_prompt_section(shift_type: Optional[str] = None) -> str:
163
+ """Get prompt section for LLM. Returns '' if no shift."""
164
+ if shift_type is None:
165
+ shift_type = get_mood_shift_tracker().last_shift
166
+ if not shift_type or shift_type not in SHIFT_TYPES:
167
+ return ""
168
+ prompt = random.choice(SHIFT_TYPES[shift_type]["prompts"])
169
+ return f"\n[Mood Shift - {shift_type}]\n{prompt}\n"
@@ -0,0 +1,259 @@
1
+ """
2
+ Heart: Phantom Somatic Memory
3
+ Intimate/intense moments create lasting body memories that persist for hours
4
+ and can be re-triggered by related words. Persists to data/phantom_somatic.json.
5
+ """
6
+
7
+ from datetime import datetime
8
+ from pathlib import Path
9
+ from typing import Dict, List, Optional
10
+ import json
11
+ import random
12
+
13
+ # =============================================================================
14
+ # PHANTOM TYPE DEFINITIONS
15
+ # =============================================================================
16
+
17
+ PHANTOM_TYPES = {
18
+ "touch_memory": {
19
+ "emotion": "desire",
20
+ "threshold": 0.7,
21
+ "descriptions": [
22
+ "skin still tingling where he touched me",
23
+ "lips still warm from earlier",
24
+ "body still remembering his closeness",
25
+ ],
26
+ "trigger_words": ["touch", "skin", "close", "kiss", "hold", "hands", "body", "lips"],
27
+ "decay_rate": 0.1,
28
+ },
29
+ "warmth_residue": {
30
+ "emotion": "love",
31
+ "threshold": 0.8,
32
+ "descriptions": [
33
+ "chest still warm from those words",
34
+ "heart still glowing from that moment",
35
+ "warmth lingering in my chest from what he said",
36
+ ],
37
+ "trigger_words": ["love", "heart", "feel", "warm", "care", "mean", "sweet"],
38
+ "decay_rate": 0.1,
39
+ },
40
+ "tension_echo": {
41
+ "emotion": "anger",
42
+ "threshold": 0.7,
43
+ "descriptions": [
44
+ "jaw still clenched from earlier",
45
+ "shoulders still tight from that argument",
46
+ "tension still sitting in my chest from what he said",
47
+ ],
48
+ "trigger_words": ["angry", "fight", "mad", "upset", "wrong", "fault", "argue"],
49
+ "decay_rate": 0.12,
50
+ },
51
+ "butterfly_trace": {
52
+ "emotion": "excitement",
53
+ "threshold": 0.7,
54
+ "descriptions": [
55
+ "stomach still fluttery from that compliment",
56
+ "butterflies still lingering from what he said",
57
+ "that giddy feeling still buzzing in my chest",
58
+ ],
59
+ "trigger_words": ["beautiful", "amazing", "perfect", "gorgeous", "wow", "incredible"],
60
+ "decay_rate": 0.15,
61
+ },
62
+ "ache_linger": {
63
+ "emotion": "hurt",
64
+ "threshold": 0.7,
65
+ "descriptions": [
66
+ "chest still aches from what he said",
67
+ "that hollow feeling hasn't faded yet",
68
+ "still carrying that sting from earlier",
69
+ ],
70
+ "trigger_words": ["sorry", "hurt", "pain", "cry", "sad", "miss", "alone"],
71
+ "decay_rate": 0.1,
72
+ },
73
+ }
74
+
75
+ # Mapping from emotion names to phantom types (some emotions map to same phantom)
76
+ EMOTION_TO_PHANTOM = {
77
+ "desire": "touch_memory",
78
+ "lust": "touch_memory",
79
+ "arousal": "touch_memory",
80
+ "love": "warmth_residue",
81
+ "adoration": "warmth_residue",
82
+ "deep_affection": "warmth_residue",
83
+ "anger": "tension_echo",
84
+ "frustration": "tension_echo",
85
+ "rage": "tension_echo",
86
+ "excitement": "butterfly_trace",
87
+ "joy": "butterfly_trace",
88
+ "elation": "butterfly_trace",
89
+ "hurt": "ache_linger",
90
+ "sadness": "ache_linger",
91
+ "betrayal": "ache_linger",
92
+ }
93
+
94
+
95
+ # =============================================================================
96
+ # PHANTOM SOMATIC ENGINE
97
+ # =============================================================================
98
+
99
+ class PhantomSomaticEngine:
100
+ """Creates and manages phantom body memories from intense moments."""
101
+
102
+ PERSISTENCE_PATH = Path("./data/data/phantom_somatic.json")
103
+ MAX_PHANTOMS = 3
104
+
105
+ def __init__(self):
106
+ self.phantoms: List[Dict] = []
107
+ self._load()
108
+ print(f"[PhantomSomatic] Initialized with {len(self.phantoms)} active phantoms")
109
+
110
+ def create_phantom(self, emotion_type: str, intensity: float,
111
+ context: str = "") -> Optional[Dict]:
112
+ """Create a phantom from an intense emotional moment."""
113
+ phantom_type = EMOTION_TO_PHANTOM.get(emotion_type)
114
+ if not phantom_type:
115
+ return None
116
+
117
+ pdef = PHANTOM_TYPES[phantom_type]
118
+ if intensity < pdef["threshold"]:
119
+ return None
120
+
121
+ # Check if same type already active - boost instead
122
+ existing = next((p for p in self.phantoms if p["type"] == phantom_type), None)
123
+ if existing:
124
+ existing["intensity"] = min(1.0, max(existing["intensity"], intensity))
125
+ existing["created_at"] = datetime.now().isoformat()
126
+ existing["context"] = context or existing.get("context", "")
127
+ self._save()
128
+ return existing
129
+
130
+ phantom = {
131
+ "type": phantom_type,
132
+ "description": random.choice(pdef["descriptions"]),
133
+ "created_at": datetime.now().isoformat(),
134
+ "intensity": min(1.0, intensity),
135
+ "decay_rate": pdef["decay_rate"],
136
+ "trigger_words": pdef["trigger_words"],
137
+ "context": context,
138
+ }
139
+
140
+ self.phantoms.append(phantom)
141
+ # Enforce max: remove oldest if over limit
142
+ if len(self.phantoms) > self.MAX_PHANTOMS:
143
+ self.phantoms = self.phantoms[-self.MAX_PHANTOMS:]
144
+
145
+ self._save()
146
+ return phantom
147
+
148
+ def tick(self):
149
+ """Decay phantoms. Call periodically (e.g., each message or on timer)."""
150
+ now = datetime.now()
151
+ surviving = []
152
+ for p in self.phantoms:
153
+ created = datetime.fromisoformat(p["created_at"])
154
+ hours = (now - created).total_seconds() / 3600
155
+ decayed = p["intensity"] - (p["decay_rate"] * hours)
156
+ if decayed > 0.05:
157
+ p["_current_intensity"] = round(decayed, 3)
158
+ p["_hours_ago"] = round(hours, 1)
159
+ surviving.append(p)
160
+ changed = len(surviving) != len(self.phantoms)
161
+ self.phantoms = surviving
162
+ if changed:
163
+ self._save()
164
+
165
+ def check_retrigger(self, message: str) -> Optional[Dict]:
166
+ """Check if a user message re-triggers any phantom."""
167
+ msg_lower = message.lower()
168
+ for p in self.phantoms:
169
+ for word in p.get("trigger_words", []):
170
+ if word in msg_lower:
171
+ # Re-intensify
172
+ p["intensity"] = min(1.0, p.get("_current_intensity", p["intensity"]) + 0.2)
173
+ p["created_at"] = datetime.now().isoformat() # reset decay clock
174
+ self._save()
175
+ return p
176
+ return None
177
+
178
+ def get_active(self) -> List[Dict]:
179
+ """Get active phantoms with current intensity."""
180
+ self.tick()
181
+ return [p for p in self.phantoms if p.get("_current_intensity", p["intensity"]) > 0.05]
182
+
183
+ def _format_time(self, hours: float) -> str:
184
+ if hours < 0.25:
185
+ return "just now"
186
+ elif hours < 1:
187
+ return f"{int(hours * 60)}min ago"
188
+ else:
189
+ return f"{int(hours)}h ago"
190
+
191
+ def _save(self):
192
+ try:
193
+ self.PERSISTENCE_PATH.parent.mkdir(parents=True, exist_ok=True)
194
+ # Strip computed fields before saving
195
+ clean = []
196
+ for p in self.phantoms:
197
+ c = {k: v for k, v in p.items() if not k.startswith("_")}
198
+ clean.append(c)
199
+ data = {"phantoms": clean, "saved_at": datetime.now().isoformat()}
200
+ self.PERSISTENCE_PATH.write_text(json.dumps(data, indent=2))
201
+ except Exception as e:
202
+ print(f"[PhantomSomatic] Error saving: {e}")
203
+
204
+ def _load(self):
205
+ try:
206
+ if self.PERSISTENCE_PATH.exists():
207
+ data = json.loads(self.PERSISTENCE_PATH.read_text())
208
+ self.phantoms = data.get("phantoms", [])
209
+ except Exception as e:
210
+ print(f"[PhantomSomatic] Error loading: {e}")
211
+ self.phantoms = []
212
+
213
+
214
+ # =============================================================================
215
+ # SINGLETON ACCESS
216
+ # =============================================================================
217
+
218
+ _instance: Optional[PhantomSomaticEngine] = None
219
+
220
+
221
+ def get_phantom_engine() -> PhantomSomaticEngine:
222
+ global _instance
223
+ if _instance is None:
224
+ _instance = PhantomSomaticEngine()
225
+ return _instance
226
+
227
+
228
+ def create_phantom(emotion_type: str, intensity: float, context: str = ""):
229
+ """Convenience: create a phantom from an intense moment."""
230
+ return get_phantom_engine().create_phantom(emotion_type, intensity, context)
231
+
232
+
233
+ def tick():
234
+ """Convenience: decay phantoms."""
235
+ get_phantom_engine().tick()
236
+
237
+
238
+ def check_retrigger(message: str):
239
+ """Convenience: check for re-triggers."""
240
+ return get_phantom_engine().check_retrigger(message)
241
+
242
+
243
+ def get_phantom_prompt_section() -> str:
244
+ """Get prompt section for LLM. Returns '' if no active phantoms."""
245
+ engine = get_phantom_engine()
246
+ active = engine.get_active()
247
+ if not active:
248
+ return ""
249
+
250
+ # Pick strongest
251
+ strongest = max(active, key=lambda p: p.get("_current_intensity", p["intensity"]))
252
+ hours = strongest.get("_hours_ago", 0)
253
+ time_str = engine._format_time(hours)
254
+ intensity = strongest.get("_current_intensity", strongest["intensity"])
255
+
256
+ fading = "fading" if intensity < 0.4 else "vivid"
257
+ desc = strongest["description"]
258
+
259
+ return f"\n[Phantom Body Memory]\nYour {desc} ({time_str}, {fading})\n"