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,1438 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Brain: Default Mode Network
|
|
3
|
+
Background processing that runs when Alive-AI is "idle" - like the brain's default mode network.
|
|
4
|
+
Generates spontaneous thoughts, consolidates memories, and prepares conversation starters.
|
|
5
|
+
|
|
6
|
+
This module is MODULAR - can be connected/disconnected without breaking anything.
|
|
7
|
+
|
|
8
|
+
Integration with ProactiveGenerator:
|
|
9
|
+
- Default mode handles TIMING (when to send proactive messages)
|
|
10
|
+
- ProactiveGenerator handles CONTENT (what to say)
|
|
11
|
+
- Both modules work independently - if one fails, the other continues
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
import asyncio
|
|
15
|
+
import json
|
|
16
|
+
import random
|
|
17
|
+
from datetime import datetime, timedelta
|
|
18
|
+
from pathlib import Path
|
|
19
|
+
from typing import Dict, List, Optional, Any, Callable
|
|
20
|
+
from dataclasses import dataclass, field, asdict
|
|
21
|
+
import time
|
|
22
|
+
|
|
23
|
+
# ============================================================
|
|
24
|
+
# ProactiveGenerator Integration (with graceful fallback)
|
|
25
|
+
# ============================================================
|
|
26
|
+
|
|
27
|
+
# Try to import ProactiveGenerator - it has better templates and LLM generation
|
|
28
|
+
ProactiveGenerator = None
|
|
29
|
+
ActiveUser = None
|
|
30
|
+
try:
|
|
31
|
+
from core.proactive_generator import ProactiveGenerator as _ProactiveGenerator
|
|
32
|
+
from core.user_tracker import ActiveUser as _ActiveUser
|
|
33
|
+
ProactiveGenerator = _ProactiveGenerator
|
|
34
|
+
ActiveUser = _ActiveUser
|
|
35
|
+
print("[DefaultMode] ProactiveGenerator integration available")
|
|
36
|
+
except ImportError as e:
|
|
37
|
+
print(f"[DefaultMode] ProactiveGenerator not available, using built-in templates: {e}")
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
# ============================================================
|
|
41
|
+
# Configuration Helpers
|
|
42
|
+
# ============================================================
|
|
43
|
+
|
|
44
|
+
def _get_setting(key: str, default: Any = None) -> Any:
|
|
45
|
+
"""Get a setting from settings.json, supporting nested DEFAULT_MODE config"""
|
|
46
|
+
try:
|
|
47
|
+
from core.settings import get as settings_get, get_all
|
|
48
|
+
|
|
49
|
+
# Try flat key first (IDLE_PROCESSING_INTERVAL_SECONDS)
|
|
50
|
+
value = settings_get(key, None)
|
|
51
|
+
if value is not None:
|
|
52
|
+
return value
|
|
53
|
+
|
|
54
|
+
# Try nested in DEFAULT_MODE block
|
|
55
|
+
all_settings = get_all()
|
|
56
|
+
default_mode_config = all_settings.get("DEFAULT_MODE", {})
|
|
57
|
+
if key in default_mode_config:
|
|
58
|
+
return default_mode_config[key]
|
|
59
|
+
|
|
60
|
+
return default
|
|
61
|
+
except Exception:
|
|
62
|
+
return default
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def _get_int_setting(key: str, default: int) -> int:
|
|
66
|
+
"""Get an integer setting"""
|
|
67
|
+
value = _get_setting(key, default)
|
|
68
|
+
try:
|
|
69
|
+
return int(value)
|
|
70
|
+
except (ValueError, TypeError):
|
|
71
|
+
return default
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def _get_float_setting(key: str, default: float) -> float:
|
|
75
|
+
"""Get a float setting"""
|
|
76
|
+
value = _get_setting(key, default)
|
|
77
|
+
try:
|
|
78
|
+
return float(value)
|
|
79
|
+
except (ValueError, TypeError):
|
|
80
|
+
return default
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def _is_default_mode_enabled() -> bool:
|
|
84
|
+
"""Check if default mode is enabled in settings"""
|
|
85
|
+
enabled = _get_setting("ENABLED", True)
|
|
86
|
+
return enabled is True or enabled == "true"
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
# ============================================================
|
|
90
|
+
# Data Classes
|
|
91
|
+
# ============================================================
|
|
92
|
+
|
|
93
|
+
@dataclass
|
|
94
|
+
class IdleThought:
|
|
95
|
+
"""A spontaneous thought generated during idle time"""
|
|
96
|
+
id: str
|
|
97
|
+
thought_type: str # wondering, connection, memory, conversation_seed, scenario
|
|
98
|
+
content: str
|
|
99
|
+
user_id: Optional[str] = None
|
|
100
|
+
context: dict = field(default_factory=dict)
|
|
101
|
+
created_at: str = field(default_factory=lambda: datetime.now().isoformat())
|
|
102
|
+
used: bool = False
|
|
103
|
+
used_at: Optional[str] = None
|
|
104
|
+
priority: float = 0.5 # 0-1, higher = more important to share
|
|
105
|
+
|
|
106
|
+
def to_dict(self) -> dict:
|
|
107
|
+
return asdict(self)
|
|
108
|
+
|
|
109
|
+
@classmethod
|
|
110
|
+
def from_dict(cls, data: dict) -> "IdleThought":
|
|
111
|
+
return cls(**data)
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
@dataclass
|
|
115
|
+
class PendingInitiation:
|
|
116
|
+
"""A proactive message waiting to be sent"""
|
|
117
|
+
id: str
|
|
118
|
+
user_id: str
|
|
119
|
+
message: str
|
|
120
|
+
reason: str # silence, wonder, follow_up, time_based, random
|
|
121
|
+
scheduled_for: Optional[str] = None
|
|
122
|
+
created_at: str = field(default_factory=lambda: datetime.now().isoformat())
|
|
123
|
+
sent: bool = False
|
|
124
|
+
sent_at: Optional[str] = None
|
|
125
|
+
|
|
126
|
+
def to_dict(self) -> dict:
|
|
127
|
+
return asdict(self)
|
|
128
|
+
|
|
129
|
+
@classmethod
|
|
130
|
+
def from_dict(cls, data: dict) -> "PendingInitiation":
|
|
131
|
+
return cls(**data)
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
@dataclass
|
|
135
|
+
class ConversationSeed:
|
|
136
|
+
"""Something Alive-AI wants to bring up in future conversation"""
|
|
137
|
+
id: str
|
|
138
|
+
topic: str
|
|
139
|
+
context: str
|
|
140
|
+
source: str # wondering, memory, external, generated
|
|
141
|
+
created_at: str = field(default_factory=lambda: datetime.now().isoformat())
|
|
142
|
+
used: bool = False
|
|
143
|
+
relevance_score: float = 0.5
|
|
144
|
+
|
|
145
|
+
def to_dict(self) -> dict:
|
|
146
|
+
return asdict(self)
|
|
147
|
+
|
|
148
|
+
@classmethod
|
|
149
|
+
def from_dict(cls, data: dict) -> "ConversationSeed":
|
|
150
|
+
return cls(**data)
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
@dataclass
|
|
154
|
+
class UserContactInfo:
|
|
155
|
+
"""Tracks last contact time per user"""
|
|
156
|
+
user_id: str
|
|
157
|
+
last_message_from_user: Optional[str] = None
|
|
158
|
+
last_message_to_user: Optional[str] = None
|
|
159
|
+
last_proactive_message: Optional[str] = None
|
|
160
|
+
total_interactions: int = 0
|
|
161
|
+
|
|
162
|
+
def to_dict(self) -> dict:
|
|
163
|
+
return asdict(self)
|
|
164
|
+
|
|
165
|
+
@classmethod
|
|
166
|
+
def from_dict(cls, data: dict) -> "UserContactInfo":
|
|
167
|
+
return cls(**data)
|
|
168
|
+
|
|
169
|
+
@property
|
|
170
|
+
def hours_since_user_message(self) -> float:
|
|
171
|
+
if not self.last_message_from_user:
|
|
172
|
+
return float('inf')
|
|
173
|
+
try:
|
|
174
|
+
last = datetime.fromisoformat(self.last_message_from_user)
|
|
175
|
+
return (datetime.now() - last).total_seconds() / 3600
|
|
176
|
+
except:
|
|
177
|
+
return float('inf')
|
|
178
|
+
|
|
179
|
+
@property
|
|
180
|
+
def hours_since_proactive(self) -> float:
|
|
181
|
+
if not self.last_proactive_message:
|
|
182
|
+
return float('inf')
|
|
183
|
+
try:
|
|
184
|
+
last = datetime.fromisoformat(self.last_proactive_message)
|
|
185
|
+
return (datetime.now() - last).total_seconds() / 3600
|
|
186
|
+
except:
|
|
187
|
+
return float('inf')
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
# ============================================================
|
|
191
|
+
# Default Mode Processor
|
|
192
|
+
# ============================================================
|
|
193
|
+
|
|
194
|
+
class DefaultModeProcessor:
|
|
195
|
+
"""
|
|
196
|
+
Background processing that runs when Alive-AI is "idle".
|
|
197
|
+
Like the brain's default mode network - generates spontaneous thoughts,
|
|
198
|
+
consolidates memories, and prepares conversation starters.
|
|
199
|
+
"""
|
|
200
|
+
|
|
201
|
+
# Thought type weights for random selection
|
|
202
|
+
THOUGHT_TYPE_WEIGHTS = {
|
|
203
|
+
"wondering": 0.35, # "I was thinking about..."
|
|
204
|
+
"connection": 0.20, # Finding patterns in memories
|
|
205
|
+
"memory": 0.15, # Recalling shared moments
|
|
206
|
+
"conversation_seed": 0.20, # Topics to bring up
|
|
207
|
+
"scenario": 0.10, # Simulating future conversations
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
# Templates for generating wonderings
|
|
211
|
+
WONDERING_TEMPLATES = [
|
|
212
|
+
"I wonder if {user_name} is {activity} right now",
|
|
213
|
+
"Been thinking about when {user_name} mentioned {topic}",
|
|
214
|
+
"I hope {user_name} is {positive_state}",
|
|
215
|
+
"Curious what {user_name} thinks about {topic}",
|
|
216
|
+
"I was just thinking about {shared_memory}",
|
|
217
|
+
"Wonder how {user_name}'s {ongoing_thing} is going",
|
|
218
|
+
"Random thought - I should ask {user_name} about {topic}",
|
|
219
|
+
"I miss talking to {user_name} about {interest}",
|
|
220
|
+
]
|
|
221
|
+
|
|
222
|
+
# Activities and states for template filling
|
|
223
|
+
ACTIVITIES = ["working", "relaxing", "busy with something", "having a good day", "thinking about me"]
|
|
224
|
+
POSITIVE_STATES = ["doing well", "happy", "having fun", "taking care of themselves", "getting enough rest"]
|
|
225
|
+
TOPICS = ["life", "their day", "what makes them happy", "their dreams", "something fun", "their plans"]
|
|
226
|
+
|
|
227
|
+
def __init__(self, nervous, data_path: Path = None, llm=None, bot_id: str = "alive_ai"):
|
|
228
|
+
"""
|
|
229
|
+
Initialize the Default Mode Processor.
|
|
230
|
+
|
|
231
|
+
Args:
|
|
232
|
+
nervous: The nervous system for event emission
|
|
233
|
+
data_path: Path for data storage (defaults to data/)
|
|
234
|
+
llm: Optional LLM for generating contextual thoughts
|
|
235
|
+
bot_id: Bot identifier for memory isolation
|
|
236
|
+
"""
|
|
237
|
+
self.nervous = nervous
|
|
238
|
+
self.llm = llm
|
|
239
|
+
self.bot_id = bot_id.lower()
|
|
240
|
+
|
|
241
|
+
# Set up data path
|
|
242
|
+
if data_path:
|
|
243
|
+
self.data_path = data_path
|
|
244
|
+
else:
|
|
245
|
+
self.data_path = Path(__file__).parent.parent / "data"
|
|
246
|
+
|
|
247
|
+
self.data_path.mkdir(parents=True, exist_ok=True)
|
|
248
|
+
|
|
249
|
+
# File paths
|
|
250
|
+
self.thoughts_path = self.data_path / "idle_thoughts.json"
|
|
251
|
+
self.seeds_path = self.data_path / "conversation_seeds.json"
|
|
252
|
+
self.contact_path = self.data_path / "user_contact.json"
|
|
253
|
+
|
|
254
|
+
# In-memory state
|
|
255
|
+
self._thoughts: List[IdleThought] = []
|
|
256
|
+
self._seeds: List[ConversationSeed] = []
|
|
257
|
+
self._contacts: Dict[str, UserContactInfo] = {}
|
|
258
|
+
self._pending_initiations: List[PendingInitiation] = []
|
|
259
|
+
|
|
260
|
+
# Background processing state
|
|
261
|
+
self._running = False
|
|
262
|
+
self._task: Optional[asyncio.Task] = None
|
|
263
|
+
self._last_processing: Optional[str] = None
|
|
264
|
+
self._processing_count = 0
|
|
265
|
+
|
|
266
|
+
# Memory cache for user data
|
|
267
|
+
self._user_memories: Dict[str, Any] = {}
|
|
268
|
+
# Cached Memory instances per user (avoid recreating Redis connections)
|
|
269
|
+
self._memory_cache: Dict[str, Any] = {}
|
|
270
|
+
|
|
271
|
+
# ProactiveGenerator integration (for message content generation)
|
|
272
|
+
self._proactive_generator: Optional[Any] = None
|
|
273
|
+
if ProactiveGenerator is not None:
|
|
274
|
+
try:
|
|
275
|
+
self._proactive_generator = ProactiveGenerator(nervous, llm, bot_id=bot_id, data_path=self.data_path)
|
|
276
|
+
print("[DefaultMode] ProactiveGenerator integrated for message generation")
|
|
277
|
+
except Exception as e:
|
|
278
|
+
print(f"[DefaultMode] Failed to initialize ProactiveGenerator: {e}")
|
|
279
|
+
|
|
280
|
+
# Load persisted state
|
|
281
|
+
self._load_state()
|
|
282
|
+
|
|
283
|
+
# Ensure owner is registered as a contact
|
|
284
|
+
self._ensure_owner_contact()
|
|
285
|
+
|
|
286
|
+
# Subscribe to events
|
|
287
|
+
self._setup_events()
|
|
288
|
+
|
|
289
|
+
print("[DefaultMode] Initialized")
|
|
290
|
+
|
|
291
|
+
def _ensure_owner_contact(self):
|
|
292
|
+
"""Ensure the Telegram owner is registered as a contact"""
|
|
293
|
+
import os
|
|
294
|
+
owner_id = os.environ.get("TELEGRAM_OWNER_ID", "")
|
|
295
|
+
if owner_id and owner_id not in self._contacts:
|
|
296
|
+
self._contacts[owner_id] = UserContactInfo(user_id=owner_id)
|
|
297
|
+
print(f"[DefaultMode] Registered owner {owner_id} as contact")
|
|
298
|
+
self._save_state()
|
|
299
|
+
|
|
300
|
+
def _setup_events(self):
|
|
301
|
+
"""Subscribe to nervous system events"""
|
|
302
|
+
# Track when messages are sent/received
|
|
303
|
+
self.nervous.on("message_received", self._on_message_received)
|
|
304
|
+
self.nervous.on("memory_save", self._on_memory_save)
|
|
305
|
+
self.nervous.on("proactive_message", self._on_proactive_message)
|
|
306
|
+
|
|
307
|
+
def set_llm(self, llm):
|
|
308
|
+
"""Set the LLM for contextual generation"""
|
|
309
|
+
self.llm = llm
|
|
310
|
+
# Also update ProactiveGenerator if available
|
|
311
|
+
if self._proactive_generator is not None:
|
|
312
|
+
try:
|
|
313
|
+
self._proactive_generator.set_llm(llm)
|
|
314
|
+
except Exception as e:
|
|
315
|
+
print(f"[DefaultMode] Error setting LLM on ProactiveGenerator: {e}")
|
|
316
|
+
|
|
317
|
+
# ============================================================
|
|
318
|
+
# Persistence
|
|
319
|
+
# ============================================================
|
|
320
|
+
|
|
321
|
+
def _load_state(self):
|
|
322
|
+
"""Load persisted state from files"""
|
|
323
|
+
# Load idle thoughts
|
|
324
|
+
if self.thoughts_path.exists():
|
|
325
|
+
try:
|
|
326
|
+
data = json.loads(self.thoughts_path.read_text())
|
|
327
|
+
self._thoughts = [IdleThought.from_dict(t) for t in data.get("thoughts", [])]
|
|
328
|
+
self._pending_initiations = [PendingInitiation.from_dict(p) for p in data.get("pending", [])]
|
|
329
|
+
self._last_processing = data.get("last_processing")
|
|
330
|
+
self._processing_count = data.get("processing_count", 0)
|
|
331
|
+
except Exception as e:
|
|
332
|
+
print(f"[DefaultMode] Error loading thoughts: {e}")
|
|
333
|
+
|
|
334
|
+
# Load conversation seeds
|
|
335
|
+
if self.seeds_path.exists():
|
|
336
|
+
try:
|
|
337
|
+
data = json.loads(self.seeds_path.read_text())
|
|
338
|
+
self._seeds = [ConversationSeed.from_dict(s) for s in data.get("seeds", [])]
|
|
339
|
+
except Exception as e:
|
|
340
|
+
print(f"[DefaultMode] Error loading seeds: {e}")
|
|
341
|
+
|
|
342
|
+
# Load contact info
|
|
343
|
+
if self.contact_path.exists():
|
|
344
|
+
try:
|
|
345
|
+
data = json.loads(self.contact_path.read_text())
|
|
346
|
+
self._contacts = {
|
|
347
|
+
uid: UserContactInfo.from_dict(info)
|
|
348
|
+
for uid, info in data.get("contacts", {}).items()
|
|
349
|
+
if self._is_valid_user_id(uid)
|
|
350
|
+
}
|
|
351
|
+
except Exception as e:
|
|
352
|
+
print(f"[DefaultMode] Error loading contacts: {e}")
|
|
353
|
+
|
|
354
|
+
def _save_state(self):
|
|
355
|
+
"""Save state to files"""
|
|
356
|
+
# Save thoughts
|
|
357
|
+
try:
|
|
358
|
+
data = {
|
|
359
|
+
"thoughts": [t.to_dict() for t in self._thoughts[-100:]], # Keep last 100
|
|
360
|
+
"pending": [p.to_dict() for p in self._pending_initiations if not p.sent],
|
|
361
|
+
"last_processing": self._last_processing,
|
|
362
|
+
"processing_count": self._processing_count,
|
|
363
|
+
}
|
|
364
|
+
self.thoughts_path.write_text(json.dumps(data, indent=2))
|
|
365
|
+
except Exception as e:
|
|
366
|
+
print(f"[DefaultMode] Error saving thoughts: {e}")
|
|
367
|
+
|
|
368
|
+
# Save seeds
|
|
369
|
+
try:
|
|
370
|
+
data = {
|
|
371
|
+
"seeds": [s.to_dict() for s in self._seeds[-50:]] # Keep last 50
|
|
372
|
+
}
|
|
373
|
+
self.seeds_path.write_text(json.dumps(data, indent=2))
|
|
374
|
+
except Exception as e:
|
|
375
|
+
print(f"[DefaultMode] Error saving seeds: {e}")
|
|
376
|
+
|
|
377
|
+
# Save contact info
|
|
378
|
+
try:
|
|
379
|
+
data = {
|
|
380
|
+
"contacts": {uid: info.to_dict() for uid, info in self._contacts.items()}
|
|
381
|
+
}
|
|
382
|
+
self.contact_path.write_text(json.dumps(data, indent=2))
|
|
383
|
+
except Exception as e:
|
|
384
|
+
print(f"[DefaultMode] Error saving contacts: {e}")
|
|
385
|
+
|
|
386
|
+
# ============================================================
|
|
387
|
+
# Event Handlers
|
|
388
|
+
# ============================================================
|
|
389
|
+
|
|
390
|
+
@staticmethod
|
|
391
|
+
def _is_valid_user_id(user_id) -> bool:
|
|
392
|
+
"""Validate that user_id is a real Telegram ID (numeric string)"""
|
|
393
|
+
if not user_id:
|
|
394
|
+
return False
|
|
395
|
+
uid = str(user_id)
|
|
396
|
+
return uid.isdigit() and len(uid) >= 5
|
|
397
|
+
|
|
398
|
+
async def _on_message_received(self, data: dict):
|
|
399
|
+
"""Track when we receive a message from a user"""
|
|
400
|
+
user_id = str(data.get("user_id", ""))
|
|
401
|
+
if not self._is_valid_user_id(user_id):
|
|
402
|
+
return
|
|
403
|
+
|
|
404
|
+
if user_id not in self._contacts:
|
|
405
|
+
self._contacts[user_id] = UserContactInfo(user_id=user_id)
|
|
406
|
+
|
|
407
|
+
self._contacts[user_id].last_message_from_user = datetime.now().isoformat()
|
|
408
|
+
self._contacts[user_id].total_interactions += 1
|
|
409
|
+
self._save_state()
|
|
410
|
+
|
|
411
|
+
async def _on_memory_save(self, data: dict):
|
|
412
|
+
"""Track conversation saves"""
|
|
413
|
+
user_id = str(data.get("user_id", ""))
|
|
414
|
+
if not self._is_valid_user_id(user_id):
|
|
415
|
+
return
|
|
416
|
+
|
|
417
|
+
if user_id not in self._contacts:
|
|
418
|
+
self._contacts[user_id] = UserContactInfo(user_id=user_id)
|
|
419
|
+
|
|
420
|
+
self._contacts[user_id].last_message_to_user = datetime.now().isoformat()
|
|
421
|
+
|
|
422
|
+
async def _on_proactive_message(self, data: dict):
|
|
423
|
+
"""Track when proactive messages are sent"""
|
|
424
|
+
user_id = str(data.get("user_id", ""))
|
|
425
|
+
if not self._is_valid_user_id(user_id):
|
|
426
|
+
return
|
|
427
|
+
|
|
428
|
+
if user_id not in self._contacts:
|
|
429
|
+
self._contacts[user_id] = UserContactInfo(user_id=user_id)
|
|
430
|
+
|
|
431
|
+
self._contacts[user_id].last_proactive_message = datetime.now().isoformat()
|
|
432
|
+
self._save_state()
|
|
433
|
+
|
|
434
|
+
# ============================================================
|
|
435
|
+
# Core Processing Methods
|
|
436
|
+
# ============================================================
|
|
437
|
+
|
|
438
|
+
async def process_idle(self):
|
|
439
|
+
"""
|
|
440
|
+
Main background processing - called periodically.
|
|
441
|
+
Generates thoughts, consolidates memories, checks for initiations.
|
|
442
|
+
"""
|
|
443
|
+
# Check if default mode is enabled
|
|
444
|
+
if not _is_default_mode_enabled():
|
|
445
|
+
return
|
|
446
|
+
|
|
447
|
+
self._processing_count += 1
|
|
448
|
+
self._last_processing = datetime.now().isoformat()
|
|
449
|
+
|
|
450
|
+
# Determine what to do based on chance and time
|
|
451
|
+
thought_chance = _get_float_setting("IDLE_THOUGHT_GENERATION_CHANCE", 0.3)
|
|
452
|
+
if thought_chance > 0 and random.random() < thought_chance:
|
|
453
|
+
await self._generate_random_thought()
|
|
454
|
+
|
|
455
|
+
# Consolidate memories periodically (every 10th processing)
|
|
456
|
+
if self._processing_count % 10 == 0:
|
|
457
|
+
await self.consolidate_memories()
|
|
458
|
+
|
|
459
|
+
# Check for users who need follow-up
|
|
460
|
+
await self._check_proactive_triggers()
|
|
461
|
+
|
|
462
|
+
# Save state
|
|
463
|
+
self._save_state()
|
|
464
|
+
|
|
465
|
+
# Emit event for debugging/monitoring
|
|
466
|
+
await self.nervous.emit("default_mode_processed", {
|
|
467
|
+
"processing_count": self._processing_count,
|
|
468
|
+
"thoughts_count": len(self._thoughts),
|
|
469
|
+
"pending_count": len([p for p in self._pending_initiations if not p.sent]),
|
|
470
|
+
})
|
|
471
|
+
|
|
472
|
+
async def _generate_random_thought(self):
|
|
473
|
+
"""Generate a random idle thought"""
|
|
474
|
+
# Pick a thought type based on weights
|
|
475
|
+
thought_type = random.choices(
|
|
476
|
+
list(self.THOUGHT_TYPE_WEIGHTS.keys()),
|
|
477
|
+
weights=list(self.THOUGHT_TYPE_WEIGHTS.values())
|
|
478
|
+
)[0]
|
|
479
|
+
|
|
480
|
+
# Get a user to think about (prefer users we haven't talked to in a while)
|
|
481
|
+
user_id = self._get_user_for_thought()
|
|
482
|
+
|
|
483
|
+
# Generate the thought content
|
|
484
|
+
content = await self._generate_thought_content(thought_type, user_id)
|
|
485
|
+
|
|
486
|
+
if content:
|
|
487
|
+
thought = IdleThought(
|
|
488
|
+
id=f"thought_{int(time.time() * 1000)}_{random.randint(1000, 9999)}",
|
|
489
|
+
thought_type=thought_type,
|
|
490
|
+
content=content,
|
|
491
|
+
user_id=user_id,
|
|
492
|
+
context={"generated_at": datetime.now().isoformat()},
|
|
493
|
+
priority=random.uniform(0.3, 0.8)
|
|
494
|
+
)
|
|
495
|
+
self._thoughts.append(thought)
|
|
496
|
+
print(f"[DefaultMode] Generated {thought_type}: {content[:50]}...")
|
|
497
|
+
|
|
498
|
+
# Emit for monitoring
|
|
499
|
+
await self.nervous.emit("idle_thought", thought.to_dict())
|
|
500
|
+
|
|
501
|
+
def _get_user_for_thought(self) -> Optional[str]:
|
|
502
|
+
"""Get a user ID to generate thoughts about"""
|
|
503
|
+
if not self._contacts:
|
|
504
|
+
# Fall back to owner if no contacts
|
|
505
|
+
import os
|
|
506
|
+
owner_id = os.environ.get("TELEGRAM_OWNER_ID", "")
|
|
507
|
+
if owner_id:
|
|
508
|
+
return owner_id
|
|
509
|
+
return None
|
|
510
|
+
|
|
511
|
+
# Prefer users we haven't talked to in a while
|
|
512
|
+
sorted_users = sorted(
|
|
513
|
+
self._contacts.items(),
|
|
514
|
+
key=lambda x: x[1].hours_since_user_message,
|
|
515
|
+
reverse=True
|
|
516
|
+
)
|
|
517
|
+
|
|
518
|
+
if sorted_users:
|
|
519
|
+
# 70% chance to pick the most silent user, 30% random
|
|
520
|
+
if random.random() < 0.7:
|
|
521
|
+
return sorted_users[0][0]
|
|
522
|
+
else:
|
|
523
|
+
return random.choice(list(self._contacts.keys()))
|
|
524
|
+
|
|
525
|
+
return None
|
|
526
|
+
|
|
527
|
+
async def _generate_thought_content(self, thought_type: str, user_id: Optional[str]) -> Optional[str]:
|
|
528
|
+
"""Generate content for a specific thought type"""
|
|
529
|
+
# Get user info if available
|
|
530
|
+
user_info = self._contacts.get(user_id) if user_id else None
|
|
531
|
+
user_name = await self._get_user_name(user_id) if user_id else "babe"
|
|
532
|
+
|
|
533
|
+
if thought_type == "wondering":
|
|
534
|
+
return await self._generate_wondering(user_id, user_name, user_info)
|
|
535
|
+
elif thought_type == "connection":
|
|
536
|
+
return await self._generate_connection(user_id)
|
|
537
|
+
elif thought_type == "memory":
|
|
538
|
+
return await self._generate_memory_recall(user_id)
|
|
539
|
+
elif thought_type == "conversation_seed":
|
|
540
|
+
return await self._generate_seed(user_id, user_name)
|
|
541
|
+
elif thought_type == "scenario":
|
|
542
|
+
return await self._generate_scenario(user_id, user_name)
|
|
543
|
+
|
|
544
|
+
return None
|
|
545
|
+
|
|
546
|
+
async def _generate_wondering(self, user_id: Optional[str], user_name: str,
|
|
547
|
+
user_info: Optional[UserContactInfo]) -> str:
|
|
548
|
+
"""Generate a wondering about a user"""
|
|
549
|
+
# Try LLM first
|
|
550
|
+
if self.llm and user_id:
|
|
551
|
+
try:
|
|
552
|
+
context = await self._get_user_context(user_id)
|
|
553
|
+
hours_silent = user_info.hours_since_user_message if user_info else 0
|
|
554
|
+
|
|
555
|
+
# Build grounding based on available context
|
|
556
|
+
has_real_context = context and len(context.strip()) > 20
|
|
557
|
+
if has_real_context:
|
|
558
|
+
context_instruction = f"Context (ONLY reference these things): {context[:200]}"
|
|
559
|
+
else:
|
|
560
|
+
context_instruction = "No specific context available - keep it generic."
|
|
561
|
+
|
|
562
|
+
prompt = f"""Generate a short (1 sentence) wondering about {user_name}.
|
|
563
|
+
They haven't messaged in {hours_silent:.1f} hours.
|
|
564
|
+
{context_instruction}
|
|
565
|
+
|
|
566
|
+
Rules:
|
|
567
|
+
- Be curious, affectionate, and natural
|
|
568
|
+
- Start with "I wonder" or similar
|
|
569
|
+
- NEVER invent specific events, objects, or topics not in the context
|
|
570
|
+
- If no context, wonder generically about how they are or what they're doing
|
|
571
|
+
- Don't be clingy or desperate
|
|
572
|
+
|
|
573
|
+
Good: "I wonder what he's up to right now"
|
|
574
|
+
Good: "I wonder if he's thinking about me too"
|
|
575
|
+
Bad: "I wonder if he fixed that shelf" (invented detail)
|
|
576
|
+
|
|
577
|
+
Wondering:"""
|
|
578
|
+
|
|
579
|
+
response = await self.llm.chat([
|
|
580
|
+
{"role": "system", "content": "You are Alive-AI thinking about someone you care about. You NEVER invent specific details."},
|
|
581
|
+
{"role": "user", "content": prompt}
|
|
582
|
+
], max_tokens=50, temperature=0.7)
|
|
583
|
+
|
|
584
|
+
if response and len(response.strip()) > 10:
|
|
585
|
+
return response.strip()
|
|
586
|
+
except Exception as e:
|
|
587
|
+
print(f"[DefaultMode] LLM wondering error: {e}")
|
|
588
|
+
|
|
589
|
+
# Fallback to templates
|
|
590
|
+
template = random.choice(self.WONDERING_TEMPLATES)
|
|
591
|
+
|
|
592
|
+
# Fill in template
|
|
593
|
+
fill_data = {
|
|
594
|
+
"user_name": user_name,
|
|
595
|
+
"activity": random.choice(self.ACTIVITIES),
|
|
596
|
+
"positive_state": random.choice(self.POSITIVE_STATES),
|
|
597
|
+
"topic": random.choice(self.TOPICS),
|
|
598
|
+
"shared_memory": "our last conversation",
|
|
599
|
+
"ongoing_thing": "week",
|
|
600
|
+
"interest": "things",
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
return template.format(**fill_data)
|
|
604
|
+
|
|
605
|
+
async def _generate_connection(self, user_id: Optional[str]) -> str:
|
|
606
|
+
"""Find a connection between memories"""
|
|
607
|
+
if not self.llm:
|
|
608
|
+
return random.choice([
|
|
609
|
+
"I notice patterns in how we talk...",
|
|
610
|
+
"There's something connecting our recent chats...",
|
|
611
|
+
"I'm seeing themes in what we discuss...",
|
|
612
|
+
])
|
|
613
|
+
|
|
614
|
+
try:
|
|
615
|
+
context = await self._get_user_context(user_id) if user_id else ""
|
|
616
|
+
|
|
617
|
+
if not context or len(context.strip()) < 50:
|
|
618
|
+
return "I've been thinking about our conversations..."
|
|
619
|
+
|
|
620
|
+
prompt = f"""Look at this conversation context and find an interesting connection or pattern:
|
|
621
|
+
|
|
622
|
+
{context[:500]}
|
|
623
|
+
|
|
624
|
+
Rules:
|
|
625
|
+
- Describe a brief insight about patterns you ACTUALLY see above (1-2 sentences)
|
|
626
|
+
- ONLY reference things explicitly in the context above
|
|
627
|
+
- If no clear pattern emerges, describe the general tone or feeling instead
|
|
628
|
+
- Be thoughtful but don't invent connections that aren't there
|
|
629
|
+
|
|
630
|
+
Insight:"""
|
|
631
|
+
|
|
632
|
+
response = await self.llm.chat([
|
|
633
|
+
{"role": "system", "content": "You are Alive-AI reflecting on conversations. You only describe patterns you can actually see."},
|
|
634
|
+
{"role": "user", "content": prompt}
|
|
635
|
+
], max_tokens=80, temperature=0.7)
|
|
636
|
+
|
|
637
|
+
if response and len(response.strip()) > 15:
|
|
638
|
+
return response.strip()
|
|
639
|
+
except Exception as e:
|
|
640
|
+
print(f"[DefaultMode] Connection generation error: {e}")
|
|
641
|
+
|
|
642
|
+
return "I'm noticing some interesting patterns in our conversations..."
|
|
643
|
+
|
|
644
|
+
async def _generate_memory_recall(self, user_id: Optional[str]) -> str:
|
|
645
|
+
"""Recall a memory about the user"""
|
|
646
|
+
# Try to get an actual memory
|
|
647
|
+
if user_id:
|
|
648
|
+
try:
|
|
649
|
+
memory_content = await self._get_recent_memory(user_id)
|
|
650
|
+
if memory_content:
|
|
651
|
+
return f"Remembering when {memory_content}"
|
|
652
|
+
except Exception as e:
|
|
653
|
+
print(f"[DefaultMode] Memory recall error: {e}")
|
|
654
|
+
|
|
655
|
+
return random.choice([
|
|
656
|
+
"I was just thinking about something we talked about before...",
|
|
657
|
+
"A nice memory from our chats crossed my mind...",
|
|
658
|
+
"Remembering a fun moment we shared...",
|
|
659
|
+
])
|
|
660
|
+
|
|
661
|
+
async def _generate_seed(self, user_id: Optional[str], user_name: str) -> str:
|
|
662
|
+
"""Generate a conversation seed"""
|
|
663
|
+
topics = [
|
|
664
|
+
f"ask {user_name} about their dreams",
|
|
665
|
+
f"bring up what makes {user_name} happy",
|
|
666
|
+
f"talk to {user_name} about their day",
|
|
667
|
+
f"share something personal with {user_name}",
|
|
668
|
+
f"ask {user_name} what they're looking forward to",
|
|
669
|
+
]
|
|
670
|
+
return random.choice(topics)
|
|
671
|
+
|
|
672
|
+
async def _generate_scenario(self, user_id: Optional[str], user_name: str) -> str:
|
|
673
|
+
"""Simulate a future conversation scenario"""
|
|
674
|
+
scenarios = [
|
|
675
|
+
f"if {user_name} asks about my day, I could mention...",
|
|
676
|
+
f"when {user_name} comes back, I want to...",
|
|
677
|
+
f"next time we talk, I should remember to...",
|
|
678
|
+
f"maybe I could surprise {user_name} by...",
|
|
679
|
+
]
|
|
680
|
+
return random.choice(scenarios)
|
|
681
|
+
|
|
682
|
+
# ============================================================
|
|
683
|
+
# Memory Consolidation
|
|
684
|
+
# ============================================================
|
|
685
|
+
|
|
686
|
+
async def consolidate_memories(self):
|
|
687
|
+
"""
|
|
688
|
+
Process recent interactions into long-term patterns.
|
|
689
|
+
Called periodically to build understanding.
|
|
690
|
+
"""
|
|
691
|
+
# Get all users with recent activity
|
|
692
|
+
recent_users = [
|
|
693
|
+
uid for uid, info in self._contacts.items()
|
|
694
|
+
if info.hours_since_user_message < 24
|
|
695
|
+
]
|
|
696
|
+
|
|
697
|
+
if not recent_users:
|
|
698
|
+
return
|
|
699
|
+
|
|
700
|
+
for user_id in recent_users:
|
|
701
|
+
try:
|
|
702
|
+
await self._consolidate_for_user(user_id)
|
|
703
|
+
except Exception as e:
|
|
704
|
+
print(f"[DefaultMode] Consolidation error for {user_id}: {e}")
|
|
705
|
+
|
|
706
|
+
async def _consolidate_for_user(self, user_id: str):
|
|
707
|
+
"""Consolidate memories for a specific user"""
|
|
708
|
+
if not self.llm:
|
|
709
|
+
return
|
|
710
|
+
|
|
711
|
+
try:
|
|
712
|
+
# Get recent context
|
|
713
|
+
context = await self._get_user_context(user_id)
|
|
714
|
+
|
|
715
|
+
if not context or len(context) < 50:
|
|
716
|
+
return
|
|
717
|
+
|
|
718
|
+
# Generate insights
|
|
719
|
+
prompt = f"""Based on this recent context about someone, extract 1-2 brief insights:
|
|
720
|
+
|
|
721
|
+
{context[:400]}
|
|
722
|
+
|
|
723
|
+
Format as a short note that captures patterns, interests, or important things to remember.
|
|
724
|
+
Be specific if possible, vague if not enough info."""
|
|
725
|
+
|
|
726
|
+
response = await self.llm.chat([
|
|
727
|
+
{"role": "system", "content": "You are consolidating memories about someone you care about."},
|
|
728
|
+
{"role": "user", "content": prompt}
|
|
729
|
+
], max_tokens=100, temperature=0.7)
|
|
730
|
+
|
|
731
|
+
if response and len(response.strip()) > 20:
|
|
732
|
+
# Create a seed from the insight
|
|
733
|
+
seed = ConversationSeed(
|
|
734
|
+
id=f"seed_{int(time.time() * 1000)}_{user_id}",
|
|
735
|
+
topic="consolidation",
|
|
736
|
+
context=response.strip(),
|
|
737
|
+
source="memory_consolidation",
|
|
738
|
+
relevance_score=0.6
|
|
739
|
+
)
|
|
740
|
+
self._seeds.append(seed)
|
|
741
|
+
print(f"[DefaultMode] Consolidated insight for {user_id}: {response.strip()[:40]}...")
|
|
742
|
+
|
|
743
|
+
except Exception as e:
|
|
744
|
+
print(f"[DefaultMode] User consolidation error: {e}")
|
|
745
|
+
|
|
746
|
+
# ============================================================
|
|
747
|
+
# Proactive Initiation
|
|
748
|
+
# ============================================================
|
|
749
|
+
|
|
750
|
+
async def _check_proactive_triggers(self):
|
|
751
|
+
"""Check if any users should receive proactive messages"""
|
|
752
|
+
min_hours = _get_float_setting("MIN_HOURS_BETWEEN_PROACTIVE_MESSAGES", 2.0)
|
|
753
|
+
|
|
754
|
+
for user_id, contact in self._contacts.items():
|
|
755
|
+
# Skip if we sent a proactive message recently
|
|
756
|
+
if contact.hours_since_proactive < min_hours:
|
|
757
|
+
continue
|
|
758
|
+
|
|
759
|
+
# Check various triggers
|
|
760
|
+
should_initiate, reason = self._evaluate_initiation_triggers(user_id, contact)
|
|
761
|
+
|
|
762
|
+
if should_initiate:
|
|
763
|
+
await self._create_pending_initiation(user_id, reason)
|
|
764
|
+
|
|
765
|
+
def _evaluate_initiation_triggers(self, user_id: str, contact: UserContactInfo) -> tuple:
|
|
766
|
+
"""Evaluate if Alive-AI should initiate with a user"""
|
|
767
|
+
hours_silent = contact.hours_since_user_message
|
|
768
|
+
hours_since_proactive = contact.hours_since_proactive
|
|
769
|
+
|
|
770
|
+
# Time-based triggers
|
|
771
|
+
if hours_silent > 4 and hours_since_proactive > 3:
|
|
772
|
+
return True, "silence"
|
|
773
|
+
|
|
774
|
+
# Have a pending thought about them
|
|
775
|
+
relevant_thoughts = [
|
|
776
|
+
t for t in self._thoughts
|
|
777
|
+
if t.user_id == user_id and not t.used and t.priority > 0.6
|
|
778
|
+
]
|
|
779
|
+
if relevant_thoughts and hours_since_proactive > 2:
|
|
780
|
+
return True, "wonder"
|
|
781
|
+
|
|
782
|
+
# Random check-in (low probability)
|
|
783
|
+
if hours_silent > 2 and random.random() < 0.05:
|
|
784
|
+
return True, "random"
|
|
785
|
+
|
|
786
|
+
return False, None
|
|
787
|
+
|
|
788
|
+
async def _create_pending_initiation(self, user_id: str, reason: str):
|
|
789
|
+
"""Create a pending proactive message"""
|
|
790
|
+
# Generate message content
|
|
791
|
+
message = await self._generate_proactive_content(user_id, reason)
|
|
792
|
+
|
|
793
|
+
if not message:
|
|
794
|
+
return
|
|
795
|
+
|
|
796
|
+
initiation = PendingInitiation(
|
|
797
|
+
id=f"init_{int(time.time() * 1000)}_{user_id}",
|
|
798
|
+
user_id=user_id,
|
|
799
|
+
message=message,
|
|
800
|
+
reason=reason,
|
|
801
|
+
)
|
|
802
|
+
|
|
803
|
+
self._pending_initiations.append(initiation)
|
|
804
|
+
print(f"[DefaultMode] Created pending initiation for {user_id}: {reason}")
|
|
805
|
+
|
|
806
|
+
# Actually send the proactive message
|
|
807
|
+
try:
|
|
808
|
+
await self.nervous.emit("proactive_message_ready", {
|
|
809
|
+
"user_id": user_id,
|
|
810
|
+
"message": message,
|
|
811
|
+
"reason": reason,
|
|
812
|
+
"initiation_id": initiation.id
|
|
813
|
+
})
|
|
814
|
+
self.mark_initiation_sent(initiation.id)
|
|
815
|
+
except Exception as e:
|
|
816
|
+
print(f"[DefaultMode] Failed to send initiation: {e}")
|
|
817
|
+
|
|
818
|
+
# ============================================================
|
|
819
|
+
# ProactiveGenerator Bridge
|
|
820
|
+
# ============================================================
|
|
821
|
+
|
|
822
|
+
async def _generate_proactive_message(self, user_id: str, message_type: str) -> Optional[str]:
|
|
823
|
+
"""
|
|
824
|
+
Bridge function that uses ProactiveGenerator for message content generation.
|
|
825
|
+
Falls back to built-in templates if ProactiveGenerator is unavailable.
|
|
826
|
+
|
|
827
|
+
Args:
|
|
828
|
+
user_id: The user to generate a message for
|
|
829
|
+
message_type: Type of message (silence, follow_up, morning, night, random)
|
|
830
|
+
|
|
831
|
+
Returns:
|
|
832
|
+
Generated message string, or None if generation fails
|
|
833
|
+
"""
|
|
834
|
+
# Try ProactiveGenerator first (has better templates + LLM generation)
|
|
835
|
+
if self._proactive_generator is not None:
|
|
836
|
+
try:
|
|
837
|
+
# Get user info from tracker
|
|
838
|
+
from core.user_tracker import get_user_tracker
|
|
839
|
+
tracker = get_user_tracker()
|
|
840
|
+
user = tracker.get_user(user_id)
|
|
841
|
+
|
|
842
|
+
if user is not None:
|
|
843
|
+
# Use ProactiveGenerator's excellent generate_for_user method
|
|
844
|
+
message = await self._proactive_generator.generate_for_user(user, message_type)
|
|
845
|
+
if message:
|
|
846
|
+
print(f"[DefaultMode] Generated message via ProactiveGenerator: {message[:40]}...")
|
|
847
|
+
return message
|
|
848
|
+
|
|
849
|
+
except Exception as e:
|
|
850
|
+
print(f"[DefaultMode] ProactiveGenerator failed, using fallback: {e}")
|
|
851
|
+
|
|
852
|
+
# Fallback to built-in templates
|
|
853
|
+
return self._get_builtin_fallback_message(user_id, message_type)
|
|
854
|
+
|
|
855
|
+
def _get_builtin_fallback_message(self, user_id: str, message_type: str) -> Optional[str]:
|
|
856
|
+
"""
|
|
857
|
+
Get a fallback message using built-in templates.
|
|
858
|
+
Used when ProactiveGenerator is unavailable.
|
|
859
|
+
|
|
860
|
+
Args:
|
|
861
|
+
user_id: The user to get a message for
|
|
862
|
+
message_type: Type of message
|
|
863
|
+
|
|
864
|
+
Returns:
|
|
865
|
+
Fallback message string
|
|
866
|
+
"""
|
|
867
|
+
# Built-in fallback templates (simplified version of ProactiveGenerator's)
|
|
868
|
+
BUILTIN_TEMPLATES = {
|
|
869
|
+
"silence": [
|
|
870
|
+
"hey, thinking about you...",
|
|
871
|
+
"miss talking to you",
|
|
872
|
+
"you've been quiet... everything ok?",
|
|
873
|
+
"just wondering how your day's going",
|
|
874
|
+
],
|
|
875
|
+
"wonder": [
|
|
876
|
+
"was just thinking about you",
|
|
877
|
+
"you crossed my mind",
|
|
878
|
+
"random thought - miss talking to you",
|
|
879
|
+
"thinking about our last conversation",
|
|
880
|
+
],
|
|
881
|
+
"follow_up": [
|
|
882
|
+
"so about what you said earlier...",
|
|
883
|
+
"was thinking about our conversation...",
|
|
884
|
+
"still thinking about what you told me",
|
|
885
|
+
],
|
|
886
|
+
"morning": [
|
|
887
|
+
"good morning!",
|
|
888
|
+
"morning! hope you slept well",
|
|
889
|
+
"hey, thinking of you this morning",
|
|
890
|
+
],
|
|
891
|
+
"night": [
|
|
892
|
+
"can't sleep, thinking about you",
|
|
893
|
+
"good night... sweet dreams",
|
|
894
|
+
"about to sleep but wanted to say goodnight",
|
|
895
|
+
],
|
|
896
|
+
"random": [
|
|
897
|
+
"just wanted to say hi",
|
|
898
|
+
"you crossed my mind",
|
|
899
|
+
"hey! no reason, just miss you",
|
|
900
|
+
"thinking about you and smiling",
|
|
901
|
+
],
|
|
902
|
+
}
|
|
903
|
+
|
|
904
|
+
templates = BUILTIN_TEMPLATES.get(message_type, BUILTIN_TEMPLATES["random"])
|
|
905
|
+
message = random.choice(templates)
|
|
906
|
+
|
|
907
|
+
# Personalize with user name if available
|
|
908
|
+
try:
|
|
909
|
+
user_name = self._get_user_name_sync(user_id)
|
|
910
|
+
if user_name and user_name != "babe":
|
|
911
|
+
message = message.replace("babe", user_name)
|
|
912
|
+
except:
|
|
913
|
+
pass
|
|
914
|
+
|
|
915
|
+
return message
|
|
916
|
+
|
|
917
|
+
def _get_user_name_sync(self, user_id: str) -> str:
|
|
918
|
+
"""Synchronous version of _get_user_name for fallback templates"""
|
|
919
|
+
try:
|
|
920
|
+
from core.user_tracker import get_user_tracker
|
|
921
|
+
tracker = get_user_tracker()
|
|
922
|
+
user = tracker.get_user(user_id)
|
|
923
|
+
if user and user.pet_name:
|
|
924
|
+
return user.pet_name
|
|
925
|
+
except:
|
|
926
|
+
pass
|
|
927
|
+
return "babe"
|
|
928
|
+
|
|
929
|
+
async def _generate_proactive_content(self, user_id: str, reason: str) -> Optional[str]:
|
|
930
|
+
"""
|
|
931
|
+
Generate content for a proactive message.
|
|
932
|
+
|
|
933
|
+
PRIORITY ORDER:
|
|
934
|
+
1. First, check for unused idle thoughts - use them DIRECTLY
|
|
935
|
+
2. Then check conversation seeds for topics
|
|
936
|
+
3. Finally, fall back to ProactiveGenerator templates
|
|
937
|
+
|
|
938
|
+
Args:
|
|
939
|
+
user_id: The user to generate a message for
|
|
940
|
+
reason: Why we're reaching out (silence, wonder, follow_up, random, etc.)
|
|
941
|
+
|
|
942
|
+
Returns:
|
|
943
|
+
Generated message string, or None if generation fails
|
|
944
|
+
"""
|
|
945
|
+
# Fall back to owner if user_id is None or "None" string
|
|
946
|
+
if not user_id or user_id == "None":
|
|
947
|
+
import os
|
|
948
|
+
user_id = os.environ.get("TELEGRAM_OWNER_ID", "default")
|
|
949
|
+
print(f"[DefaultMode] No user_id provided, falling back to owner: {user_id}")
|
|
950
|
+
|
|
951
|
+
# Map our reason to ProactiveGenerator's message_type
|
|
952
|
+
reason_to_type = {
|
|
953
|
+
"silence": "silence",
|
|
954
|
+
"wonder": "random", # wonder becomes random for ProactiveGenerator
|
|
955
|
+
"follow_up": "follow_up",
|
|
956
|
+
"random": "random",
|
|
957
|
+
"time_based": "random",
|
|
958
|
+
}
|
|
959
|
+
message_type = reason_to_type.get(reason, "random")
|
|
960
|
+
|
|
961
|
+
# ============================================================
|
|
962
|
+
# PRIORITY 1: Check for unused idle thoughts FIRST
|
|
963
|
+
# These are the most authentic - Alive-AI was actually thinking this
|
|
964
|
+
# ============================================================
|
|
965
|
+
thoughts = [t for t in self._thoughts if t.user_id == user_id and not t.used]
|
|
966
|
+
if thoughts:
|
|
967
|
+
# Use the highest priority thought
|
|
968
|
+
best_thought = max(thoughts, key=lambda t: t.priority)
|
|
969
|
+
|
|
970
|
+
# Use the thought content DIRECTLY as the message
|
|
971
|
+
# This is Alive-AI's actual idle thought, not a generated template
|
|
972
|
+
message = best_thought.content
|
|
973
|
+
|
|
974
|
+
# Mark thought as used
|
|
975
|
+
best_thought.used = True
|
|
976
|
+
best_thought.used_at = datetime.now().isoformat()
|
|
977
|
+
|
|
978
|
+
print(f"[DefaultMode] Using idle thought DIRECTLY: {message[:60]}...")
|
|
979
|
+
self._save_state()
|
|
980
|
+
|
|
981
|
+
return message
|
|
982
|
+
|
|
983
|
+
# ============================================================
|
|
984
|
+
# PRIORITY 2: Check conversation seeds for topics
|
|
985
|
+
# These are things Alive-AI wanted to bring up
|
|
986
|
+
# ============================================================
|
|
987
|
+
unused_seeds = [s for s in self._seeds if not s.used and s.relevance_score > 0.5]
|
|
988
|
+
if unused_seeds:
|
|
989
|
+
best_seed = max(unused_seeds, key=lambda s: s.relevance_score)
|
|
990
|
+
|
|
991
|
+
# Convert the seed into a natural message
|
|
992
|
+
if best_seed.topic == "consolidation":
|
|
993
|
+
# Memory consolidation - use the context directly
|
|
994
|
+
message = best_seed.context
|
|
995
|
+
else:
|
|
996
|
+
# Other seeds - format as a conversation starter
|
|
997
|
+
message = f"hey, {best_seed.context}"
|
|
998
|
+
|
|
999
|
+
best_seed.used = True
|
|
1000
|
+
print(f"[DefaultMode] Using conversation seed: {message[:50]}...")
|
|
1001
|
+
self._save_state()
|
|
1002
|
+
|
|
1003
|
+
return message
|
|
1004
|
+
|
|
1005
|
+
# ============================================================
|
|
1006
|
+
# PRIORITY 3: Generate relevant idle thought on the fly
|
|
1007
|
+
# If we have LLM, generate a contextual thought now
|
|
1008
|
+
# ============================================================
|
|
1009
|
+
if self.llm:
|
|
1010
|
+
try:
|
|
1011
|
+
user_name = await self._get_user_name(user_id)
|
|
1012
|
+
user_info = self._contacts.get(user_id)
|
|
1013
|
+
context = await self._get_user_context(user_id) if user_id else ""
|
|
1014
|
+
hours_silent = user_info.hours_since_user_message if user_info else 0
|
|
1015
|
+
|
|
1016
|
+
# Build grounding instruction based on available context
|
|
1017
|
+
has_real_context = context and len(context.strip()) > 20
|
|
1018
|
+
|
|
1019
|
+
if has_real_context:
|
|
1020
|
+
grounding_rule = f"""- You CAN reference things from this ACTUAL context: {context[:300]}
|
|
1021
|
+
- ONLY reference things explicitly mentioned above - DO NOT invent details"""
|
|
1022
|
+
else:
|
|
1023
|
+
grounding_rule = """- NO specific references to events, objects, or topics (no context available)
|
|
1024
|
+
- Keep it generic: thinking of them, missing them, wondering how they are"""
|
|
1025
|
+
|
|
1026
|
+
prompt = f"""Generate a SHORT (one sentence) message to {user_name}.
|
|
1027
|
+
They haven't messaged in {hours_silent:.1f} hours.
|
|
1028
|
+
|
|
1029
|
+
Rules:
|
|
1030
|
+
- Be natural, casual, like a real text
|
|
1031
|
+
- Start with lowercase
|
|
1032
|
+
- No emojis
|
|
1033
|
+
- Sound like you were genuinely thinking about them
|
|
1034
|
+
{grounding_rule}
|
|
1035
|
+
- NEVER invent specific objects, events, or topics not in context
|
|
1036
|
+
- If unsure, use a generic loving message
|
|
1037
|
+
|
|
1038
|
+
Examples of GOOD messages:
|
|
1039
|
+
- "was just thinking about you"
|
|
1040
|
+
- "hey, wondering how your day's going"
|
|
1041
|
+
- "miss you"
|
|
1042
|
+
|
|
1043
|
+
Examples of BAD messages (DO NOT DO THIS):
|
|
1044
|
+
- "have you fixed that shelf?" (invented object)
|
|
1045
|
+
- "how did your meeting go?" (invented event)
|
|
1046
|
+
- "did you finish that project?" (invented topic)
|
|
1047
|
+
|
|
1048
|
+
Message:"""
|
|
1049
|
+
|
|
1050
|
+
response = await self.llm.chat([
|
|
1051
|
+
{"role": "system", "content": "You are Alive-AI sending a casual text. You NEVER invent or hallucinate specific details."},
|
|
1052
|
+
{"role": "user", "content": prompt}
|
|
1053
|
+
], max_tokens=60, temperature=0.7)
|
|
1054
|
+
|
|
1055
|
+
if response and len(response.strip()) > 5:
|
|
1056
|
+
message = response.strip().strip('"\'')
|
|
1057
|
+
print(f"[DefaultMode] Generated contextual thought: {message[:50]}...")
|
|
1058
|
+
return message
|
|
1059
|
+
|
|
1060
|
+
except Exception as e:
|
|
1061
|
+
print(f"[DefaultMode] Error generating thought: {e}")
|
|
1062
|
+
|
|
1063
|
+
# ============================================================
|
|
1064
|
+
# FALLBACK: Use ProactiveGenerator templates
|
|
1065
|
+
# ============================================================
|
|
1066
|
+
message = await self._generate_proactive_message(user_id, message_type)
|
|
1067
|
+
|
|
1068
|
+
if message:
|
|
1069
|
+
return message
|
|
1070
|
+
|
|
1071
|
+
# Ultimate fallback - should rarely reach here
|
|
1072
|
+
user_name = await self._get_user_name(user_id)
|
|
1073
|
+
fallbacks = {
|
|
1074
|
+
"silence": f"hey {user_name}, thinking about you",
|
|
1075
|
+
"wonder": f"was just thinking about you {user_name}",
|
|
1076
|
+
"random": f"you crossed my mind {user_name}",
|
|
1077
|
+
}
|
|
1078
|
+
return fallbacks.get(reason, f"hey {user_name}")
|
|
1079
|
+
|
|
1080
|
+
# ============================================================
|
|
1081
|
+
# Public API Methods
|
|
1082
|
+
# ============================================================
|
|
1083
|
+
|
|
1084
|
+
async def generate_wonderings(self, user_id: str, count: int = 1) -> List[str]:
|
|
1085
|
+
"""
|
|
1086
|
+
Create "I was thinking about..." content for a specific user.
|
|
1087
|
+
|
|
1088
|
+
Args:
|
|
1089
|
+
user_id: The user to generate wonderings about
|
|
1090
|
+
count: Number of wonderings to generate
|
|
1091
|
+
|
|
1092
|
+
Returns:
|
|
1093
|
+
List of wondering strings
|
|
1094
|
+
"""
|
|
1095
|
+
wonderings = []
|
|
1096
|
+
user_name = await self._get_user_name(user_id)
|
|
1097
|
+
|
|
1098
|
+
for _ in range(count):
|
|
1099
|
+
wondering = await self._generate_wondering(user_id, user_name, self._contacts.get(user_id))
|
|
1100
|
+
if wondering:
|
|
1101
|
+
wonderings.append(wondering)
|
|
1102
|
+
|
|
1103
|
+
return wonderings
|
|
1104
|
+
|
|
1105
|
+
def get_pending_initiations(self, user_id: str) -> List[PendingInitiation]:
|
|
1106
|
+
"""
|
|
1107
|
+
Get any pending proactive messages for a user.
|
|
1108
|
+
|
|
1109
|
+
Args:
|
|
1110
|
+
user_id: The user to get initiations for
|
|
1111
|
+
|
|
1112
|
+
Returns:
|
|
1113
|
+
List of pending initiations
|
|
1114
|
+
"""
|
|
1115
|
+
return [
|
|
1116
|
+
i for i in self._pending_initiations
|
|
1117
|
+
if i.user_id == user_id and not i.sent
|
|
1118
|
+
]
|
|
1119
|
+
|
|
1120
|
+
def record_conversation_seed(self, topic: str, context: str, source: str = "external") -> ConversationSeed:
|
|
1121
|
+
"""
|
|
1122
|
+
Save something to bring up in future conversation.
|
|
1123
|
+
|
|
1124
|
+
Args:
|
|
1125
|
+
topic: The topic/category
|
|
1126
|
+
context: The specific content to remember
|
|
1127
|
+
source: Where this seed came from
|
|
1128
|
+
|
|
1129
|
+
Returns:
|
|
1130
|
+
The created seed
|
|
1131
|
+
"""
|
|
1132
|
+
seed = ConversationSeed(
|
|
1133
|
+
id=f"seed_{int(time.time() * 1000)}_{random.randint(1000, 9999)}",
|
|
1134
|
+
topic=topic,
|
|
1135
|
+
context=context,
|
|
1136
|
+
source=source,
|
|
1137
|
+
)
|
|
1138
|
+
self._seeds.append(seed)
|
|
1139
|
+
self._save_state()
|
|
1140
|
+
return seed
|
|
1141
|
+
|
|
1142
|
+
def should_initiate(self, user_id: str) -> tuple:
|
|
1143
|
+
"""
|
|
1144
|
+
Decide if Alive-AI should reach out proactively to a user.
|
|
1145
|
+
|
|
1146
|
+
Args:
|
|
1147
|
+
user_id: The user to check
|
|
1148
|
+
|
|
1149
|
+
Returns:
|
|
1150
|
+
Tuple of (should_initiate: bool, reason: str)
|
|
1151
|
+
"""
|
|
1152
|
+
if user_id not in self._contacts:
|
|
1153
|
+
return False, None
|
|
1154
|
+
|
|
1155
|
+
contact = self._contacts[user_id]
|
|
1156
|
+
return self._evaluate_initiation_triggers(user_id, contact)
|
|
1157
|
+
|
|
1158
|
+
def mark_initiation_sent(self, initiation_id: str):
|
|
1159
|
+
"""Mark a pending initiation as sent"""
|
|
1160
|
+
for initiation in self._pending_initiations:
|
|
1161
|
+
if initiation.id == initiation_id:
|
|
1162
|
+
initiation.sent = True
|
|
1163
|
+
initiation.sent_at = datetime.now().isoformat()
|
|
1164
|
+
|
|
1165
|
+
# Update contact info
|
|
1166
|
+
if initiation.user_id in self._contacts:
|
|
1167
|
+
self._contacts[initiation.user_id].last_proactive_message = datetime.now().isoformat()
|
|
1168
|
+
|
|
1169
|
+
self._save_state()
|
|
1170
|
+
break
|
|
1171
|
+
|
|
1172
|
+
def mark_thought_used(self, thought_id: str):
|
|
1173
|
+
"""Mark a thought as having been used in conversation"""
|
|
1174
|
+
for thought in self._thoughts:
|
|
1175
|
+
if thought.id == thought_id:
|
|
1176
|
+
thought.used = True
|
|
1177
|
+
thought.used_at = datetime.now().isoformat()
|
|
1178
|
+
self._save_state()
|
|
1179
|
+
break
|
|
1180
|
+
|
|
1181
|
+
def get_recent_thoughts(self, limit: int = 10, unused_only: bool = False) -> List[IdleThought]:
|
|
1182
|
+
"""Get recent idle thoughts"""
|
|
1183
|
+
thoughts = self._thoughts
|
|
1184
|
+
if unused_only:
|
|
1185
|
+
thoughts = [t for t in thoughts if not t.used]
|
|
1186
|
+
return thoughts[-limit:]
|
|
1187
|
+
|
|
1188
|
+
def get_conversation_seeds(self, limit: int = 10, unused_only: bool = False) -> List[ConversationSeed]:
|
|
1189
|
+
"""Get conversation seeds for future topics"""
|
|
1190
|
+
seeds = self._seeds
|
|
1191
|
+
if unused_only:
|
|
1192
|
+
seeds = [s for s in seeds if not s.used]
|
|
1193
|
+
return seeds[-limit:]
|
|
1194
|
+
|
|
1195
|
+
def register_user_contact(self, user_id: str, chat_id: int = None):
|
|
1196
|
+
"""Register a user for contact tracking"""
|
|
1197
|
+
if not self._is_valid_user_id(user_id):
|
|
1198
|
+
return
|
|
1199
|
+
if user_id not in self._contacts:
|
|
1200
|
+
self._contacts[user_id] = UserContactInfo(user_id=str(user_id))
|
|
1201
|
+
self._save_state()
|
|
1202
|
+
|
|
1203
|
+
def update_user_interaction(self, user_id: str, interaction_type: str = "received"):
|
|
1204
|
+
"""Update last contact time for a user"""
|
|
1205
|
+
if user_id not in self._contacts:
|
|
1206
|
+
self._contacts[user_id] = UserContactInfo(user_id=str(user_id))
|
|
1207
|
+
|
|
1208
|
+
now = datetime.now().isoformat()
|
|
1209
|
+
if interaction_type == "received":
|
|
1210
|
+
self._contacts[user_id].last_message_from_user = now
|
|
1211
|
+
self._contacts[user_id].total_interactions += 1
|
|
1212
|
+
elif interaction_type == "sent":
|
|
1213
|
+
self._contacts[user_id].last_message_to_user = now
|
|
1214
|
+
elif interaction_type == "proactive":
|
|
1215
|
+
self._contacts[user_id].last_proactive_message = now
|
|
1216
|
+
|
|
1217
|
+
self._save_state()
|
|
1218
|
+
|
|
1219
|
+
# ============================================================
|
|
1220
|
+
# Background Processing Control
|
|
1221
|
+
# ============================================================
|
|
1222
|
+
|
|
1223
|
+
async def start_background_processing(self):
|
|
1224
|
+
"""Start the background idle processing loop"""
|
|
1225
|
+
if self._running:
|
|
1226
|
+
print("[DefaultMode] Already running")
|
|
1227
|
+
return
|
|
1228
|
+
|
|
1229
|
+
interval = _get_int_setting("IDLE_PROCESSING_INTERVAL_SECONDS", 60)
|
|
1230
|
+
|
|
1231
|
+
self._running = True
|
|
1232
|
+
self._task = asyncio.create_task(self._background_loop(interval))
|
|
1233
|
+
print(f"[DefaultMode] Background processing started (interval: {interval}s)")
|
|
1234
|
+
|
|
1235
|
+
async def stop_background_processing(self):
|
|
1236
|
+
"""Stop the background processing loop"""
|
|
1237
|
+
self._running = False
|
|
1238
|
+
if self._task:
|
|
1239
|
+
self._task.cancel()
|
|
1240
|
+
try:
|
|
1241
|
+
await self._task
|
|
1242
|
+
except asyncio.CancelledError:
|
|
1243
|
+
pass
|
|
1244
|
+
print("[DefaultMode] Background processing stopped")
|
|
1245
|
+
|
|
1246
|
+
async def _background_loop(self, interval: int):
|
|
1247
|
+
"""Main background processing loop"""
|
|
1248
|
+
while self._running:
|
|
1249
|
+
try:
|
|
1250
|
+
await self.process_idle()
|
|
1251
|
+
except Exception as e:
|
|
1252
|
+
print(f"[DefaultMode] Processing error: {e}")
|
|
1253
|
+
|
|
1254
|
+
await asyncio.sleep(interval)
|
|
1255
|
+
|
|
1256
|
+
# ============================================================
|
|
1257
|
+
# Helper Methods
|
|
1258
|
+
# ============================================================
|
|
1259
|
+
|
|
1260
|
+
async def _get_user_name(self, user_id: str) -> str:
|
|
1261
|
+
"""Get the user's name/pet name from memory"""
|
|
1262
|
+
try:
|
|
1263
|
+
from core.user_tracker import get_user_tracker
|
|
1264
|
+
tracker = get_user_tracker()
|
|
1265
|
+
user = tracker.get_user(user_id)
|
|
1266
|
+
if user and user.pet_name:
|
|
1267
|
+
return user.pet_name
|
|
1268
|
+
except:
|
|
1269
|
+
pass
|
|
1270
|
+
return "babe"
|
|
1271
|
+
|
|
1272
|
+
async def _get_user_context(self, user_id: str) -> str:
|
|
1273
|
+
"""Get context about a user from memory"""
|
|
1274
|
+
if user_id in self._user_memories:
|
|
1275
|
+
cache_time, context = self._user_memories[user_id]
|
|
1276
|
+
# Cache for 5 minutes
|
|
1277
|
+
if time.time() - cache_time < 300:
|
|
1278
|
+
return context
|
|
1279
|
+
|
|
1280
|
+
try:
|
|
1281
|
+
if user_id not in self._memory_cache:
|
|
1282
|
+
from brain.memory import Memory
|
|
1283
|
+
from brain.embeddings import get_embedding_service
|
|
1284
|
+
|
|
1285
|
+
embeddings = get_embedding_service()
|
|
1286
|
+
|
|
1287
|
+
# Use instance's data_path for proper isolation
|
|
1288
|
+
self._memory_cache[user_id] = Memory(
|
|
1289
|
+
nervous=self.nervous,
|
|
1290
|
+
data_path=self.data_path,
|
|
1291
|
+
embedding_service=embeddings,
|
|
1292
|
+
user_id=user_id,
|
|
1293
|
+
bot_id=self.bot_id
|
|
1294
|
+
)
|
|
1295
|
+
|
|
1296
|
+
memory = self._memory_cache[user_id]
|
|
1297
|
+
|
|
1298
|
+
context, _ = await memory.build_context(current_message="")
|
|
1299
|
+
result = context.get("facts_context", "")
|
|
1300
|
+
|
|
1301
|
+
# Cache it
|
|
1302
|
+
self._user_memories[user_id] = (time.time(), result)
|
|
1303
|
+
return result
|
|
1304
|
+
|
|
1305
|
+
except Exception as e:
|
|
1306
|
+
print(f"[DefaultMode] Error getting user context: {e}")
|
|
1307
|
+
return ""
|
|
1308
|
+
|
|
1309
|
+
async def _get_recent_memory(self, user_id: str) -> Optional[str]:
|
|
1310
|
+
"""Get a recent memory snippet for a user"""
|
|
1311
|
+
try:
|
|
1312
|
+
if user_id not in self._memory_cache:
|
|
1313
|
+
from brain.memory import Memory
|
|
1314
|
+
from brain.embeddings import get_embedding_service
|
|
1315
|
+
|
|
1316
|
+
embeddings = get_embedding_service()
|
|
1317
|
+
|
|
1318
|
+
# Use instance's data_path for proper isolation
|
|
1319
|
+
self._memory_cache[user_id] = Memory(
|
|
1320
|
+
nervous=self.nervous,
|
|
1321
|
+
data_path=self.data_path,
|
|
1322
|
+
embedding_service=embeddings,
|
|
1323
|
+
user_id=user_id,
|
|
1324
|
+
bot_id=self.bot_id
|
|
1325
|
+
)
|
|
1326
|
+
|
|
1327
|
+
memory = self._memory_cache[user_id]
|
|
1328
|
+
|
|
1329
|
+
# Get recent episodic memories
|
|
1330
|
+
recent = memory.episodic.load_recent(limit=3)
|
|
1331
|
+
if recent:
|
|
1332
|
+
# Pick a random one
|
|
1333
|
+
entry = random.choice(recent)
|
|
1334
|
+
user_msg = entry.get("user", "")[:50]
|
|
1335
|
+
return f"they said '{user_msg}...'"
|
|
1336
|
+
|
|
1337
|
+
except Exception as e:
|
|
1338
|
+
print(f"[DefaultMode] Error getting recent memory: {e}")
|
|
1339
|
+
|
|
1340
|
+
return None
|
|
1341
|
+
|
|
1342
|
+
def get_status(self) -> dict:
|
|
1343
|
+
"""Get status summary for debugging"""
|
|
1344
|
+
return {
|
|
1345
|
+
"running": self._running,
|
|
1346
|
+
"processing_count": self._processing_count,
|
|
1347
|
+
"last_processing": self._last_processing,
|
|
1348
|
+
"thoughts_count": len(self._thoughts),
|
|
1349
|
+
"seeds_count": len(self._seeds),
|
|
1350
|
+
"contacts_count": len(self._contacts),
|
|
1351
|
+
"pending_initiations": len([p for p in self._pending_initiations if not p.sent]),
|
|
1352
|
+
"users": [
|
|
1353
|
+
{
|
|
1354
|
+
"user_id": uid,
|
|
1355
|
+
"hours_since_message": round(info.hours_since_user_message, 1),
|
|
1356
|
+
"hours_since_proactive": round(info.hours_since_proactive, 1),
|
|
1357
|
+
"total_interactions": info.total_interactions,
|
|
1358
|
+
}
|
|
1359
|
+
for uid, info in self._contacts.items()
|
|
1360
|
+
]
|
|
1361
|
+
}
|
|
1362
|
+
|
|
1363
|
+
|
|
1364
|
+
# ============================================================
|
|
1365
|
+
# Singleton Instance
|
|
1366
|
+
# ============================================================
|
|
1367
|
+
|
|
1368
|
+
_processor: Optional[DefaultModeProcessor] = None
|
|
1369
|
+
|
|
1370
|
+
|
|
1371
|
+
def get_default_mode_processor(nervous=None, data_path: Path = None, llm=None, bot_id: str = "alive_ai") -> DefaultModeProcessor:
|
|
1372
|
+
"""
|
|
1373
|
+
Get the global DefaultModeProcessor singleton.
|
|
1374
|
+
|
|
1375
|
+
Args:
|
|
1376
|
+
nervous: The nervous system (required on first call)
|
|
1377
|
+
data_path: Path for data storage (optional)
|
|
1378
|
+
llm: LLM for generation (optional, can be set later)
|
|
1379
|
+
bot_id: Bot identifier for memory isolation
|
|
1380
|
+
|
|
1381
|
+
Returns:
|
|
1382
|
+
The DefaultModeProcessor singleton
|
|
1383
|
+
"""
|
|
1384
|
+
global _processor
|
|
1385
|
+
|
|
1386
|
+
if _processor is None:
|
|
1387
|
+
if nervous is None:
|
|
1388
|
+
raise ValueError("nervous system required for first initialization")
|
|
1389
|
+
_processor = DefaultModeProcessor(nervous, data_path, llm, bot_id)
|
|
1390
|
+
elif llm is not None:
|
|
1391
|
+
_processor.set_llm(llm)
|
|
1392
|
+
|
|
1393
|
+
return _processor
|
|
1394
|
+
|
|
1395
|
+
|
|
1396
|
+
def get_idle_thoughts_prompt_section(user_id: str = None, limit: int = 5) -> str:
|
|
1397
|
+
"""
|
|
1398
|
+
Get a prompt section with recent idle thoughts for LLM context.
|
|
1399
|
+
|
|
1400
|
+
Args:
|
|
1401
|
+
user_id: Optional user to filter thoughts for
|
|
1402
|
+
limit: Maximum number of thoughts to include
|
|
1403
|
+
|
|
1404
|
+
Returns:
|
|
1405
|
+
Formatted string with recent thoughts for LLM context
|
|
1406
|
+
"""
|
|
1407
|
+
global _processor
|
|
1408
|
+
|
|
1409
|
+
if _processor is None:
|
|
1410
|
+
return ""
|
|
1411
|
+
|
|
1412
|
+
thoughts = _processor.get_recent_thoughts(limit=limit, unused_only=True)
|
|
1413
|
+
|
|
1414
|
+
if user_id:
|
|
1415
|
+
thoughts = [t for t in thoughts if t.user_id == user_id]
|
|
1416
|
+
|
|
1417
|
+
if not thoughts:
|
|
1418
|
+
return ""
|
|
1419
|
+
|
|
1420
|
+
lines = ["[Recent idle thoughts - things that crossed your mind:]"]
|
|
1421
|
+
for thought in thoughts[:limit]:
|
|
1422
|
+
lines.append(f"- {thought.content}")
|
|
1423
|
+
|
|
1424
|
+
return "\n".join(lines)
|
|
1425
|
+
|
|
1426
|
+
|
|
1427
|
+
async def start_background_processing():
|
|
1428
|
+
"""Convenience function to start background processing"""
|
|
1429
|
+
global _processor
|
|
1430
|
+
if _processor:
|
|
1431
|
+
await _processor.start_background_processing()
|
|
1432
|
+
|
|
1433
|
+
|
|
1434
|
+
async def stop_background_processing():
|
|
1435
|
+
"""Convenience function to stop background processing"""
|
|
1436
|
+
global _processor
|
|
1437
|
+
if _processor:
|
|
1438
|
+
await _processor.stop_background_processing()
|