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
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"
|