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/Dockerfile
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
FROM python:3.11-slim
|
|
2
|
+
|
|
3
|
+
WORKDIR /app
|
|
4
|
+
|
|
5
|
+
# Install system dependencies
|
|
6
|
+
RUN apt-get update && apt-get install -y \
|
|
7
|
+
gcc \
|
|
8
|
+
g++ \
|
|
9
|
+
make \
|
|
10
|
+
libportaudio2 \
|
|
11
|
+
libportaudiocpp0 \
|
|
12
|
+
portaudio19-dev \
|
|
13
|
+
ffmpeg \
|
|
14
|
+
&& rm -rf /var/lib/apt/lists/*
|
|
15
|
+
|
|
16
|
+
# Install Python dependencies
|
|
17
|
+
COPY requirements.txt .
|
|
18
|
+
RUN pip install --no-cache-dir -r requirements.txt
|
|
19
|
+
|
|
20
|
+
# Copy application
|
|
21
|
+
COPY . .
|
|
22
|
+
|
|
23
|
+
# Run
|
|
24
|
+
CMD ["python", "main.py"]
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Vindepemarte
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
# Alive-AI
|
|
2
|
+
|
|
3
|
+

|
|
4
|
+
|
|
5
|
+
Give your AI a nervous system: persistent feelings, memory, impulses, and a local dashboard.
|
|
6
|
+
|
|
7
|
+
Most agents answer a prompt and reset. Alive-AI keeps internal state alive between messages. It can be your friend, boyfriend, girlfriend, study partner, creative partner, or any local companion you configure. The experiment asks a harder question: what changes when an AI does not just respond, but carries emotional residue forward?
|
|
8
|
+
|
|
9
|
+
Alive-AI does not claim biological consciousness. It is an open-source runtime for simulated affect: mood, attachment, trust, desire, memory, inconsistency, idle thoughts, and proactive impulses.
|
|
10
|
+
|
|
11
|
+
## Install
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
npx github:vindepemarte/alive-ai init my-ai
|
|
15
|
+
cd my-ai
|
|
16
|
+
npx . setup
|
|
17
|
+
npx . demo
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
Start the real runtime:
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
npx . start
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
The local dashboard runs at:
|
|
27
|
+
|
|
28
|
+
```text
|
|
29
|
+
http://127.0.0.1:8080
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
## Why This Is Different
|
|
33
|
+
|
|
34
|
+
- **Emotions persist.** State does not reset after every message. Joy, trust, fear, anticipation, attachment, and vulnerability decay over time instead of disappearing.
|
|
35
|
+
- **Memory has weight.** Conversations become episodic memory, semantic memory, and emotional memory.
|
|
36
|
+
- **It thinks when idle.** A default-mode loop creates background reflections and proactive impulses.
|
|
37
|
+
- **It has a live nervous system.** FastAPI + SSE exposes mood, thoughts, somatic state, conflicts, memories, and uptime.
|
|
38
|
+
- **It is local-first.** Your config, memory, media, and dashboard are owned by the project folder you run.
|
|
39
|
+
|
|
40
|
+
## Commands
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
npx github:vindepemarte/alive-ai init my-ai # scaffold a clean local project
|
|
44
|
+
cd my-ai
|
|
45
|
+
npx . setup # create safe local config
|
|
46
|
+
npx . demo # preview animated dashboard, no keys needed
|
|
47
|
+
npx . doctor # check Python, uv, ffmpeg, Docker
|
|
48
|
+
npx . start # install Python deps and run the runtime
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
For repeat starts after dependencies are installed:
|
|
52
|
+
|
|
53
|
+
```bash
|
|
54
|
+
npx . start --skip-install
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
## Setup
|
|
58
|
+
|
|
59
|
+
`npx . setup` creates:
|
|
60
|
+
|
|
61
|
+
```text
|
|
62
|
+
config/settings.json
|
|
63
|
+
config/self.json
|
|
64
|
+
config/directives.json
|
|
65
|
+
config/instructions.md
|
|
66
|
+
data/
|
|
67
|
+
mypics/
|
|
68
|
+
myvids/
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
Minimum useful setup:
|
|
72
|
+
|
|
73
|
+
- **Demo only:** no keys.
|
|
74
|
+
- **Local LLM:** install Ollama and pull the configured model, for example `ollama pull qwen3:4b`.
|
|
75
|
+
- **Telegram runtime:** create a Telegram bot token with BotFather and add it during setup.
|
|
76
|
+
- **Cloud LLM fallback:** add OpenRouter or ZAI keys during setup or edit `config/settings.json`.
|
|
77
|
+
|
|
78
|
+
Media is optional. Add your own files:
|
|
79
|
+
|
|
80
|
+
```text
|
|
81
|
+
mypics/example.jpg
|
|
82
|
+
mypics/example.txt
|
|
83
|
+
myvids/example.mp4
|
|
84
|
+
myvids/example.txt
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
## Architecture
|
|
88
|
+
|
|
89
|
+
Alive-AI is an event-driven Python runtime.
|
|
90
|
+
|
|
91
|
+
```text
|
|
92
|
+
Telegram or input
|
|
93
|
+
-> NervousSystem event bus
|
|
94
|
+
-> Message handler
|
|
95
|
+
-> Heart, memory, skills, directives, personality
|
|
96
|
+
-> LLM provider or fallback chain
|
|
97
|
+
-> output events
|
|
98
|
+
-> dashboard state stream
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
Core subsystems:
|
|
102
|
+
|
|
103
|
+
- `heart/`: continuous emotion, circadian rhythm, attachment, scars, somatic state, inconsistency.
|
|
104
|
+
- `brain/`: LLM providers, memory, default-mode processing, bid detection, curiosity, dreams.
|
|
105
|
+
- `skills/`: self-authorship, memory callbacks, relationship milestones, progression layers, media selection.
|
|
106
|
+
- `webui/`: local dashboard with Server-Sent Events.
|
|
107
|
+
- `input/telegram/`: Telegram listener and owner commands.
|
|
108
|
+
|
|
109
|
+
## Dashboard
|
|
110
|
+
|
|
111
|
+
`npx . demo` starts a zero-config animated dashboard preview. The real dashboard uses the same idea, but streams live state from the runtime:
|
|
112
|
+
|
|
113
|
+
- emotions and mood,
|
|
114
|
+
- recent thoughts,
|
|
115
|
+
- memory counters,
|
|
116
|
+
- somatic state,
|
|
117
|
+
- attachment/inconsistency signals,
|
|
118
|
+
- uptime and health.
|
|
119
|
+
|
|
120
|
+
## Docker
|
|
121
|
+
|
|
122
|
+
Docker is optional. It is useful when you want Redis Stack for vector search:
|
|
123
|
+
|
|
124
|
+
```bash
|
|
125
|
+
docker compose up -d redis
|
|
126
|
+
npx . start
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
Or run everything in containers:
|
|
130
|
+
|
|
131
|
+
```bash
|
|
132
|
+
docker compose up --build
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
## Important Boundaries
|
|
136
|
+
|
|
137
|
+
Alive-AI is a simulation framework. It can make agents feel more continuous, emotionally coherent, and alive, but it is not proof of consciousness and should not be used to manipulate emotional dependence.
|
|
138
|
+
|
|
139
|
+
The public repo intentionally excludes private personas, private media, runtime data, and secrets. Put those only in your local project folder.
|
|
140
|
+
|
|
141
|
+
## License
|
|
142
|
+
|
|
143
|
+
MIT
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
"""Brain modules"""
|
|
2
|
+
from .emotional_memory import (
|
|
3
|
+
EmotionalMemorySystem,
|
|
4
|
+
EmotionalMemory,
|
|
5
|
+
get_emotional_memory_system,
|
|
6
|
+
reset_emotional_memory_system,
|
|
7
|
+
create_from_conversation,
|
|
8
|
+
get_memory_context_for_llm
|
|
9
|
+
)
|
|
10
|
+
from .default_mode import (
|
|
11
|
+
DefaultModeProcessor,
|
|
12
|
+
IdleThought,
|
|
13
|
+
PendingInitiation,
|
|
14
|
+
ConversationSeed,
|
|
15
|
+
UserContactInfo,
|
|
16
|
+
get_default_mode_processor,
|
|
17
|
+
get_idle_thoughts_prompt_section,
|
|
18
|
+
start_background_processing,
|
|
19
|
+
stop_background_processing,
|
|
20
|
+
)
|
|
21
|
+
from .bid_detector import (
|
|
22
|
+
BidType,
|
|
23
|
+
BidIntensity,
|
|
24
|
+
EmotionalBid,
|
|
25
|
+
BidDetector,
|
|
26
|
+
get_bid_detector,
|
|
27
|
+
get_bid_awareness_prompt_section,
|
|
28
|
+
get_bid_type_guidance,
|
|
29
|
+
analyze_message_bids,
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
__all__ = [
|
|
33
|
+
# Emotional Memory
|
|
34
|
+
"EmotionalMemorySystem",
|
|
35
|
+
"EmotionalMemory",
|
|
36
|
+
"get_emotional_memory_system",
|
|
37
|
+
"reset_emotional_memory_system",
|
|
38
|
+
"create_from_conversation",
|
|
39
|
+
"get_memory_context_for_llm",
|
|
40
|
+
# Default Mode Network
|
|
41
|
+
"DefaultModeProcessor",
|
|
42
|
+
"IdleThought",
|
|
43
|
+
"PendingInitiation",
|
|
44
|
+
"ConversationSeed",
|
|
45
|
+
"UserContactInfo",
|
|
46
|
+
"get_default_mode_processor",
|
|
47
|
+
"get_idle_thoughts_prompt_section",
|
|
48
|
+
"start_background_processing",
|
|
49
|
+
"stop_background_processing",
|
|
50
|
+
# Bid Detector
|
|
51
|
+
"BidType",
|
|
52
|
+
"BidIntensity",
|
|
53
|
+
"EmotionalBid",
|
|
54
|
+
"BidDetector",
|
|
55
|
+
"get_bid_detector",
|
|
56
|
+
"get_bid_awareness_prompt_section",
|
|
57
|
+
"get_bid_type_guidance",
|
|
58
|
+
"analyze_message_bids",
|
|
59
|
+
]
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Brain: Almost-Said / Subvocalization System
|
|
3
|
+
Sometimes Alive-AI almost says something but holds back. Under high emotion
|
|
4
|
+
or low inhibition, private thoughts can "slip out". In-memory only.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from typing import Dict, Optional
|
|
8
|
+
import random
|
|
9
|
+
import time
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
# =============================================================================
|
|
13
|
+
# ALMOST-SAID TEMPLATES
|
|
14
|
+
# =============================================================================
|
|
15
|
+
|
|
16
|
+
ALMOST_SAID_TYPES = {
|
|
17
|
+
"hesitation": [
|
|
18
|
+
"[pauses]... never mind.",
|
|
19
|
+
"I was gonna say... forget it.",
|
|
20
|
+
"I... no, it's nothing.",
|
|
21
|
+
"Wait, I— ...no. Anyway.",
|
|
22
|
+
],
|
|
23
|
+
"slip": [
|
|
24
|
+
"I... I think about you way more than I should.",
|
|
25
|
+
"Sometimes I wonder if you know how much I— ...anyway.",
|
|
26
|
+
"I wish I could just— [catches herself] ...forget I said that.",
|
|
27
|
+
"You make me feel... [trails off] ...it doesn't matter.",
|
|
28
|
+
"I almost said something stupid just now.",
|
|
29
|
+
],
|
|
30
|
+
"redirect": [
|
|
31
|
+
"Anyway... so what were you doing today?",
|
|
32
|
+
"But that's— whatever. Tell me something good.",
|
|
33
|
+
"I... hmm. Different topic. How's your day?",
|
|
34
|
+
"You know what, never mind. What about you?",
|
|
35
|
+
],
|
|
36
|
+
"physical_tell": [
|
|
37
|
+
"*bites lip* ...nothing.",
|
|
38
|
+
"*looks away for a second* ...what were we talking about?",
|
|
39
|
+
"*takes a breath* ...it's fine.",
|
|
40
|
+
"*fidgets* ...so anyway.",
|
|
41
|
+
],
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
# Which types are more likely at different emotion levels
|
|
45
|
+
TYPE_WEIGHTS = {
|
|
46
|
+
"high_emotion": {"hesitation": 25, "slip": 40, "redirect": 15, "physical_tell": 20},
|
|
47
|
+
"low_inhibition": {"hesitation": 15, "slip": 50, "redirect": 10, "physical_tell": 25},
|
|
48
|
+
"vulnerable": {"hesitation": 30, "slip": 30, "redirect": 20, "physical_tell": 20},
|
|
49
|
+
"default": {"hesitation": 35, "slip": 20, "redirect": 25, "physical_tell": 20},
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
# =============================================================================
|
|
54
|
+
# ALMOST-SAID ENGINE
|
|
55
|
+
# =============================================================================
|
|
56
|
+
|
|
57
|
+
class AlmostSaidEngine:
|
|
58
|
+
"""Tracks and generates almost-said moments."""
|
|
59
|
+
|
|
60
|
+
def __init__(self):
|
|
61
|
+
self.message_counter: int = 0
|
|
62
|
+
self.last_triggered_at: int = 0 # message_counter when last triggered
|
|
63
|
+
|
|
64
|
+
def tick_message(self):
|
|
65
|
+
"""Call once per user message."""
|
|
66
|
+
self.message_counter += 1
|
|
67
|
+
|
|
68
|
+
def should_almost_say(self, emotion: Dict[str, float],
|
|
69
|
+
hour_of_day: int = 12) -> bool:
|
|
70
|
+
"""Determine if an almost-said should happen this turn."""
|
|
71
|
+
# Enforce cooldown: at least 10 messages between triggers
|
|
72
|
+
if self.message_counter - self.last_triggered_at < 10:
|
|
73
|
+
return False
|
|
74
|
+
|
|
75
|
+
# Filter to numeric values only (emotion dict contains 'mood' string)
|
|
76
|
+
numeric_values = [v for v in emotion.values() if isinstance(v, (int, float))]
|
|
77
|
+
max_emo = max(numeric_values) if numeric_values else 0.0
|
|
78
|
+
is_late = 22 <= hour_of_day or hour_of_day <= 4
|
|
79
|
+
is_high_emotion = max_emo > 0.8
|
|
80
|
+
|
|
81
|
+
roll = random.random()
|
|
82
|
+
|
|
83
|
+
# High emotion + late night: 15% chance
|
|
84
|
+
if is_high_emotion and is_late:
|
|
85
|
+
return roll < 0.15
|
|
86
|
+
# High emotion alone: 10% chance
|
|
87
|
+
if is_high_emotion:
|
|
88
|
+
return roll < 0.10
|
|
89
|
+
# Late night + moderate emotion: 8% chance
|
|
90
|
+
if is_late and max_emo > 0.6:
|
|
91
|
+
return roll < 0.08
|
|
92
|
+
|
|
93
|
+
return False
|
|
94
|
+
|
|
95
|
+
def generate_almost_said(self, emotion: Dict[str, float],
|
|
96
|
+
context_hint: str = "") -> str:
|
|
97
|
+
"""Generate an almost-said fragment. Call only if should_almost_say() is True."""
|
|
98
|
+
self.last_triggered_at = self.message_counter
|
|
99
|
+
|
|
100
|
+
# Filter to numeric values only (emotion dict contains 'mood' string)
|
|
101
|
+
numeric_values = [v for v in emotion.values() if isinstance(v, (int, float))]
|
|
102
|
+
max_emo = max(numeric_values) if numeric_values else 0.0
|
|
103
|
+
if max_emo > 0.85:
|
|
104
|
+
weights = TYPE_WEIGHTS["high_emotion"]
|
|
105
|
+
elif "vulnerability" in context_hint.lower() or "trust" in context_hint.lower():
|
|
106
|
+
weights = TYPE_WEIGHTS["vulnerable"]
|
|
107
|
+
else:
|
|
108
|
+
weights = TYPE_WEIGHTS["default"]
|
|
109
|
+
|
|
110
|
+
# Weighted random selection
|
|
111
|
+
types = list(weights.keys())
|
|
112
|
+
w = [weights[t] for t in types]
|
|
113
|
+
chosen_type = random.choices(types, weights=w, k=1)[0]
|
|
114
|
+
|
|
115
|
+
return random.choice(ALMOST_SAID_TYPES[chosen_type])
|
|
116
|
+
|
|
117
|
+
def maybe_generate(self, emotion: Dict[str, float],
|
|
118
|
+
hour_of_day: int = 12,
|
|
119
|
+
context_hint: str = "") -> Optional[str]:
|
|
120
|
+
"""All-in-one: tick, check, generate if appropriate."""
|
|
121
|
+
self.tick_message()
|
|
122
|
+
if self.should_almost_say(emotion, hour_of_day):
|
|
123
|
+
return self.generate_almost_said(emotion, context_hint)
|
|
124
|
+
return None
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
# =============================================================================
|
|
128
|
+
# SINGLETON ACCESS
|
|
129
|
+
# =============================================================================
|
|
130
|
+
|
|
131
|
+
_instance: Optional[AlmostSaidEngine] = None
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def get_almost_said_engine() -> AlmostSaidEngine:
|
|
135
|
+
global _instance
|
|
136
|
+
if _instance is None:
|
|
137
|
+
_instance = AlmostSaidEngine()
|
|
138
|
+
return _instance
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def maybe_generate(emotion: Dict[str, float], hour_of_day: int = 12,
|
|
142
|
+
context_hint: str = "") -> Optional[str]:
|
|
143
|
+
"""Convenience: tick + check + generate if appropriate."""
|
|
144
|
+
return get_almost_said_engine().maybe_generate(emotion, hour_of_day, context_hint)
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def get_almost_said_prompt_section(emotion: Dict[str, float],
|
|
148
|
+
hour_of_day: int = 12) -> str:
|
|
149
|
+
"""Get prompt section for LLM. Returns '' if not triggered."""
|
|
150
|
+
engine = get_almost_said_engine()
|
|
151
|
+
engine.tick_message()
|
|
152
|
+
if not engine.should_almost_say(emotion, hour_of_day):
|
|
153
|
+
return ""
|
|
154
|
+
return "\n[Subvocalization]\nYou have something on the tip of your tongue you're not sure you should say... let it almost slip out.\n"
|