affective-longing 0.1.0__py3-none-any.whl

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.
@@ -0,0 +1,47 @@
1
+ """
2
+ affective-longing: Emotional extension for AI companions.
3
+
4
+ Built on revive-companion, adds:
5
+ - Memory triggers (embedding similarity)
6
+ - Relationship state machine (HMM + OU process)
7
+ - AI self-emotion modeling (VAD model)
8
+
9
+ Three layers, one decision:
10
+ 1. Memory — past memories trigger present longing
11
+ 2. Relationship — 6-stage lifecycle with OU decay
12
+ 3. Emotion — VAD model for AI's internal state
13
+ """
14
+
15
+ from .emotion import Emotion, EmotionEngine, EmotionEngineConfig, EmotionalState
16
+ from .engine import AffectiveLonging, AffectiveResult
17
+ from .memory import MemoryStore, TriggerEngine
18
+ from .relationship import (
19
+ Event,
20
+ OUProcess,
21
+ RelationshipStateMachine,
22
+ RelationshipState,
23
+ Stage,
24
+ TransitionConfig,
25
+ )
26
+
27
+ __all__ = [
28
+ # Core engine
29
+ "AffectiveLonging",
30
+ "AffectiveResult",
31
+ # Memory
32
+ "MemoryStore",
33
+ "TriggerEngine",
34
+ # Relationship
35
+ "Stage",
36
+ "RelationshipState",
37
+ "Event",
38
+ "RelationshipStateMachine",
39
+ "TransitionConfig",
40
+ "OUProcess",
41
+ # Emotion
42
+ "Emotion",
43
+ "EmotionalState",
44
+ "EmotionEngine",
45
+ "EmotionEngineConfig",
46
+ ]
47
+ __version__ = "0.1.0"
@@ -0,0 +1,18 @@
1
+ """
2
+ Emotion module — VAD model for AI companion's internal state.
3
+
4
+ Components:
5
+ - Emotion: Discrete emotion labels (joy, sadness, anger, etc.)
6
+ - EmotionalState: VAD snapshot with emotion label
7
+ - EmotionEngine: Manages emotional state with OU decay + events
8
+ """
9
+
10
+ from .engine import EmotionEngine, EmotionEngineConfig
11
+ from .vad import Emotion, EmotionalState
12
+
13
+ __all__ = [
14
+ "Emotion",
15
+ "EmotionalState",
16
+ "EmotionEngine",
17
+ "EmotionEngineConfig",
18
+ ]
@@ -0,0 +1,235 @@
1
+ """
2
+ Emotion engine — manages the AI companion's emotional state.
3
+
4
+ Combines:
5
+ 1. OU processes (one per VAD dimension) for time decay
6
+ 2. Event-driven bumps (user actions affect emotions)
7
+ 3. Relationship state influence (cold war → low valence)
8
+
9
+ Design:
10
+ - Each VAD dimension has its own OU process
11
+ - Events bump specific dimensions
12
+ - Relationship state modulates baselines (e.g., COLD → lower valence baseline)
13
+ - Emotions decay toward dynamic baselines over time
14
+ """
15
+
16
+ from __future__ import annotations
17
+
18
+ import logging
19
+ from dataclasses import dataclass, field
20
+
21
+ from ..relationship.ou_process import OUProcess
22
+ from ..relationship.stages import Stage
23
+ from .vad import Emotion, EmotionalState
24
+
25
+ logger = logging.getLogger(__name__)
26
+
27
+
28
+ # Event → (valence_bump, arousal_bump, dominance_bump)
29
+ EVENT_BUMPS: dict[str, tuple[float, float, float]] = {
30
+ # Positive events
31
+ "reply_fast": (0.08, 0.05, 0.03),
32
+ "affection": (0.12, 0.08, 0.02),
33
+ "initiate": (0.10, 0.06, 0.05),
34
+ "long_message": (0.06, 0.04, 0.01),
35
+
36
+ # Negative events
37
+ "no_reply": (-0.06, 0.08, -0.05),
38
+ "reply_slow": (-0.03, 0.02, -0.02),
39
+ "long_silence": (-0.10, 0.10, -0.08),
40
+ "reject": (-0.12, 0.10, -0.10),
41
+ "fight": (-0.15, 0.15, -0.05),
42
+
43
+ # Recovery
44
+ "apology": (0.05, -0.05, 0.03),
45
+ }
46
+
47
+ # Relationship stage → baseline adjustments (V, A, D)
48
+ STAGE_BASELINES: dict[Stage, tuple[float, float, float]] = {
49
+ Stage.COURTING: (0.2, 0.6, 0.3), # Slightly positive, anxious, uncertain
50
+ Stage.SWEET: (0.7, 0.5, 0.5), # Happy, moderate arousal, balanced
51
+ Stage.PASSIONATE: (0.8, 0.7, 0.6), # Very happy, excited, confident
52
+ Stage.STABLE: (0.5, 0.3, 0.5), # Content, calm, balanced
53
+ Stage.COLD: (-0.5, 0.5, 0.3), # Unhappy, tense, low control
54
+ Stage.REPAIRING: (0.0, 0.5, 0.4), # Neutral, attentive, cautious
55
+ }
56
+
57
+
58
+ @dataclass
59
+ class EmotionEngineConfig:
60
+ """Configuration for emotion engine."""
61
+
62
+ # Default baselines (overridden by relationship stage)
63
+ valence_baseline: float = 0.3
64
+ arousal_baseline: float = 0.4
65
+ dominance_baseline: float = 0.5
66
+
67
+ # OU parameters
68
+ valence_reversion: float = 0.06 # Slow decay (emotions are sticky)
69
+ arousal_reversion: float = 0.10 # Faster decay (excitement fades)
70
+ dominance_reversion: float = 0.08
71
+
72
+ valence_volatility: float = 0.02
73
+ arousal_volatility: float = 0.03
74
+ dominance_volatility: float = 0.02
75
+
76
+
77
+ class EmotionEngine:
78
+ """
79
+ Manages the AI companion's emotional state.
80
+
81
+ Example:
82
+ >>> engine = EmotionEngine(seed=42)
83
+ >>> engine.current_state
84
+ EmotionalState(😐 neutral, V=+0.30, A=0.40, D=0.50)
85
+
86
+ >>> engine.observe("affection")
87
+ >>> engine.current_state
88
+ EmotionalState(😊 joy, V=+0.42, A=0.48, D=0.52)
89
+
90
+ >>> engine.update_relationship_stage(Stage.COLD)
91
+ >>> engine.step_time(24)
92
+ >>> engine.current_state
93
+ EmotionalState(😢 sadness, V=-0.20, ...)
94
+ """
95
+
96
+ def __init__(
97
+ self,
98
+ config: EmotionEngineConfig | None = None,
99
+ seed: int | None = None,
100
+ ):
101
+ self.config = config or EmotionEngineConfig()
102
+
103
+ # OU processes for each dimension
104
+ self._valence = OUProcess(
105
+ baseline=self.config.valence_baseline,
106
+ reversion_speed=self.config.valence_reversion,
107
+ volatility=self.config.valence_volatility,
108
+ seed=seed,
109
+ )
110
+ self._arousal = OUProcess(
111
+ baseline=self.config.arousal_baseline,
112
+ reversion_speed=self.config.arousal_reversion,
113
+ volatility=self.config.arousal_volatility,
114
+ seed=(seed + 1) if seed is not None else None,
115
+ )
116
+ self._dominance = OUProcess(
117
+ baseline=self.config.dominance_baseline,
118
+ reversion_speed=self.config.dominance_reversion,
119
+ volatility=self.config.dominance_volatility,
120
+ seed=(seed + 2) if seed is not None else None,
121
+ )
122
+
123
+ # Valence is [-1, 1], not [0, 1], so we need special handling
124
+ self._valence.value = self.config.valence_baseline
125
+
126
+ self._current_stage: Stage | None = None
127
+
128
+ logger.info(
129
+ "EmotionEngine initialized: %s",
130
+ self.current_state,
131
+ )
132
+
133
+ @property
134
+ def current_state(self) -> EmotionalState:
135
+ """Current emotional snapshot."""
136
+ return EmotionalState(
137
+ valence=self._valence.value,
138
+ arousal=self._arousal.value,
139
+ dominance=self._dominance.value,
140
+ )
141
+
142
+ @property
143
+ def valence(self) -> float:
144
+ return self._valence.value
145
+
146
+ @property
147
+ def arousal(self) -> float:
148
+ return self._arousal.value
149
+
150
+ @property
151
+ def dominance(self) -> float:
152
+ return self._dominance.value
153
+
154
+ def observe(self, event: str) -> EmotionalState:
155
+ """
156
+ React to an event.
157
+
158
+ Args:
159
+ event: Event name (e.g., "reply_fast", "fight", "affection")
160
+
161
+ Returns:
162
+ New emotional state.
163
+ """
164
+ bumps = EVENT_BUMPS.get(event, (0.0, 0.0, 0.0))
165
+ v_bump, a_bump, d_bump = bumps
166
+
167
+ self._valence.bump(v_bump)
168
+ self._arousal.bump(a_bump)
169
+ self._dominance.bump(d_bump)
170
+
171
+ logger.debug(
172
+ "Emotion event=%s: bumps=(V%+.2f, A%+.2f, D%+.2f) → %s",
173
+ event,
174
+ v_bump,
175
+ a_bump,
176
+ d_bump,
177
+ self.current_state,
178
+ )
179
+
180
+ return self.current_state
181
+
182
+ def update_relationship_stage(self, stage: Stage) -> None:
183
+ """
184
+ Update emotional baselines based on relationship stage.
185
+
186
+ Call this when the relationship state machine transitions.
187
+ """
188
+ if stage == self._current_stage:
189
+ return
190
+
191
+ self._current_stage = stage
192
+ v_base, a_base, d_base = STAGE_BASELINES.get(
193
+ stage,
194
+ (self.config.valence_baseline,
195
+ self.config.arousal_baseline,
196
+ self.config.dominance_baseline),
197
+ )
198
+
199
+ self._valence.baseline = v_base
200
+ self._arousal.baseline = a_base
201
+ self._dominance.baseline = d_base
202
+
203
+ logger.info(
204
+ "Emotion baselines updated for stage=%s: (V=%.2f, A=%.2f, D=%.2f)",
205
+ stage.value,
206
+ v_base,
207
+ a_base,
208
+ d_base,
209
+ )
210
+
211
+ def step_time(self, dt: float = 1.0) -> EmotionalState:
212
+ """
213
+ Advance time — emotions decay toward baselines.
214
+
215
+ Args:
216
+ dt: Time step in hours.
217
+
218
+ Returns:
219
+ New emotional state.
220
+ """
221
+ self._valence.step(dt)
222
+ self._arousal.step(dt)
223
+ self._dominance.step(dt)
224
+
225
+ return self.current_state
226
+
227
+ def reset(self) -> None:
228
+ """Reset to default emotional state."""
229
+ self._valence.reset()
230
+ self._arousal.reset()
231
+ self._dominance.reset()
232
+ self._current_stage = None
233
+
234
+ def __repr__(self) -> str:
235
+ return f"EmotionEngine({self.current_state})"
@@ -0,0 +1,116 @@
1
+ """
2
+ VAD emotion model — Valence, Arousal, Dominance.
3
+
4
+ Based on Russell's circumplex model of affect (1980) and
5
+ Mehrabian's PAD (Pleasure-Arousal-Dominance) model (1996).
6
+
7
+ Used in:
8
+ - Sentiment analysis
9
+ - Emotion recognition
10
+ - Affective computing
11
+ - Human-robot interaction
12
+
13
+ Our use: Model the AI companion's "internal emotional state"
14
+ as it interacts with the user over time.
15
+ """
16
+
17
+ from __future__ import annotations
18
+
19
+ from dataclasses import dataclass
20
+ from enum import Enum
21
+
22
+
23
+ class Emotion(Enum):
24
+ """
25
+ Discrete emotion labels mapped from VAD space.
26
+
27
+ Based on Plutchik's wheel (1980) + modern affective computing.
28
+ """
29
+
30
+ JOY = "joy" # V+, A+, D+
31
+ TRUST = "trust" # V+, A~, D~
32
+ CONTENTMENT = "contentment" # V+, A-, D~
33
+ EXCITEMENT = "excitement" # V+, A+, D~
34
+ LOVE = "love" # V++, A+, D~
35
+ ANTICIPATION = "anticipation" # V~, A+, D~
36
+ ANXIETY = "anxiety" # V-, A+, D-
37
+ SADNESS = "sadness" # V-, A-, D-
38
+ ANGER = "anger" # V-, A+, D+
39
+ FEAR = "fear" # V--, A++, D--
40
+ NEUTRAL = "neutral" # V~, A~, D~
41
+
42
+ @staticmethod
43
+ def from_vad(valence: float, arousal: float, dominance: float) -> Emotion:
44
+ """Map continuous VAD to discrete emotion."""
45
+ # Thresholds
46
+ HIGH_V = 0.5
47
+ LOW_V = -0.5
48
+ HIGH_A = 0.6
49
+ LOW_A = 0.4
50
+ HIGH_D = 0.6
51
+ LOW_D = 0.4
52
+
53
+ if valence > HIGH_V:
54
+ if arousal > HIGH_A:
55
+ if dominance > HIGH_D:
56
+ return Emotion.JOY
57
+ return Emotion.EXCITEMENT
58
+ elif arousal < LOW_A:
59
+ return Emotion.CONTENTMENT
60
+ else:
61
+ return Emotion.TRUST
62
+ elif valence < LOW_V:
63
+ if arousal > HIGH_A:
64
+ if dominance > HIGH_D:
65
+ return Emotion.ANGER
66
+ elif dominance < LOW_D:
67
+ return Emotion.FEAR
68
+ return Emotion.ANXIETY
69
+ elif arousal < LOW_A:
70
+ return Emotion.SADNESS
71
+ else:
72
+ return Emotion.ANXIETY
73
+ else:
74
+ # valence near zero
75
+ if arousal > HIGH_A:
76
+ return Emotion.ANTICIPATION
77
+ return Emotion.NEUTRAL
78
+
79
+
80
+ @dataclass
81
+ class EmotionalState:
82
+ """Full emotional snapshot."""
83
+
84
+ valence: float # -1 (negative) to +1 (positive)
85
+ arousal: float # 0 (calm) to 1 (excited)
86
+ dominance: float # 0 (submissive) to 1 (dominant)
87
+
88
+ @property
89
+ def emotion(self) -> Emotion:
90
+ """Discrete emotion label."""
91
+ return Emotion.from_vad(self.valence, self.arousal, self.dominance)
92
+
93
+ @property
94
+ def emoji(self) -> str:
95
+ return {
96
+ Emotion.JOY: "😊",
97
+ Emotion.TRUST: "🤝",
98
+ Emotion.CONTENTMENT: "😌",
99
+ Emotion.EXCITEMENT: "🤩",
100
+ Emotion.LOVE: "❤️",
101
+ Emotion.ANTICIPATION: "🤔",
102
+ Emotion.ANXIETY: "😰",
103
+ Emotion.SADNESS: "😢",
104
+ Emotion.ANGER: "😠",
105
+ Emotion.FEAR: "😨",
106
+ Emotion.NEUTRAL: "😐",
107
+ }[self.emotion]
108
+
109
+ def __repr__(self) -> str:
110
+ return (
111
+ f"EmotionalState("
112
+ f"{self.emoji} {self.emotion.value}, "
113
+ f"V={self.valence:+.2f}, "
114
+ f"A={self.arousal:.2f}, "
115
+ f"D={self.dominance:.2f})"
116
+ )
@@ -0,0 +1,288 @@
1
+ """
2
+ AffectiveLonging — unified emotional extension engine.
3
+
4
+ Integrates three modules:
5
+ 1. Memory triggers (embedding similarity → longing boost)
6
+ 2. Relationship state machine (HMM + OU → stage transitions)
7
+ 3. AI self-emotion (VAD model → emotional state)
8
+
9
+ Usage:
10
+ engine = AffectiveLonging()
11
+
12
+ # Store memories
13
+ engine.remember("你喜欢下雨天", tags=["weather"])
14
+
15
+ # Main loop
16
+ result = engine.tick(context="今天下雨了")
17
+ if result.should_send:
18
+ send(result.prompt)
19
+
20
+ # Observe events
21
+ engine.observe("reply_fast")
22
+
23
+ # Time passes
24
+ engine.step_time(hours=12)
25
+ """
26
+
27
+ from __future__ import annotations
28
+
29
+ import logging
30
+ from dataclasses import dataclass, field
31
+ from datetime import datetime
32
+ from typing import Any
33
+
34
+ from revive_my_lover import PoissonLove
35
+
36
+ from .emotion import EmotionEngine, EmotionalState
37
+ from .memory import MemoryStore, TriggerEngine
38
+ from .relationship import Event, RelationshipStateMachine, RelationshipState, Stage
39
+
40
+ logger = logging.getLogger(__name__)
41
+
42
+
43
+ @dataclass
44
+ class AffectiveResult:
45
+ """Full result of an AffectiveLonging tick."""
46
+
47
+ # Base decision
48
+ should_send: bool
49
+ base_probability: float
50
+
51
+ # Memory trigger
52
+ memory_trigger: str | None = None
53
+ memory_similarity: float = 0.0
54
+ longing_boost: float = 0.0
55
+ boosted_probability: float = 0.0
56
+
57
+ # Relationship
58
+ relationship_stage: Stage = Stage.COURTING
59
+ intimacy: float = 0.5
60
+ conflict: float = 0.1
61
+
62
+ # Emotion
63
+ emotional_state: EmotionalState | None = None
64
+
65
+ # Prompt
66
+ prompt: str = ""
67
+ reason: str = ""
68
+
69
+ metadata: dict[str, Any] = field(default_factory=dict)
70
+
71
+
72
+ class AffectiveLonging:
73
+ """
74
+ Unified emotional engagement engine.
75
+
76
+ Combines three layers:
77
+ 1. Memory — past memories trigger present longing
78
+ 2. Relationship — 6-stage lifecycle with OU decay
79
+ 3. Emotion — VAD model for AI's internal state
80
+
81
+ Args:
82
+ memory_persist_dir: Where to store memory embeddings.
83
+ relationship_seed: Seed for relationship state machine.
84
+ emotion_seed: Seed for emotion engine.
85
+ **kwargs: Passed to PoissonLove (config, seed, etc.)
86
+
87
+ Example:
88
+ >>> engine = AffectiveLonging()
89
+ >>> engine.remember("你喜欢下雨天")
90
+ >>> engine.observe("reply_fast")
91
+ >>> result = engine.tick(context="今天下雨了")
92
+ >>> result.emotional_state
93
+ EmotionalState(😊 joy, V=+0.45, A=0.52, D=0.55)
94
+ """
95
+
96
+ def __init__(
97
+ self,
98
+ memory_persist_dir: str = "./companion_memory_db",
99
+ relationship_seed: int | None = None,
100
+ emotion_seed: int | None = None,
101
+ **kwargs,
102
+ ):
103
+ # Base engine (revive-companion)
104
+ self._base = PoissonLove(**kwargs)
105
+
106
+ # Memory layer
107
+ self.memory = MemoryStore(persist_dir=memory_persist_dir)
108
+ self.trigger = TriggerEngine(self.memory)
109
+
110
+ # Relationship layer
111
+ self.relationship = RelationshipStateMachine(seed=relationship_seed)
112
+
113
+ # Emotion layer
114
+ self.emotion = EmotionEngine(seed=emotion_seed)
115
+
116
+ logger.info("AffectiveLonging initialized with 3 layers")
117
+
118
+ # ── Memory API ──
119
+
120
+ def remember(
121
+ self, text: str, tags: list[str] | None = None, **metadata
122
+ ) -> str:
123
+ """
124
+ Store a memory for future triggering.
125
+
126
+ Args:
127
+ text: The memory content (e.g., "你喜欢下雨天").
128
+ tags: Optional tags for filtering.
129
+ **metadata: Additional metadata.
130
+
131
+ Returns:
132
+ Memory ID.
133
+ """
134
+ meta = dict(metadata)
135
+ if tags:
136
+ meta["tags"] = ",".join(tags)
137
+ return self.memory.add(text, metadata=meta)
138
+
139
+ # ── Event API ──
140
+
141
+ def observe(self, event: str) -> None:
142
+ """
143
+ Observe an event and update relationship + emotion.
144
+
145
+ Maps string event names to:
146
+ - Relationship Event enum
147
+ - Emotion engine bumps
148
+
149
+ Args:
150
+ event: One of: reply_fast, reply_slow, no_reply, long_silence,
151
+ affection, fight, apology, initiate, reject, long_message
152
+ """
153
+ # Update relationship state
154
+ try:
155
+ rel_event = Event(event)
156
+ self.relationship.observe(rel_event)
157
+ except ValueError:
158
+ logger.warning("Unknown relationship event: %s", event)
159
+
160
+ # Update emotion engine
161
+ self.emotion.observe(event)
162
+
163
+ # Sync emotion baselines with relationship stage
164
+ self.emotion.update_relationship_stage(self.relationship.stage)
165
+
166
+ logger.debug(
167
+ "Observed event=%s, stage=%s, emotion=%s",
168
+ event,
169
+ self.relationship.stage.value,
170
+ self.emotion.current_state.emotion.value,
171
+ )
172
+
173
+ def step_time(self, hours: float = 1.0) -> None:
174
+ """
175
+ Let time pass — decays relationship intimacy/conflict and emotion.
176
+
177
+ Args:
178
+ hours: Hours to advance.
179
+ """
180
+ self.relationship.step_time(hours)
181
+ self.emotion.step_time(hours)
182
+
183
+ # Keep emotion baselines in sync
184
+ self.emotion.update_relationship_stage(self.relationship.stage)
185
+
186
+ # ── Tick API ──
187
+
188
+ def tick(
189
+ self,
190
+ now: datetime | None = None,
191
+ context: str = "",
192
+ ) -> AffectiveResult:
193
+ """
194
+ Run one tick of the full pipeline.
195
+
196
+ Flow:
197
+ 1. Base Poisson tick (timing decision)
198
+ 2. Memory trigger (if context provided)
199
+ 3. Package result with relationship + emotion state
200
+
201
+ Args:
202
+ now: Current time.
203
+ context: Situation text for memory matching.
204
+
205
+ Returns:
206
+ AffectiveResult with all layers' state.
207
+ """
208
+ # 1. Base engine
209
+ base_result = self._base.tick(now=now)
210
+
211
+ # 2. Memory trigger
212
+ trigger_text = None
213
+ trigger_sim = 0.0
214
+ boost = 0.0
215
+ boosted_prob = base_result.probability
216
+
217
+ if context and self.memory.count() > 0:
218
+ trigger_info = self.trigger.compute_trigger_score(context)
219
+ if trigger_info["memories"]:
220
+ trigger_text = trigger_info["memories"][0]["text"]
221
+ trigger_sim = trigger_info["max_similarity"]
222
+ boost = trigger_info["trigger_score"]
223
+ boosted_prob = self.trigger.boost_longing(
224
+ base_result.probability, context
225
+ )
226
+
227
+ # 3. Build result
228
+ rel_state = self.relationship.current_state
229
+ emo_state = self.emotion.current_state
230
+
231
+ return AffectiveResult(
232
+ should_send=base_result.should_send,
233
+ base_probability=base_result.probability,
234
+ memory_trigger=trigger_text,
235
+ memory_similarity=trigger_sim,
236
+ longing_boost=boost,
237
+ boosted_probability=boosted_prob,
238
+ relationship_stage=rel_state.stage,
239
+ intimacy=rel_state.intimacy,
240
+ conflict=rel_state.conflict,
241
+ emotional_state=emo_state,
242
+ prompt=base_result.prompt,
243
+ reason=base_result.reason,
244
+ )
245
+
246
+ # ── Pass-through API ──
247
+
248
+ def record_reply(self, **kwargs) -> None:
249
+ """Record user reply — passes to base engine."""
250
+ self._base.record_reply(**kwargs)
251
+
252
+ def record_send(self, **kwargs) -> None:
253
+ """Record that we sent — passes to base engine."""
254
+ self._base.record_send(**kwargs)
255
+
256
+ # ── State API ──
257
+
258
+ def get_state(self) -> dict:
259
+ """Get full system state snapshot."""
260
+ rel = self.relationship.current_state
261
+ emo = self.emotion.current_state
262
+ return {
263
+ "relationship": {
264
+ "stage": rel.stage.value,
265
+ "stage_display": rel.stage.display_name,
266
+ "intimacy": rel.intimacy,
267
+ "conflict": rel.conflict,
268
+ "confidence": rel.confidence,
269
+ },
270
+ "emotion": {
271
+ "emotion": emo.emotion.value,
272
+ "emoji": emo.emoji,
273
+ "valence": emo.valence,
274
+ "arousal": emo.arousal,
275
+ "dominance": emo.dominance,
276
+ },
277
+ "memory_count": self.memory.count(),
278
+ }
279
+
280
+ def __repr__(self) -> str:
281
+ rel = self.relationship.current_state
282
+ emo = self.emotion.current_state
283
+ return (
284
+ f"AffectiveLonging("
285
+ f"stage={rel.stage.display_name}, "
286
+ f"{emo.emoji} {emo.emotion.value}, "
287
+ f"memories={self.memory.count()})"
288
+ )