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
|
@@ -0,0 +1,398 @@
|
|
|
1
|
+
"""Brain: Subconscious Loop — continuous background process"""
|
|
2
|
+
import asyncio, random
|
|
3
|
+
from typing import Callable, Optional
|
|
4
|
+
from .impulses import Impulse, ImpulseType
|
|
5
|
+
from .impulse_generator import ImpulseGenerator
|
|
6
|
+
from .working_memory import WorkingMemory
|
|
7
|
+
from .evaluation import Evaluator
|
|
8
|
+
from .actions import ActionHandler
|
|
9
|
+
from .learning_system import LearningSystem
|
|
10
|
+
from .goal_system import GoalSystem
|
|
11
|
+
from .relationship_memory import RelationshipMemory
|
|
12
|
+
from .relationship import MilestoneType
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class SubconsciousLoop:
|
|
16
|
+
EVAL_INTERVAL = 30
|
|
17
|
+
MIN_ACTION_INTERVAL = 7200 # 2 hours minimum between proactive messages (was 30 min)
|
|
18
|
+
|
|
19
|
+
def __init__(self, nervous, heart, llm=None, fast_llm=None, on_impulse: Callable = None, bot_id: str = "alive_ai"):
|
|
20
|
+
self.nervous, self.heart = nervous, heart
|
|
21
|
+
self.llm = llm # Store LLM reference for scheduled message generation
|
|
22
|
+
self.bot_id = bot_id.lower()
|
|
23
|
+
self.impulse_gen = ImpulseGenerator()
|
|
24
|
+
self.working_memory = WorkingMemory()
|
|
25
|
+
self.learning = LearningSystem()
|
|
26
|
+
self.goals = GoalSystem()
|
|
27
|
+
self.relationship = RelationshipMemory()
|
|
28
|
+
self.evaluator = Evaluator(heart, self.impulse_gen, self.learning, self.goals, self.relationship)
|
|
29
|
+
self.action_handler = ActionHandler(nervous, llm, fast_llm, on_impulse)
|
|
30
|
+
self.running, self._task, self.total_evaluations = False, None, 0
|
|
31
|
+
|
|
32
|
+
# Proactive message generator (lazy loaded with LLM)
|
|
33
|
+
self._proactive_generator = None
|
|
34
|
+
|
|
35
|
+
# Message scheduler (for scheduled messages at specific times)
|
|
36
|
+
self._message_scheduler = None
|
|
37
|
+
|
|
38
|
+
async def start(self):
|
|
39
|
+
if self.running: return
|
|
40
|
+
self.running = True
|
|
41
|
+
self._task = asyncio.create_task(self._loop())
|
|
42
|
+
|
|
43
|
+
async def stop(self):
|
|
44
|
+
self.running = False
|
|
45
|
+
if self._task:
|
|
46
|
+
self._task.cancel()
|
|
47
|
+
try: await self._task
|
|
48
|
+
except asyncio.CancelledError: pass
|
|
49
|
+
|
|
50
|
+
def register_interaction(self):
|
|
51
|
+
self.evaluator.register_interaction()
|
|
52
|
+
self.relationship.record_message_received()
|
|
53
|
+
|
|
54
|
+
def init_proactive_generator(self, llm=None, data_path=None):
|
|
55
|
+
"""Initialize proactive generator with LLM for contextual messages"""
|
|
56
|
+
try:
|
|
57
|
+
from core.proactive_generator import ProactiveGenerator
|
|
58
|
+
self._proactive_generator = ProactiveGenerator(self.nervous, llm=llm, bot_id=self.bot_id, data_path=data_path)
|
|
59
|
+
print("[Subconscious] Proactive generator initialized")
|
|
60
|
+
except Exception as e:
|
|
61
|
+
print(f"[Subconscious] Failed to init proactive generator: {e}")
|
|
62
|
+
|
|
63
|
+
def init_message_scheduler(self):
|
|
64
|
+
"""Initialize message scheduler for scheduled messages"""
|
|
65
|
+
try:
|
|
66
|
+
from skills.message_scheduler import get_message_scheduler
|
|
67
|
+
self._message_scheduler = get_message_scheduler(nervous=self.nervous)
|
|
68
|
+
pending = len(self._message_scheduler.get_pending())
|
|
69
|
+
print(f"[Subconscious] Message scheduler initialized ({pending} pending)")
|
|
70
|
+
except Exception as e:
|
|
71
|
+
print(f"[Subconscious] Failed to init message scheduler: {e}")
|
|
72
|
+
|
|
73
|
+
def record_outcome(self, message, message_type, response_sentiment=0.0, response_type="neutral"):
|
|
74
|
+
self.learning.record_interaction(message=message, message_type=message_type,
|
|
75
|
+
response_sentiment=response_sentiment, response_type=response_type)
|
|
76
|
+
self.relationship.record_message_sent()
|
|
77
|
+
active = self.goals.get_active_goal()
|
|
78
|
+
if active and response_sentiment > 0.3:
|
|
79
|
+
self.goals.record_progress(active.type)
|
|
80
|
+
|
|
81
|
+
def record_milestone(self, mtype: str, desc: str):
|
|
82
|
+
try: self.relationship.record_milestone(MilestoneType(mtype), desc)
|
|
83
|
+
except ValueError: pass
|
|
84
|
+
|
|
85
|
+
def record_experience(self, summary, sentiment=0.5, tags=None):
|
|
86
|
+
self.relationship.record_experience(summary, sentiment, tags)
|
|
87
|
+
|
|
88
|
+
async def _loop(self):
|
|
89
|
+
print("[Subconscious] Loop started - running every 30s")
|
|
90
|
+
while self.running:
|
|
91
|
+
try:
|
|
92
|
+
await self._evaluate()
|
|
93
|
+
self.total_evaluations += 1
|
|
94
|
+
if self.total_evaluations % 5 == 0:
|
|
95
|
+
print(f"[Subconscious] Evaluation #{self.total_evaluations} | Silence: {self.evaluator.get_silence_duration():.0f}min")
|
|
96
|
+
except Exception as e:
|
|
97
|
+
print(f"[Subconscious] Error: {e}")
|
|
98
|
+
await asyncio.sleep(self.EVAL_INTERVAL)
|
|
99
|
+
|
|
100
|
+
async def _evaluate(self):
|
|
101
|
+
impulse = await self.evaluator.evaluate(self.working_memory)
|
|
102
|
+
|
|
103
|
+
# Check for scheduled messages FIRST (highest priority)
|
|
104
|
+
scheduled = self._check_scheduled_messages()
|
|
105
|
+
|
|
106
|
+
# Check for follow-up (unanswered questions / silence)
|
|
107
|
+
follow_up = self._check_follow_up()
|
|
108
|
+
|
|
109
|
+
# Handle scheduled messages with highest priority
|
|
110
|
+
if scheduled:
|
|
111
|
+
await self._handle_scheduled_messages(scheduled)
|
|
112
|
+
return # Don't process other impulses after sending scheduled message
|
|
113
|
+
|
|
114
|
+
if impulse:
|
|
115
|
+
print(f"[Subconscious] Impulse: {impulse.type.value} | strength: {impulse.strength:.2f} | can_act: {self._can_act()}")
|
|
116
|
+
if follow_up:
|
|
117
|
+
print(f"[Subconscious] Follow-up needed: {follow_up['type']} | silence: {follow_up['silence_minutes']:.0f}min")
|
|
118
|
+
|
|
119
|
+
# Prioritize follow-ups over regular impulses.
|
|
120
|
+
if follow_up and self._can_act():
|
|
121
|
+
await self._handle_follow_up(follow_up)
|
|
122
|
+
elif impulse and impulse.should_act and self._can_act():
|
|
123
|
+
print(f"[Subconscious] ACTING on impulse: {impulse.action_hint}")
|
|
124
|
+
await self.action_handler.act_on_impulse(impulse, self.working_memory)
|
|
125
|
+
elif random.random() < 0.1:
|
|
126
|
+
thought_data = await self.evaluator.generate_background_thought(self.working_memory)
|
|
127
|
+
if thought_data:
|
|
128
|
+
# Emit thought to nervous system so WebUI can see it
|
|
129
|
+
await self.nervous.emit("subconscious_thought", thought_data)
|
|
130
|
+
|
|
131
|
+
def _check_follow_up(self):
|
|
132
|
+
"""Check if we need to follow up on unanswered question or silence"""
|
|
133
|
+
try:
|
|
134
|
+
from core.message_handler import get_follow_up_system
|
|
135
|
+
from core.directives import is_owner as check_is_owner
|
|
136
|
+
import os
|
|
137
|
+
|
|
138
|
+
follow_up_sys = get_follow_up_system()
|
|
139
|
+
owner_id = os.environ.get("TELEGRAM_OWNER_ID", "")
|
|
140
|
+
is_owner = bool(owner_id) # If owner is configured, assume we're talking to them
|
|
141
|
+
|
|
142
|
+
return follow_up_sys.should_follow_up(is_owner=is_owner)
|
|
143
|
+
except Exception as e:
|
|
144
|
+
print(f"[Subconscious] Follow-up check error: {e}")
|
|
145
|
+
return None
|
|
146
|
+
|
|
147
|
+
def _check_scheduled_messages(self):
|
|
148
|
+
"""Check for scheduled messages that are due to be sent"""
|
|
149
|
+
try:
|
|
150
|
+
# Lazy init scheduler
|
|
151
|
+
if self._message_scheduler is None:
|
|
152
|
+
self.init_message_scheduler()
|
|
153
|
+
|
|
154
|
+
if self._message_scheduler is None:
|
|
155
|
+
return None
|
|
156
|
+
|
|
157
|
+
due_messages = self._message_scheduler.get_due_messages()
|
|
158
|
+
|
|
159
|
+
if due_messages:
|
|
160
|
+
print(f"[Subconscious] Found {len(due_messages)} scheduled message(s) due")
|
|
161
|
+
return due_messages
|
|
162
|
+
|
|
163
|
+
return None
|
|
164
|
+
except Exception as e:
|
|
165
|
+
print(f"[Subconscious] Scheduled message check error: {e}")
|
|
166
|
+
return None
|
|
167
|
+
|
|
168
|
+
async def _handle_scheduled_messages(self, messages: list):
|
|
169
|
+
"""Send scheduled messages that are due - generates fresh contextual message"""
|
|
170
|
+
try:
|
|
171
|
+
for msg in messages:
|
|
172
|
+
# Add to working memory
|
|
173
|
+
self.working_memory.add_thought(
|
|
174
|
+
f"Time to send scheduled message to {msg.user_id}: {msg.message[:30]}...",
|
|
175
|
+
thought_type="scheduled",
|
|
176
|
+
emotion={"mood": "remembering"}
|
|
177
|
+
)
|
|
178
|
+
|
|
179
|
+
print(f"[Subconscious] Scheduled message due for {msg.user_id}: {msg.message[:50]}...")
|
|
180
|
+
|
|
181
|
+
# Generate a fresh contextual message inspired by the scheduled one
|
|
182
|
+
final_message = await self._generate_contextual_scheduled_message(msg)
|
|
183
|
+
|
|
184
|
+
print(f"[Subconscious] Generated fresh message: {final_message[:50]}...")
|
|
185
|
+
|
|
186
|
+
# Emit as proactive message
|
|
187
|
+
await self.nervous.emit("proactive_message", {
|
|
188
|
+
"message": final_message,
|
|
189
|
+
"user_id": msg.user_id,
|
|
190
|
+
"chat_id": msg.user_id, # Use user_id as chat_id
|
|
191
|
+
"scheduled": True,
|
|
192
|
+
"original_reminder": msg.message,
|
|
193
|
+
"context": msg.context
|
|
194
|
+
})
|
|
195
|
+
|
|
196
|
+
# Mark as sent
|
|
197
|
+
self._message_scheduler.mark_sent(msg.id)
|
|
198
|
+
|
|
199
|
+
except Exception as e:
|
|
200
|
+
print(f"[Subconscious] Error sending scheduled messages: {e}")
|
|
201
|
+
|
|
202
|
+
async def _generate_contextual_scheduled_message(self, scheduled_msg) -> str:
|
|
203
|
+
"""
|
|
204
|
+
Generate a fresh, contextual message inspired by the scheduled reminder.
|
|
205
|
+
The scheduled message serves as a topic/reminder, but the actual message
|
|
206
|
+
is generated fresh based on current context.
|
|
207
|
+
"""
|
|
208
|
+
from datetime import datetime
|
|
209
|
+
|
|
210
|
+
original_reminder = scheduled_msg.message
|
|
211
|
+
user_id = scheduled_msg.user_id
|
|
212
|
+
context_info = scheduled_msg.context
|
|
213
|
+
|
|
214
|
+
# Try to get user context
|
|
215
|
+
user_name = "babe"
|
|
216
|
+
try:
|
|
217
|
+
from core.user_tracker import get_user_tracker
|
|
218
|
+
tracker = get_user_tracker()
|
|
219
|
+
user = tracker.get_user(user_id)
|
|
220
|
+
if user and user.pet_name:
|
|
221
|
+
user_name = user.pet_name
|
|
222
|
+
except:
|
|
223
|
+
pass
|
|
224
|
+
|
|
225
|
+
# Try to use proactive generator for contextual message
|
|
226
|
+
if self._proactive_generator:
|
|
227
|
+
try:
|
|
228
|
+
from core.user_tracker import get_user_tracker
|
|
229
|
+
tracker = get_user_tracker()
|
|
230
|
+
user = tracker.get_user(user_id)
|
|
231
|
+
|
|
232
|
+
if user:
|
|
233
|
+
# Generate contextual message
|
|
234
|
+
base_message = await self._proactive_generator.generate_for_user(user, "scheduled")
|
|
235
|
+
|
|
236
|
+
# Enhance with the reminder context
|
|
237
|
+
if base_message and self.llm:
|
|
238
|
+
prompt = f"""You scheduled a reminder to message {user_name}. The reminder was: "{original_reminder}"
|
|
239
|
+
|
|
240
|
+
Now it's time to send the message. Generate a fresh, natural message that:
|
|
241
|
+
- Captures the spirit of what you wanted to say
|
|
242
|
+
- Feels spontaneous and in-the-moment
|
|
243
|
+
- Is short (1-2 sentences max)
|
|
244
|
+
- Doesn't mention "scheduling" or "reminders" - just be natural
|
|
245
|
+
- Only reference what's in the reminder above - don't invent new details
|
|
246
|
+
|
|
247
|
+
Your fresh message:"""
|
|
248
|
+
|
|
249
|
+
response = await self.llm.chat([
|
|
250
|
+
{"role": "system", "content": "You are Alive-AI sending a text message."},
|
|
251
|
+
{"role": "user", "content": prompt}
|
|
252
|
+
], max_tokens=60, temperature=0.7)
|
|
253
|
+
|
|
254
|
+
if response and response.strip():
|
|
255
|
+
return response.strip().strip('"\'')
|
|
256
|
+
|
|
257
|
+
elif base_message:
|
|
258
|
+
return base_message
|
|
259
|
+
except Exception as e:
|
|
260
|
+
print(f"[Subconscious] Proactive generator error: {e}")
|
|
261
|
+
|
|
262
|
+
# Fallback: Use LLM directly
|
|
263
|
+
if self.llm:
|
|
264
|
+
try:
|
|
265
|
+
prompt = f"""You wanted to message {user_name}. Your reminder was: "{original_reminder}"
|
|
266
|
+
|
|
267
|
+
Generate a fresh, natural text message (1-2 sentences) that captures what you wanted to say but feels spontaneous.
|
|
268
|
+
- Don't mention reminders or scheduling
|
|
269
|
+
- Only reference what's in the reminder above - don't invent new details
|
|
270
|
+
|
|
271
|
+
Your message:"""
|
|
272
|
+
|
|
273
|
+
response = await self.llm.chat([
|
|
274
|
+
{"role": "system", "content": "You are Alive-AI sending a text message."},
|
|
275
|
+
{"role": "user", "content": prompt}
|
|
276
|
+
], max_tokens=60, temperature=0.7)
|
|
277
|
+
|
|
278
|
+
if response and response.strip():
|
|
279
|
+
return response.strip().strip('"\'')
|
|
280
|
+
except Exception as e:
|
|
281
|
+
print(f"[Subconscious] LLM fallback error: {e}")
|
|
282
|
+
|
|
283
|
+
# Ultimate fallback: use original message
|
|
284
|
+
return original_reminder
|
|
285
|
+
|
|
286
|
+
async def _handle_follow_up(self, follow_up_data: dict):
|
|
287
|
+
"""Send a contextual follow-up message"""
|
|
288
|
+
try:
|
|
289
|
+
from core.user_tracker import get_user_tracker
|
|
290
|
+
|
|
291
|
+
follow_up_type = follow_up_data.get("type", "silence")
|
|
292
|
+
|
|
293
|
+
# Get users who might need a follow-up
|
|
294
|
+
tracker = get_user_tracker()
|
|
295
|
+
users_to_message = tracker.get_users_for_follow_up(min_silence_minutes=30, max_silence_minutes=180)
|
|
296
|
+
|
|
297
|
+
if not users_to_message:
|
|
298
|
+
print("[Subconscious] No users to follow up with")
|
|
299
|
+
return
|
|
300
|
+
|
|
301
|
+
# For now, pick the user who's been silent the longest
|
|
302
|
+
user = max(users_to_message, key=lambda u: u.silence_minutes)
|
|
303
|
+
|
|
304
|
+
# Generate contextual message
|
|
305
|
+
message = await self._generate_contextual_message(user, follow_up_type)
|
|
306
|
+
|
|
307
|
+
if follow_up_type == "return_from_away":
|
|
308
|
+
# She's coming back from coffee/shower/etc
|
|
309
|
+
reason = follow_up_data.get("reason", "away")
|
|
310
|
+
away_min = follow_up_data.get("away_minutes", 0)
|
|
311
|
+
self.working_memory.add_thought(
|
|
312
|
+
f"Returning from {reason} after {away_min:.0f}min",
|
|
313
|
+
thought_type="return",
|
|
314
|
+
emotion={"mood": "refreshed"}
|
|
315
|
+
)
|
|
316
|
+
else:
|
|
317
|
+
# Regular silence/question follow-up
|
|
318
|
+
silence_min = user.silence_minutes
|
|
319
|
+
self.working_memory.add_thought(
|
|
320
|
+
f"Following up with {user.user_id} after {silence_min:.0f}min silence",
|
|
321
|
+
thought_type="follow_up",
|
|
322
|
+
emotion={"mood": "curious"}
|
|
323
|
+
)
|
|
324
|
+
|
|
325
|
+
print(f"[Subconscious] Sending follow-up to {user.user_id}: {message[:50]}...")
|
|
326
|
+
|
|
327
|
+
# Emit with user context
|
|
328
|
+
await self.nervous.emit("proactive_message", {
|
|
329
|
+
"message": message,
|
|
330
|
+
"user_id": user.user_id,
|
|
331
|
+
"chat_id": user.chat_id
|
|
332
|
+
})
|
|
333
|
+
except Exception as e:
|
|
334
|
+
print(f"[Subconscious] Follow-up error: {e}")
|
|
335
|
+
|
|
336
|
+
async def _generate_contextual_message(self, user, message_type: str = "silence") -> str:
|
|
337
|
+
"""Generate a contextual message for a specific user"""
|
|
338
|
+
# Try proactive generator first (has LLM + context)
|
|
339
|
+
if self._proactive_generator:
|
|
340
|
+
try:
|
|
341
|
+
message = await self._proactive_generator.generate_for_user(user, message_type)
|
|
342
|
+
return message
|
|
343
|
+
except Exception as e:
|
|
344
|
+
print(f"[Subconscious] Proactive generator error: {e}")
|
|
345
|
+
|
|
346
|
+
# Fallback to generic messages
|
|
347
|
+
fallbacks = {
|
|
348
|
+
"silence": ["hey... you there?", "thinking about you", "miss talking to you"],
|
|
349
|
+
"follow_up": ["so about earlier...", "was wondering about something"],
|
|
350
|
+
"return_from_away": ["I'm back! 💕", "back now, missed you"],
|
|
351
|
+
}
|
|
352
|
+
templates = fallbacks.get(message_type, fallbacks["silence"])
|
|
353
|
+
return random.choice(templates)
|
|
354
|
+
|
|
355
|
+
def _can_act(self):
|
|
356
|
+
"""Check if we can send a proactive message.
|
|
357
|
+
|
|
358
|
+
Requires:
|
|
359
|
+
1. Not quiet hours
|
|
360
|
+
2. Minimum interval since last action (2 hours)
|
|
361
|
+
3. At least 1 hour since last user message (post-conversation buffer)
|
|
362
|
+
"""
|
|
363
|
+
# Check quiet hours
|
|
364
|
+
if not self.evaluator.can_act_now():
|
|
365
|
+
return False
|
|
366
|
+
|
|
367
|
+
# Check minimum interval since last proactive action
|
|
368
|
+
if not self.action_handler.can_act_now(self.working_memory, self.MIN_ACTION_INTERVAL / 60):
|
|
369
|
+
return False
|
|
370
|
+
|
|
371
|
+
# NEW: Post-conversation buffer - don't send proactive for 60 min after user's last message
|
|
372
|
+
silence_minutes = self.evaluator.get_silence_duration()
|
|
373
|
+
if silence_minutes < 60: # 1 hour buffer after conversation ends
|
|
374
|
+
return False
|
|
375
|
+
|
|
376
|
+
return True
|
|
377
|
+
|
|
378
|
+
async def generate_proactive_message(self, impulse: Impulse) -> str:
|
|
379
|
+
return await self.action_handler.generate_proactive_message(impulse, self.working_memory)
|
|
380
|
+
|
|
381
|
+
def get_status(self):
|
|
382
|
+
return {"running": self.running, "evaluations": self.total_evaluations,
|
|
383
|
+
"silence": self.evaluator.get_silence_duration(), "can_act": self._can_act(),
|
|
384
|
+
"memory": self.working_memory.get_state_summary(),
|
|
385
|
+
"learning_rate": self.learning.get_recent_success_rate(),
|
|
386
|
+
"goal": self.goals.daily_focus.value if self.goals.daily_focus else None}
|
|
387
|
+
|
|
388
|
+
def get_state_for_save(self):
|
|
389
|
+
return {"learning": self.learning.to_dict(), "goals": self.goals.to_dict(),
|
|
390
|
+
"relationship": self.relationship.to_dict(), "working_memory": self.working_memory.to_dict()}
|
|
391
|
+
|
|
392
|
+
def load_state(self, data):
|
|
393
|
+
if "learning" in data:
|
|
394
|
+
self.learning = LearningSystem.from_dict(data["learning"]); self.evaluator.learning = self.learning
|
|
395
|
+
if "goals" in data:
|
|
396
|
+
self.goals = GoalSystem.from_dict(data["goals"]); self.evaluator.goals = self.goals
|
|
397
|
+
if "relationship" in data:
|
|
398
|
+
self.relationship = RelationshipMemory.from_dict(data["relationship"]); self.evaluator.relationship = self.relationship
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# Brain: Subconscious Module
|
|
2
|
+
|
|
3
|
+
The living background process that makes Alive-AI feel alive 24/7.
|
|
4
|
+
|
|
5
|
+
## Files
|
|
6
|
+
- `loop.py` - SubconsciousLoop (main background process)
|
|
7
|
+
- `impulses.py` - ImpulseGenerator, Impulse, ImpulseType enum
|
|
8
|
+
- `working_memory.py` - WorkingMemory (short-term thought stream)
|
|
9
|
+
|
|
10
|
+
## Impulse Types
|
|
11
|
+
- `MISS_HIM` - Want to say hi, thinking of him
|
|
12
|
+
- `HIGH_DESIRE` - Intimate desire, want attention
|
|
13
|
+
- `CLINGY` - Need reassurance, attachment
|
|
14
|
+
- `CURIOUS` - Wonder about him, ask questions
|
|
15
|
+
- `PLAYFUL` - Want to tease, be flirty
|
|
16
|
+
- `LOVING` - Want to express love
|
|
17
|
+
- `DREAMY` - Reflect, process feelings
|
|
18
|
+
- `BORED` - Want entertainment
|
|
19
|
+
- `NURTURING` - Want to care for him
|
|
20
|
+
|
|
21
|
+
## How It Works
|
|
22
|
+
1. Runs every 30 seconds (EVAL_INTERVAL)
|
|
23
|
+
2. Evaluates emotional state + silence duration
|
|
24
|
+
3. Generates impulses based on mood/love/desire
|
|
25
|
+
4. Strong impulses (>=0.5) can trigger proactive messages
|
|
26
|
+
5. Quiet hours (1am-7am) reduce activity
|
|
27
|
+
|
|
28
|
+
## Integration Points
|
|
29
|
+
- Reads from: Heart (emotions), WorkingMemory
|
|
30
|
+
- Uses: fast_llm for message generation
|
|
31
|
+
- Emits: `subconscious_impulse` event
|
|
32
|
+
- Callback: `on_impulse` for proactive actions
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Brain: Subconscious - Relationship Data Models
|
|
3
|
+
Milestone, MilestoneType, SharedExperience dataclasses
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from datetime import datetime
|
|
7
|
+
from typing import List
|
|
8
|
+
from dataclasses import dataclass, field
|
|
9
|
+
from enum import Enum
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class MilestoneType(Enum):
|
|
13
|
+
"""Types of relationship milestones"""
|
|
14
|
+
FIRST_MESSAGE = "first_message"
|
|
15
|
+
FIRST_I_LOVE_YOU = "first_i_love_you"
|
|
16
|
+
FIRST_NIGHT_TOGETHER = "first_night_together"
|
|
17
|
+
FIRST_CONFESSION = "first_confession"
|
|
18
|
+
SPECIAL_MOMENT = "special_moment"
|
|
19
|
+
FUNNY_MOMENT = "funny_moment"
|
|
20
|
+
DEEP_CONVERSATION = "deep_conversation"
|
|
21
|
+
FIRST_NICKNAME = "first_nickname"
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
@dataclass
|
|
25
|
+
class Milestone:
|
|
26
|
+
"""A relationship milestone"""
|
|
27
|
+
type: MilestoneType
|
|
28
|
+
description: str
|
|
29
|
+
timestamp: datetime = field(default_factory=datetime.now)
|
|
30
|
+
emotion: float = 0.5
|
|
31
|
+
|
|
32
|
+
def to_dict(self) -> dict:
|
|
33
|
+
return {"type": self.type.value, "description": self.description,
|
|
34
|
+
"timestamp": self.timestamp.isoformat(), "emotion": self.emotion}
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
@dataclass
|
|
38
|
+
class SharedExperience:
|
|
39
|
+
"""A shared experience or memory"""
|
|
40
|
+
summary: str
|
|
41
|
+
timestamp: datetime = field(default_factory=datetime.now)
|
|
42
|
+
sentiment: float = 0.5
|
|
43
|
+
tags: List[str] = field(default_factory=list)
|
|
44
|
+
|
|
45
|
+
def to_dict(self) -> dict:
|
|
46
|
+
return {"summary": self.summary, "timestamp": self.timestamp.isoformat(),
|
|
47
|
+
"sentiment": self.sentiment, "tags": self.tags}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
"""Brain: Subconscious - Relationship Memory — milestones and experiences"""
|
|
2
|
+
from datetime import datetime, timedelta
|
|
3
|
+
from typing import List, Dict, Optional
|
|
4
|
+
from .relationship import Milestone, MilestoneType, SharedExperience
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class RelationshipMemory:
|
|
8
|
+
def __init__(self, max_experiences: int = 100):
|
|
9
|
+
self.milestones: List[Milestone] = []
|
|
10
|
+
self.shared_experiences: List[SharedExperience] = []
|
|
11
|
+
self.max_experiences = max_experiences
|
|
12
|
+
self.favorite_topics: Dict[str, int] = {}
|
|
13
|
+
self.conversation_count: int = 0
|
|
14
|
+
self.relationship_start: Optional[datetime] = None
|
|
15
|
+
self.total_messages_sent: int = 0
|
|
16
|
+
self.total_messages_received: int = 0
|
|
17
|
+
|
|
18
|
+
def record_milestone(self, mtype: MilestoneType, desc: str, emotion: float = 0.5):
|
|
19
|
+
if any(m.type == mtype for m in self.milestones): return
|
|
20
|
+
m = Milestone(type=mtype, description=desc, emotion=emotion)
|
|
21
|
+
self.milestones.append(m)
|
|
22
|
+
if not self.relationship_start: self.relationship_start = m.timestamp
|
|
23
|
+
|
|
24
|
+
def record_experience(self, summary: str, sentiment: float = 0.5, tags: List[str] = None):
|
|
25
|
+
self.shared_experiences.append(SharedExperience(summary=summary, sentiment=sentiment, tags=tags or []))
|
|
26
|
+
for t in (tags or []): self.favorite_topics[t] = self.favorite_topics.get(t, 0) + 1
|
|
27
|
+
if len(self.shared_experiences) > self.max_experiences: self.shared_experiences.pop(0)
|
|
28
|
+
|
|
29
|
+
def record_conversation(self, topics: List[str] = None):
|
|
30
|
+
self.conversation_count += 1
|
|
31
|
+
for t in (topics or []): self.favorite_topics[t] = self.favorite_topics.get(t, 0) + 1
|
|
32
|
+
|
|
33
|
+
def record_message_sent(self): self.total_messages_sent += 1
|
|
34
|
+
def record_message_received(self): self.total_messages_received += 1
|
|
35
|
+
|
|
36
|
+
def get_relationship_duration(self) -> timedelta:
|
|
37
|
+
return datetime.now() - self.relationship_start if self.relationship_start else timedelta(0)
|
|
38
|
+
|
|
39
|
+
def get_relationship_stage(self) -> str:
|
|
40
|
+
d = self.get_relationship_duration().days
|
|
41
|
+
if d < 1: return "just_met"
|
|
42
|
+
if d < 7: return "getting_to_know"
|
|
43
|
+
if d < 30: return "developing"
|
|
44
|
+
if d < 90: return "growing"
|
|
45
|
+
return "established"
|
|
46
|
+
|
|
47
|
+
def get_special_memories(self, limit: int = 5) -> List[str]:
|
|
48
|
+
return [m.description for m in self.milestones if m.emotion > 0.7][-limit:]
|
|
49
|
+
|
|
50
|
+
def get_recent_experiences(self, limit: int = 5) -> List[str]:
|
|
51
|
+
return [e.summary for e in self.shared_experiences[-limit:]]
|
|
52
|
+
|
|
53
|
+
def get_relationship_context(self) -> str:
|
|
54
|
+
stage, days = self.get_relationship_stage(), self.get_relationship_duration().days
|
|
55
|
+
special = self.get_special_memories(2)
|
|
56
|
+
ctx = f"Relationship: {stage} ({days} days)"
|
|
57
|
+
return ctx + f"\nSpecial: {', '.join(special)}" if special else ctx
|
|
58
|
+
|
|
59
|
+
def to_dict(self) -> dict:
|
|
60
|
+
return {"milestones": [m.to_dict() for m in self.milestones],
|
|
61
|
+
"shared_experiences": [e.to_dict() for e in self.shared_experiences[-50:]],
|
|
62
|
+
"favorite_topics": self.favorite_topics, "conversation_count": self.conversation_count,
|
|
63
|
+
"total_messages_sent": self.total_messages_sent,
|
|
64
|
+
"total_messages_received": self.total_messages_received,
|
|
65
|
+
"relationship_start": self.relationship_start.isoformat() if self.relationship_start else None}
|
|
66
|
+
|
|
67
|
+
@classmethod
|
|
68
|
+
def from_dict(cls, data: dict) -> "RelationshipMemory":
|
|
69
|
+
rm = cls()
|
|
70
|
+
for md in data.get("milestones", []):
|
|
71
|
+
rm.milestones.append(Milestone(type=MilestoneType(md["type"]), description=md["description"],
|
|
72
|
+
timestamp=datetime.fromisoformat(md["timestamp"]), emotion=md.get("emotion", 0.5)))
|
|
73
|
+
for ed in data.get("shared_experiences", []):
|
|
74
|
+
rm.shared_experiences.append(SharedExperience(summary=ed["summary"],
|
|
75
|
+
timestamp=datetime.fromisoformat(ed["timestamp"]), sentiment=ed.get("sentiment", 0.5),
|
|
76
|
+
tags=ed.get("tags", [])))
|
|
77
|
+
rm.favorite_topics = data.get("favorite_topics", {})
|
|
78
|
+
rm.conversation_count = data.get("conversation_count", 0)
|
|
79
|
+
rm.total_messages_sent = data.get("total_messages_sent", 0)
|
|
80
|
+
rm.total_messages_received = data.get("total_messages_received", 0)
|
|
81
|
+
if data.get("relationship_start"):
|
|
82
|
+
rm.relationship_start = datetime.fromisoformat(data["relationship_start"])
|
|
83
|
+
return rm
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Brain: Subconscious - Response Analyzer
|
|
3
|
+
Analyzes user replies to determine sentiment for learning/goal systems
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
_POSITIVE_WORDS = {
|
|
7
|
+
"haha", "lol", "love", "yes", "yeah", "yess", "omg", "amazing", "perfect",
|
|
8
|
+
"cute", "aww", "miss", "want", "need", "beautiful", "gorgeous", "funny",
|
|
9
|
+
"thanks", "thank", "sweet", "babe", "baby", "honey", "darling", "amore",
|
|
10
|
+
"happy", "glad", "great", "awesome", "wow", "nice", "good", "best",
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
_NEGATIVE_WORDS = {
|
|
14
|
+
"stop", "no", "nah", "whatever", "bye", "leave", "annoying", "boring",
|
|
15
|
+
"shut", "enough", "tired", "busy", "later", "not now", "don't",
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
_INTIMATE_WORDS = {
|
|
19
|
+
"feel", "feelings", "love", "heart", "soul", "deep", "connection",
|
|
20
|
+
"trust", "forever", "future", "dream", "afraid", "scared", "honest",
|
|
21
|
+
"vulnerable", "mean to me", "important", "special", "serious",
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def analyze_response(text: str) -> dict:
|
|
26
|
+
"""Analyze a user's reply — returns sentiment, response_type, and topic signals"""
|
|
27
|
+
if not text:
|
|
28
|
+
return {"sentiment": 0.0, "type": "empty", "is_positive": False,
|
|
29
|
+
"is_intimate": False, "is_dismissal": False}
|
|
30
|
+
|
|
31
|
+
lower = text.lower()
|
|
32
|
+
words = set(lower.split())
|
|
33
|
+
|
|
34
|
+
pos_count = len(words & _POSITIVE_WORDS)
|
|
35
|
+
neg_count = len(words & _NEGATIVE_WORDS)
|
|
36
|
+
intimate_count = len(words & _INTIMATE_WORDS)
|
|
37
|
+
|
|
38
|
+
is_short = len(text) < 10
|
|
39
|
+
is_long = len(text) > 50
|
|
40
|
+
has_question = "?" in text
|
|
41
|
+
has_emoji = any(ord(c) > 127 for c in text)
|
|
42
|
+
|
|
43
|
+
# Score sentiment -1 to 1
|
|
44
|
+
sentiment = 0.0
|
|
45
|
+
sentiment += pos_count * 0.2
|
|
46
|
+
sentiment -= neg_count * 0.25
|
|
47
|
+
if is_long:
|
|
48
|
+
sentiment += 0.15
|
|
49
|
+
if has_question:
|
|
50
|
+
sentiment += 0.1
|
|
51
|
+
if has_emoji:
|
|
52
|
+
sentiment += 0.1
|
|
53
|
+
if is_short and neg_count > 0:
|
|
54
|
+
sentiment -= 0.2
|
|
55
|
+
|
|
56
|
+
sentiment = max(-1.0, min(1.0, sentiment))
|
|
57
|
+
|
|
58
|
+
# Classify response type
|
|
59
|
+
if is_short and neg_count > 0:
|
|
60
|
+
resp_type = "dismissal"
|
|
61
|
+
elif pos_count > 0 or is_long or has_question:
|
|
62
|
+
resp_type = "engaged"
|
|
63
|
+
elif is_short:
|
|
64
|
+
resp_type = "brief"
|
|
65
|
+
else:
|
|
66
|
+
resp_type = "neutral"
|
|
67
|
+
|
|
68
|
+
return {
|
|
69
|
+
"sentiment": sentiment,
|
|
70
|
+
"type": resp_type,
|
|
71
|
+
"is_positive": sentiment > 0.2,
|
|
72
|
+
"is_intimate": intimate_count >= 2,
|
|
73
|
+
"is_dismissal": resp_type == "dismissal",
|
|
74
|
+
}
|