alive-ai 0.1.10 → 0.1.11
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 +23 -3
- package/brain/default_mode.py +71 -0
- package/brain/dreams.py +47 -12
- package/brain/global_activity.py +2 -2
- package/brain/narrative.py +2 -2
- package/brain/subconscious/evaluation.py +13 -2
- package/brain/subconscious/goal_system.py +6 -1
- package/brain/subconscious/goals.py +2 -1
- package/brain/subconscious/impulse_generator.py +61 -17
- package/brain/subconscious/loop.py +123 -34
- package/brain/subconscious/working_memory.py +9 -1
- package/core/message_handler.py +18 -2
- package/core/state.py +42 -1
- package/core/thinking.py +128 -14
- package/demo/index.html +353 -44
- package/docs/dashboard.html +46 -6
- package/docs/index.html +724 -172
- package/heart/afterglow.py +2 -2
- package/heart/attachment.py +2 -1
- package/heart/circadian.py +253 -42
- package/heart/complex_emotions.py +9 -2
- package/heart/conflicts.py +148 -5
- package/heart/core.py +262 -35
- package/heart/emotional_state.py +173 -15
- package/heart/hormonal.py +159 -6
- package/heart/inconsistency.py +25 -12
- package/heart/interoception.py +7 -0
- package/heart/phantom_somatic.py +2 -2
- package/heart/predictive.py +16 -4
- package/heart/somatic.py +148 -4
- package/heart/soul.py +46 -21
- package/heart/triggers.py +18 -0
- package/heart/unconscious.py +121 -1
- package/package.json +1 -1
- package/pyproject.toml +1 -1
- package/webui/app.py +4 -16
package/README.md
CHANGED
|
@@ -19,6 +19,24 @@ 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
|
+
| 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. |
|
|
37
|
+
|
|
38
|
+
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.
|
|
39
|
+
|
|
22
40
|
## Quick Start
|
|
23
41
|
|
|
24
42
|
```bash
|
|
@@ -252,7 +270,7 @@ The real WebUI streams local runtime state over Server-Sent Events and shows:
|
|
|
252
270
|
- recent thoughts and idle processing,
|
|
253
271
|
- memory counters and uptime,
|
|
254
272
|
- hormones and interoceptive body state,
|
|
255
|
-
- attachment, circadian rhythm, body memory, dreams, curiosity, and conflicts,
|
|
273
|
+
- attachment, circadian rhythm, sleepiness, body memory, dreams, curiosity, and conflicts,
|
|
256
274
|
- runtime health through local endpoints.
|
|
257
275
|
|
|
258
276
|
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 +300,12 @@ docker compose up --build
|
|
|
282
300
|
Implemented:
|
|
283
301
|
|
|
284
302
|
- [x] Local-first emotional runtime
|
|
285
|
-
- [x] Persistent emotion model with decay and compound state
|
|
303
|
+
- [x] Persistent emotion model with PAD-style core affect, decay, and compound state
|
|
286
304
|
- [x] Working, episodic, semantic, and emotional memory modules
|
|
287
305
|
- [x] Default-mode loop for idle thoughts and proactive impulses
|
|
288
|
-
- [x] Attachment, circadian rhythm, body memory, curiosity, dreams, and internal conflicts
|
|
306
|
+
- [x] Attachment, circadian sleep/wake rhythm, body memory, curiosity, dreams, and internal conflicts
|
|
307
|
+
- [x] Hormonal runtime effects for emotion, body state, interoception, impulses, and prompt guidance
|
|
308
|
+
- [x] Durable internal state persistence across emotion, soul, body, subconscious, dreams, and conflicts
|
|
289
309
|
- [x] Per-user memory/state isolation
|
|
290
310
|
- [x] Telegram input/output runtime
|
|
291
311
|
- [x] Terminal chat runtime with owner-style slash commands
|
package/brain/default_mode.py
CHANGED
|
@@ -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
|
-
|
|
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(
|
|
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
|
|
112
|
-
|
|
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
|
-
|
|
115
|
-
|
|
116
|
-
|
|
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
|
-
"
|
|
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
|
-
|
|
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
|
|
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
|
package/brain/global_activity.py
CHANGED
|
@@ -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 =
|
|
12
|
+
DATA_FILE = state_file("global_activity.json")
|
|
13
13
|
_lock = threading.Lock()
|
|
14
14
|
|
|
15
15
|
|
package/brain/narrative.py
CHANGED
|
@@ -5,10 +5,10 @@ enabling natural references to shared history and phase awareness.
|
|
|
5
5
|
"""
|
|
6
6
|
|
|
7
7
|
from datetime import datetime
|
|
8
|
-
from pathlib import Path
|
|
9
8
|
from typing import Dict, List, Optional
|
|
10
9
|
import json
|
|
11
10
|
import random
|
|
11
|
+
from core.paths import data_dir
|
|
12
12
|
|
|
13
13
|
# =============================================================================
|
|
14
14
|
# RELATIONSHIP PHASES
|
|
@@ -63,7 +63,7 @@ CALLBACKS_BY_PHASE = {
|
|
|
63
63
|
class NarrativeEngine:
|
|
64
64
|
"""Tracks the relationship story arc per user."""
|
|
65
65
|
|
|
66
|
-
DATA_DIR =
|
|
66
|
+
DATA_DIR = data_dir()
|
|
67
67
|
|
|
68
68
|
def __init__(self):
|
|
69
69
|
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
|
|
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"
|
|
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
|
-
|
|
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
|
-
|
|
204
|
+
cumulative = 0
|
|
168
205
|
for imp_type, chance in candidates:
|
|
169
|
-
|
|
170
|
-
if r <=
|
|
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
|
|
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]:
|