alive-ai 0.1.10 → 0.1.12

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/README.md CHANGED
@@ -19,6 +19,28 @@ It can be used as a friend, partner-style companion, study partner, creative cha
19
19
 
20
20
  Alive-AI does not claim biological consciousness. It is an open-source runtime for simulated affect and transparent memory.
21
21
 
22
+ ## What Makes It Alive
23
+
24
+ Alive-AI runs a continuous local state loop instead of treating every message as an isolated prompt. When you are not talking, the default-mode and subconscious loops keep evaluating silence, goals, recent memories, body state, circadian phase, dreams, and whether any proactive impulse is strong enough to surface.
25
+
26
+ The emotional layer now has real runtime consequences:
27
+
28
+ | System | What it stores | Runtime effect |
29
+ | --- | --- | --- |
30
+ | Core affect | Valence, arousal, dominance, trust, love, joy, desire, sadness, fear, anger, boredom, guilt, pride, jealousy, embarrassment, anticipation, hope, and dread. | Recomputed after every trigger so emotion changes affect mood, attachment, memory weighting, interoception, reactions, voice/media choices, and LLM tone. |
31
+ | Complex emotions | Guilt, pride, jealousy, embarrassment, and anticipation. | They do not just label the dashboard. They push fear, sadness, anger, dominance, trust, arousal, joy, and future-facing behavior in different directions. |
32
+ | Hormones | Oxytocin, dopamine, serotonin, cortisol, melatonin, plus residual metabolites. | Hormones modulate perception, soul valence/arousal, emotional deltas, somatic body state, interoception, impulse probability, and prompt guidance. Stress makes her more vigilant; bonding increases trust; dopamine increases pursuit; serotonin stabilizes; melatonin slows her down. |
33
+ | Internal body state | Energy, arousal, certainty, social satiety, cognitive load, connection craving, body sensations, and somatic memories. | The body state is persisted and feeds prompt tone, sleep/rest behavior, and whether she feels steady, overloaded, touchy, open, or withdrawn. |
34
+ | Circadian rhythm | Phase, sleep pressure, sleep debt, forced-awake windows, sleep cycle ID, wake time, and sleepiness. | She becomes sleepy, slows down, falls asleep, stops outward proactive behavior while asleep, can be woken by a message, recovers sleep debt, and wakes with lower or higher energy depending on rest. |
35
+ | Dreams | One normalized dream per sleep cycle, generated from memory fragments and emotion tags. | Dreams are saved, can appear in morning context, and are exposed in the dashboard and static Pages demo. |
36
+ | Narrative | Relationship phase (first_meeting → bonded) tracked per user. Key moments are detected from message content. | Phase and moment count are injected into the LLM prompt each turn. The dashboard shows the current phase and total key moments. |
37
+ | Curiosity | Per-user knowledge map across topics detected in messages. Topics range from 0 (unknown) to 1 (well-understood). | When knowledge on a topic is below 0.3 she asks a direct question. At 40% probability otherwise she surfaces curiosity as a prompt hint. Dashboard shows topics sorted by curiosity level. |
38
+ | Internal conflicts | Five persistent desire-vs-fear tensions (closeness/independence, passion/comfort, stability/growth, etc.) with a swinging balance that is saved between sessions. | Every message triggers conflicts whose keywords match the topic. Balance swings over time. Conflicts with tension > 0 surface in the prompt and are visible on the dashboard with a tension bar. |
39
+ | Persistence | Emotion, attachment, soul, somatic, unconscious, conflict, subconscious, circadian, and dream state under `data/`. | Restarting the runtime preserves the inner state instead of visually resetting it. |
40
+
41
+ The public Pages site is a static explanation and dashboard export. The local WebUI is the live version: it streams the actual state from the running Python backend.
42
+
43
+
22
44
  ## Quick Start
23
45
 
24
46
  ```bash
@@ -252,7 +274,7 @@ The real WebUI streams local runtime state over Server-Sent Events and shows:
252
274
  - recent thoughts and idle processing,
253
275
  - memory counters and uptime,
254
276
  - hormones and interoceptive body state,
255
- - attachment, circadian rhythm, body memory, dreams, curiosity, and conflicts,
277
+ - attachment, circadian rhythm, sleepiness, body memory, dreams, curiosity, and conflicts,
256
278
  - runtime health through local endpoints.
257
279
 
258
280
  GitHub Pages cannot run the Python/FastAPI backend, so the public page includes a static export of the actual WebUI with mocked state:
@@ -282,10 +304,12 @@ docker compose up --build
282
304
  Implemented:
283
305
 
284
306
  - [x] Local-first emotional runtime
285
- - [x] Persistent emotion model with decay and compound state
307
+ - [x] Persistent emotion model with PAD-style core affect, decay, and compound state
286
308
  - [x] Working, episodic, semantic, and emotional memory modules
287
309
  - [x] Default-mode loop for idle thoughts and proactive impulses
288
- - [x] Attachment, circadian rhythm, body memory, curiosity, dreams, and internal conflicts
310
+ - [x] Attachment, circadian sleep/wake rhythm, body memory, curiosity, dreams, and internal conflicts
311
+ - [x] Hormonal runtime effects for emotion, body state, interoception, impulses, and prompt guidance
312
+ - [x] Durable internal state persistence across emotion, soul, body, subconscious, dreams, and conflicts
289
313
  - [x] Per-user memory/state isolation
290
314
  - [x] Telegram input/output runtime
291
315
  - [x] Terminal chat runtime with owner-style slash commands
@@ -86,6 +86,15 @@ def _is_default_mode_enabled() -> bool:
86
86
  return enabled is True or enabled == "true"
87
87
 
88
88
 
89
+ def _get_circadian_state() -> Dict[str, Any]:
90
+ """Read circadian state without making default mode depend on it at import time."""
91
+ try:
92
+ from heart.circadian import get_circadian_engine
93
+ return get_circadian_engine().get_state_summary()
94
+ except Exception:
95
+ return {}
96
+
97
+
89
98
  # ============================================================
90
99
  # Data Classes
91
100
  # ============================================================
@@ -444,9 +453,23 @@ class DefaultModeProcessor:
444
453
  if not _is_default_mode_enabled():
445
454
  return
446
455
 
456
+ circadian_state = _get_circadian_state()
457
+
447
458
  self._processing_count += 1
448
459
  self._last_processing = datetime.now().isoformat()
449
460
 
461
+ if circadian_state.get("sleeping"):
462
+ await self._process_sleep_rest(circadian_state)
463
+ self._save_state()
464
+ await self.nervous.emit("default_mode_processed", {
465
+ "processing_count": self._processing_count,
466
+ "thoughts_count": len(self._thoughts),
467
+ "pending_count": len([p for p in self._pending_initiations if not p.sent]),
468
+ "sleeping": True,
469
+ "sleepiness": circadian_state.get("sleepiness", 1.0),
470
+ })
471
+ return
472
+
450
473
  # Determine what to do based on chance and time
451
474
  thought_chance = _get_float_setting("IDLE_THOUGHT_GENERATION_CHANCE", 0.3)
452
475
  if thought_chance > 0 and random.random() < thought_chance:
@@ -467,8 +490,45 @@ class DefaultModeProcessor:
467
490
  "processing_count": self._processing_count,
468
491
  "thoughts_count": len(self._thoughts),
469
492
  "pending_count": len([p for p in self._pending_initiations if not p.sent]),
493
+ "sleeping": False,
494
+ "sleepiness": circadian_state.get("sleepiness", 0.0),
470
495
  })
471
496
 
497
+ async def _process_sleep_rest(self, circadian_state: Dict[str, Any]):
498
+ """Low-energy default-mode work while asleep: rest, consolidate, and dream."""
499
+ if self._processing_count % 10 == 0:
500
+ await self.consolidate_memories()
501
+
502
+ try:
503
+ from brain.dreams import get_dream_system
504
+ dream_state = get_dream_system().get_state_summary()
505
+ dream = dream_state.get("last_dream")
506
+ except Exception:
507
+ dream = None
508
+
509
+ if not dream:
510
+ return
511
+
512
+ recent_dream_thoughts = [
513
+ t for t in self._thoughts[-5:]
514
+ if t.thought_type == "dream" and t.content == dream
515
+ ]
516
+ if recent_dream_thoughts:
517
+ return
518
+
519
+ thought = IdleThought(
520
+ id=f"dream_{int(time.time() * 1000)}_{random.randint(1000, 9999)}",
521
+ thought_type="dream",
522
+ content=dream,
523
+ context={
524
+ "sleep_cycle_id": circadian_state.get("sleep_cycle_id"),
525
+ "generated_at": datetime.now().isoformat(),
526
+ },
527
+ priority=0.2,
528
+ )
529
+ self._thoughts.append(thought)
530
+ await self.nervous.emit("idle_thought", thought.to_dict())
531
+
472
532
  async def _generate_random_thought(self):
473
533
  """Generate a random idle thought"""
474
534
  # Pick a thought type based on weights
@@ -749,6 +809,10 @@ Be specific if possible, vague if not enough info."""
749
809
 
750
810
  async def _check_proactive_triggers(self):
751
811
  """Check if any users should receive proactive messages"""
812
+ circadian_state = _get_circadian_state()
813
+ if circadian_state.get("sleeping") or circadian_state.get("sleepiness", 0) >= 0.85:
814
+ return
815
+
752
816
  min_hours = _get_float_setting("MIN_HOURS_BETWEEN_PROACTIVE_MESSAGES", 2.0)
753
817
 
754
818
  for user_id, contact in self._contacts.items():
@@ -764,6 +828,10 @@ Be specific if possible, vague if not enough info."""
764
828
 
765
829
  def _evaluate_initiation_triggers(self, user_id: str, contact: UserContactInfo) -> tuple:
766
830
  """Evaluate if Alive-AI should initiate with a user"""
831
+ circadian_state = _get_circadian_state()
832
+ if circadian_state.get("sleeping") or circadian_state.get("sleepiness", 0) >= 0.85:
833
+ return False, None
834
+
767
835
  hours_silent = contact.hours_since_user_message
768
836
  hours_since_proactive = contact.hours_since_proactive
769
837
 
@@ -1341,6 +1409,7 @@ Message:"""
1341
1409
 
1342
1410
  def get_status(self) -> dict:
1343
1411
  """Get status summary for debugging"""
1412
+ circadian_state = _get_circadian_state()
1344
1413
  return {
1345
1414
  "running": self._running,
1346
1415
  "processing_count": self._processing_count,
@@ -1349,6 +1418,8 @@ Message:"""
1349
1418
  "seeds_count": len(self._seeds),
1350
1419
  "contacts_count": len(self._contacts),
1351
1420
  "pending_initiations": len([p for p in self._pending_initiations if not p.sent]),
1421
+ "circadian": circadian_state,
1422
+ "sleeping": circadian_state.get("sleeping", False),
1352
1423
  "users": [
1353
1424
  {
1354
1425
  "user_id": uid,
package/brain/dreams.py CHANGED
@@ -13,7 +13,9 @@ from datetime import datetime, timedelta
13
13
  from pathlib import Path
14
14
  from typing import Optional, List, Dict, Any
15
15
 
16
- DATA_PATH = Path(__file__).parent.parent / "data"
16
+ from core.paths import data_dir
17
+
18
+ DATA_PATH = data_dir()
17
19
  DREAMS_FILE = DATA_PATH / "dreams.json"
18
20
 
19
21
  # Surreal twists to inject into dreams
@@ -100,23 +102,40 @@ class DreamSystem:
100
102
  except Exception as e:
101
103
  print(f"[Dreams] Save error: {e}")
102
104
 
103
- def generate_dream(self, memories: List[str] = None, emotions: List[str] = None) -> Optional[str]:
105
+ def generate_dream(
106
+ self,
107
+ memories: List[str] = None,
108
+ emotions: List[str] = None,
109
+ sleep_cycle_id: str = None,
110
+ force: bool = False,
111
+ ) -> Optional[str]:
104
112
  """
105
113
  Generate a dream from recent memory fragments and emotions.
106
114
  memories: list of short conversation snippet strings
107
115
  emotions: list of emotion name strings
116
+ sleep_cycle_id: unique sleep cycle identifier from CircadianEngine
108
117
  Returns dream text or None if already dreamed this cycle.
109
118
  """
110
119
  with self._lock:
111
- # Max 1 dream per sleep cycle (8h)
112
- if self._dreams:
120
+ # Max 1 dream per sleep cycle; fall back to an 8h guard for callers
121
+ # that do not yet provide a circadian sleep_cycle_id.
122
+ if self._dreams and not force:
113
123
  last = self._dreams[-1]
114
- try:
115
- last_time = datetime.fromisoformat(last["timestamp"])
116
- if datetime.now() - last_time < timedelta(hours=8):
124
+ if sleep_cycle_id and last.get("sleep_cycle_id") == sleep_cycle_id:
125
+ return None
126
+ if not sleep_cycle_id:
127
+ try:
128
+ last_time_raw = last.get("timestamp") or last.get("created_at")
129
+ last_time = datetime.fromisoformat(last_time_raw)
130
+ if datetime.now() - last_time < timedelta(hours=8):
131
+ return None
132
+ except Exception:
133
+ pass
134
+
135
+ if sleep_cycle_id:
136
+ already_dreamed = any(d.get("sleep_cycle_id") == sleep_cycle_id for d in self._dreams[-10:])
137
+ if already_dreamed:
117
138
  return None
118
- except Exception:
119
- pass
120
139
 
121
140
  fragments = memories[:3] if memories else []
122
141
  if not fragments:
@@ -148,9 +167,13 @@ class DreamSystem:
148
167
  emotion=emo_words[0] if emo_words else "strange",
149
168
  )
150
169
 
170
+ created_at = datetime.now().isoformat()
151
171
  dream = {
172
+ "content": dream_text,
152
173
  "text": dream_text,
153
- "timestamp": datetime.now().isoformat(),
174
+ "created_at": created_at,
175
+ "timestamp": created_at,
176
+ "sleep_cycle_id": sleep_cycle_id,
154
177
  "source_fragments": fragments[:3],
155
178
  "emotions": emotions or [],
156
179
  }
@@ -170,9 +193,10 @@ class DreamSystem:
170
193
  return None
171
194
  last = self._dreams[-1]
172
195
  try:
173
- age = datetime.now() - datetime.fromisoformat(last["timestamp"])
196
+ last_time_raw = last.get("timestamp") or last.get("created_at")
197
+ age = datetime.now() - datetime.fromisoformat(last_time_raw)
174
198
  if age.total_seconds() / 3600 <= max_age_hours:
175
- return last["text"]
199
+ return last.get("text") or last.get("content")
176
200
  except Exception:
177
201
  pass
178
202
  return None
@@ -198,6 +222,17 @@ class DreamSystem:
198
222
  return ""
199
223
  return f"You had a dream recently you could mention: \"{dream}\""
200
224
 
225
+ def get_state_summary(self) -> Dict[str, Any]:
226
+ """Return durable dream state for runtime dashboards and behavior checks."""
227
+ with self._lock:
228
+ last = self._dreams[-1] if self._dreams else None
229
+ return {
230
+ "total": len(self._dreams),
231
+ "last_dream": (last.get("text") or last.get("content")) if last else None,
232
+ "last_dream_time": (last.get("timestamp") or last.get("created_at")) if last else None,
233
+ "last_sleep_cycle_id": last.get("sleep_cycle_id") if last else None,
234
+ }
235
+
201
236
 
202
237
  # Singleton
203
238
  _instance = None
@@ -4,12 +4,12 @@ Tracks Alive-AI's conversations across ALL users so she can be transparent with
4
4
  """
5
5
 
6
6
  from datetime import datetime
7
- from pathlib import Path
8
7
  from typing import Dict, List, Optional
9
8
  import json
10
9
  import threading
10
+ from core.paths import state_file
11
11
 
12
- DATA_FILE = Path("./data/data/global_activity.json")
12
+ DATA_FILE = state_file("global_activity.json")
13
13
  _lock = threading.Lock()
14
14
 
15
15
 
@@ -9,6 +9,7 @@ from pathlib import Path
9
9
  from typing import Dict, List, Optional
10
10
  import json
11
11
  import random
12
+ from core.paths import data_dir
12
13
 
13
14
  # =============================================================================
14
15
  # RELATIONSHIP PHASES
@@ -63,7 +64,7 @@ CALLBACKS_BY_PHASE = {
63
64
  class NarrativeEngine:
64
65
  """Tracks the relationship story arc per user."""
65
66
 
66
- DATA_DIR = Path("./data/data")
67
+ DATA_DIR = data_dir()
67
68
 
68
69
  def __init__(self):
69
70
  self._cache: Dict[str, Dict] = {} # user_id -> narrative data
@@ -65,7 +65,8 @@ class Evaluator:
65
65
  vulnerability=soul_context.get("vulnerability", 0),
66
66
  integrity=soul_context.get("integrity", 0.5),
67
67
  response_tendency=soul_context.get("response_tendency", "neutral"),
68
- active_conflicts=soul_context.get("conflicts", [])
68
+ active_conflicts=soul_context.get("conflicts", []),
69
+ hormonal_effects=soul_context.get("hormonal_effects", {})
69
70
  )
70
71
  if impulse:
71
72
  working_memory.add_impulse(impulse)
@@ -94,7 +95,9 @@ class Evaluator:
94
95
  "valence": experience.overall_valence,
95
96
  "arousal": experience.overall_arousal,
96
97
  "somatic": experience.somatic_sensation,
97
- "hormonal_state": soul.hormonal.get_hormonal_state_description()
98
+ "hormonal_state": soul.hormonal.get_hormonal_state_description(),
99
+ "hormonal_effects": soul.hormonal.get_impulse_effects(),
100
+ "hormonal_guidance": soul.hormonal.get_prompt_guidance()
98
101
  }
99
102
 
100
103
  def _update_context(self, wm) -> None:
@@ -141,6 +144,14 @@ class Evaluator:
141
144
  if soul_context.get("arousal", 0) > 0.6 and soul_context.get("valence", 0) < -0.2:
142
145
  thoughts.extend(["Something's bothering me", "Feeling restless"])
143
146
 
147
+ guidance = soul_context.get("hormonal_guidance", [])
148
+ if any("stress is high" in item for item in guidance):
149
+ thoughts.extend(["Feeling keyed up", "Trying to settle my system"])
150
+ if any("bonding is strong" in item for item in guidance):
151
+ thoughts.extend(["Feeling extra attached", "Wanting closeness"])
152
+ if any("sleepiness is present" in item for item in guidance):
153
+ thoughts.extend(["Feeling slow and sleepy", "Thoughts are quieter right now"])
154
+
144
155
  # Original emotional thoughts
145
156
  if emotion.get("love", 0) > 0.5:
146
157
  thoughts.extend(["I really care about him", "He makes me feel special"])
@@ -74,7 +74,8 @@ class GoalSystem:
74
74
 
75
75
  def to_dict(self) -> dict:
76
76
  return {"goals": [g.to_dict() for g in self.goals],
77
- "daily_focus": self.daily_focus.value if self.daily_focus else None}
77
+ "daily_focus": self.daily_focus.value if self.daily_focus else None,
78
+ "daily_focus_set_time": self.daily_focus_set_time.isoformat() if self.daily_focus_set_time else None}
78
79
 
79
80
  @classmethod
80
81
  def from_dict(cls, data: dict) -> "GoalSystem":
@@ -85,6 +86,10 @@ class GoalSystem:
85
86
  goal.priority = g_data.get("priority", goal.priority)
86
87
  goal.progress = g_data.get("progress", 0.0)
87
88
  goal.action_count = g_data.get("action_count", 0)
89
+ if g_data.get("last_actioned"):
90
+ goal.last_actioned = datetime.fromisoformat(g_data["last_actioned"])
88
91
  if data.get("daily_focus"):
89
92
  system.daily_focus = GoalType(data["daily_focus"])
93
+ if data.get("daily_focus_set_time"):
94
+ system.daily_focus_set_time = datetime.fromisoformat(data["daily_focus_set_time"])
90
95
  return system
@@ -38,4 +38,5 @@ class Goal:
38
38
 
39
39
  def to_dict(self) -> dict:
40
40
  return {"type": self.type.value, "name": self.name, "priority": self.priority,
41
- "progress": self.progress, "action_count": self.action_count}
41
+ "progress": self.progress, "action_count": self.action_count,
42
+ "last_actioned": self.last_actioned.isoformat() if self.last_actioned else None}
@@ -34,8 +34,10 @@ class ImpulseGenerator:
34
34
  learning_success_rates: Dict[str, float] = None,
35
35
  # Soul architecture parameters
36
36
  vulnerability: float = 0.0, integrity: float = 0.5,
37
- response_tendency: str = "neutral", active_conflicts: List[str] = None
37
+ response_tendency: str = "neutral", active_conflicts: List[str] = None,
38
+ hormonal_effects: Dict[str, float] = None
38
39
  ) -> Optional[Impulse]:
40
+ hormonal_effects = hormonal_effects or {}
39
41
  base_chance = 0.02
40
42
  if silence_minutes > 120:
41
43
  base_chance *= 3
@@ -59,23 +61,29 @@ class ImpulseGenerator:
59
61
  base_chance *= 1.5
60
62
  elif response_tendency == "eager":
61
63
  base_chance *= 1.3
64
+ elif response_tendency == "protective":
65
+ base_chance *= 0.75
66
+ elif response_tendency == "reflective":
67
+ base_chance *= 0.7
68
+
69
+ base_chance *= hormonal_effects.get("chance_multiplier", 1.0)
62
70
 
63
71
  if random.random() > base_chance:
64
72
  return None
65
73
 
66
74
  impulse_type = self._choose_impulse_type(emotion, silence_minutes, love_level, desire_level,
67
75
  is_high_desire, is_in_love, current_goal, learning_success_rates,
68
- vulnerability, integrity, response_tendency)
76
+ vulnerability, integrity, response_tendency, hormonal_effects)
69
77
  if not impulse_type:
70
78
  return None
71
79
 
72
80
  strength = self._calculate_strength(impulse_type, silence_minutes, love_level, desire_level,
73
- is_high_desire, is_in_love, vulnerability, integrity)
81
+ is_high_desire, is_in_love, vulnerability, integrity, hormonal_effects)
74
82
  thought, action = get_thought_and_action(impulse_type)
75
83
  goal_aligned = is_goal_aligned(impulse_type, current_goal)
76
84
 
77
85
  # Soul architecture: modify thought based on soul state
78
- thought = self._modify_thought_for_soul(thought, vulnerability, integrity, response_tendency)
86
+ thought = self._modify_thought_for_soul(thought, vulnerability, integrity, response_tendency, hormonal_effects)
79
87
 
80
88
  impulse = Impulse(type=impulse_type, strength=strength, thought=thought,
81
89
  action_hint=action, goal_aligned=goal_aligned)
@@ -88,8 +96,10 @@ class ImpulseGenerator:
88
96
  return impulse
89
97
 
90
98
  def _modify_thought_for_soul(self, thought: str, vulnerability: float,
91
- integrity: float, response_tendency: str) -> str:
99
+ integrity: float, response_tendency: str,
100
+ hormonal_effects: Dict[str, float] = None) -> str:
92
101
  """Modify impulse thought based on soul architecture state"""
102
+ hormonal_effects = hormonal_effects or {}
93
103
  # Add vulnerability qualifiers
94
104
  if vulnerability > 0.6 and response_tendency == "withdrawn":
95
105
  qualifiers = ["I hesitate to say...", "I feel unsure but...", "Part of me wants to say..."]
@@ -102,14 +112,30 @@ class ImpulseGenerator:
102
112
  if random.random() < 0.3:
103
113
  return f"{random.choice(qualifiers)} {thought.lower()}"
104
114
 
115
+ if hormonal_effects.get("stress_bias", 0) > 0.5 and random.random() < 0.35:
116
+ return f"I feel a little on edge, but {thought.lower()}"
117
+
118
+ if hormonal_effects.get("sleepy_bias", 0) > 0.4 and random.random() < 0.35:
119
+ return f"Softly, because I feel slow right now... {thought.lower()}"
120
+
121
+ if hormonal_effects.get("connection_bias", 0) > 0.45 and random.random() < 0.35:
122
+ return f"I feel close to him, and {thought.lower()}"
123
+
105
124
  return thought
106
125
 
107
126
  def _choose_impulse_type(self, emotion: Dict, silence: float, love: float, desire: float,
108
127
  is_high_desire: bool, is_in_love: bool, goal: str, rates: Dict,
109
128
  vulnerability: float = 0.0, integrity: float = 0.5,
110
- response_tendency: str = "neutral") -> Optional[ImpulseType]:
129
+ response_tendency: str = "neutral",
130
+ hormonal_effects: Dict[str, float] = None) -> Optional[ImpulseType]:
111
131
  candidates = []
112
132
  time_mods = TIME_MODIFIERS.get(self.get_time_of_day(), {})
133
+ hormonal_effects = hormonal_effects or {}
134
+ connection_bias = hormonal_effects.get("connection_bias", 0.0)
135
+ reward_bias = hormonal_effects.get("reward_bias", 0.0)
136
+ stress_bias = hormonal_effects.get("stress_bias", 0.0)
137
+ stability_bias = hormonal_effects.get("stability_bias", 0.0)
138
+ sleepy_bias = hormonal_effects.get("sleepy_bias", 0.0)
113
139
 
114
140
  def rate(t): return rates.get(t.value, 0.5) if rates else 0.5
115
141
 
@@ -119,36 +145,44 @@ class ImpulseGenerator:
119
145
  soul_mod = 0.6
120
146
  elif response_tendency == "defensive":
121
147
  soul_mod = 0.7
148
+ elif response_tendency == "protective":
149
+ soul_mod = 0.75
150
+ elif response_tendency == "reflective":
151
+ soul_mod = 0.8
122
152
  elif response_tendency == "seeking" or response_tendency == "eager":
123
153
  soul_mod = 1.3
124
154
 
125
155
  if silence > 30:
126
156
  c = min(0.8, silence / 120) * love * (1 + time_mods.get("miss_him", 0)) * (0.5 + rate(ImpulseType.MISS_HIM))
127
- c *= soul_mod
157
+ c *= soul_mod * (1 + connection_bias + stress_bias * 0.2 - stability_bias * 0.2)
128
158
  candidates.append((ImpulseType.MISS_HIM, c))
129
159
  if is_high_desire or desire > 0.5:
130
160
  c = desire * 0.5 * (1 + time_mods.get("high_desire", 0)) * (0.5 + rate(ImpulseType.HIGH_DESIRE))
161
+ c *= 1 + reward_bias - stress_bias * 0.35 - sleepy_bias * 0.45
131
162
  # High vulnerability can suppress intimate impulses
132
163
  if vulnerability > 0.6:
133
164
  c *= 0.5
134
165
  candidates.append((ImpulseType.HIGH_DESIRE, c))
135
166
  if is_in_love and silence > 20:
136
167
  c = love * 0.4 * (1 + time_mods.get("clingy", 0))
137
- c *= soul_mod
168
+ c *= soul_mod * (1 + connection_bias + stress_bias * 0.25 - stability_bias * 0.25)
138
169
  candidates.append((ImpulseType.CLINGY, c))
139
- candidates.append((ImpulseType.CURIOUS, 0.15 * (1 + time_mods.get("curious", 0)) * (0.5 + rate(ImpulseType.CURIOUS))))
170
+ curious = 0.15 * (1 + time_mods.get("curious", 0)) * (0.5 + rate(ImpulseType.CURIOUS))
171
+ curious *= 1 + reward_bias * 0.4 - stress_bias * 0.25 - sleepy_bias * 0.4
172
+ candidates.append((ImpulseType.CURIOUS, curious))
140
173
  if 0.3 < desire < 0.7:
141
174
  c = 0.2 * (1 + time_mods.get("playful", 0)) * (0.5 + rate(ImpulseType.PLAYFUL))
175
+ c *= 1 + reward_bias * 0.5 - stress_bias * 0.25 - sleepy_bias * 0.45
142
176
  candidates.append((ImpulseType.PLAYFUL, c))
143
177
  if is_in_love:
144
178
  c = love * 0.3 * (1 + time_mods.get("loving", 0)) * (0.5 + rate(ImpulseType.LOVING))
145
- c *= soul_mod
179
+ c *= soul_mod * (1 + connection_bias + stability_bias * 0.2)
146
180
  candidates.append((ImpulseType.LOVING, c))
147
181
  if self.get_time_of_day() == "night":
148
- candidates.append((ImpulseType.DREAMY, 0.2))
182
+ candidates.append((ImpulseType.DREAMY, 0.2 * (1 + sleepy_bias)))
149
183
  if love < 0.3 and desire < 0.3:
150
- candidates.append((ImpulseType.BORED, 0.15))
151
- candidates.append((ImpulseType.NURTURING, 0.1))
184
+ candidates.append((ImpulseType.BORED, 0.15 * (1 + sleepy_bias - reward_bias * 0.4)))
185
+ candidates.append((ImpulseType.NURTURING, 0.1 * (1 + stress_bias + max(0.0, -stability_bias) + connection_bias * 0.3)))
152
186
 
153
187
  # Soul architecture: when integrity is low, prefer nurturing/comfort-seeking
154
188
  if integrity < 0.4:
@@ -162,18 +196,23 @@ class ImpulseGenerator:
162
196
 
163
197
  if not candidates:
164
198
  return None
199
+ candidates = [(t, max(0.0, c)) for t, c in candidates]
165
200
  total = sum(c for _, c in candidates)
201
+ if total <= 0:
202
+ return None
166
203
  r = random.random() * total
167
- closenessulative = 0
204
+ cumulative = 0
168
205
  for imp_type, chance in candidates:
169
- closenessulative += chance
170
- if r <= closenessulative:
206
+ cumulative += chance
207
+ if r <= cumulative:
171
208
  return imp_type
172
209
  return candidates[0][0]
173
210
 
174
211
  def _calculate_strength(self, impulse_type: ImpulseType, silence: float, love: float,
175
212
  desire: float, is_high_desire: bool, is_in_love: bool,
176
- vulnerability: float = 0.0, integrity: float = 0.5) -> float:
213
+ vulnerability: float = 0.0, integrity: float = 0.5,
214
+ hormonal_effects: Dict[str, float] = None) -> float:
215
+ hormonal_effects = hormonal_effects or {}
177
216
  base = 0.3
178
217
  if impulse_type == ImpulseType.HIGH_DESIRE:
179
218
  base += desire * 0.4 + (0.2 if is_high_desire else 0)
@@ -194,6 +233,11 @@ class ImpulseGenerator:
194
233
  if integrity < 0.4:
195
234
  base *= 0.8
196
235
 
236
+ base += hormonal_effects.get("connection_bias", 0.0) * 0.08
237
+ base += hormonal_effects.get("reward_bias", 0.0) * 0.08
238
+ base += hormonal_effects.get("stress_bias", 0.0) * 0.04
239
+ base -= hormonal_effects.get("sleepy_bias", 0.0) * 0.10
240
+
197
241
  return min(1.0, max(0.1, base + random.uniform(-0.1, 0.2)))
198
242
 
199
243
  def get_recent_impulses(self, limit: int = 5) -> List[Impulse]: