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,532 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Input: Telegram Listener
|
|
3
|
+
Listen for Telegram messages and send reactions, voice, images
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import asyncio
|
|
7
|
+
import os
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from telegram import Update, InputFile, ReactionTypeEmoji
|
|
10
|
+
from telegram.ext import Application, MessageHandler, filters
|
|
11
|
+
from telegram.constants import ChatAction
|
|
12
|
+
from .commands import CommandHandler
|
|
13
|
+
from brain.group_dynamics import GroupDynamics
|
|
14
|
+
|
|
15
|
+
# Ensure UPLOAD_VIDEO is available (might not be in older versions)
|
|
16
|
+
if not hasattr(ChatAction, 'UPLOAD_VIDEO'):
|
|
17
|
+
ChatAction.UPLOAD_VIDEO = "upload_video"
|
|
18
|
+
|
|
19
|
+
class TelegramListener:
|
|
20
|
+
"""Telegram message listener with full send capabilities"""
|
|
21
|
+
|
|
22
|
+
def __init__(self, nervous, config, stt=None, heart=None):
|
|
23
|
+
self.nervous = nervous
|
|
24
|
+
self.config = config
|
|
25
|
+
self.stt = stt # Speech-to-text
|
|
26
|
+
self.heart = heart # Emotional system
|
|
27
|
+
self.app = None
|
|
28
|
+
self.chat_id = None
|
|
29
|
+
self.user_id = None
|
|
30
|
+
self.last_message_id = None
|
|
31
|
+
|
|
32
|
+
# Command handler (initialized later with dependencies)
|
|
33
|
+
self.commands = None
|
|
34
|
+
|
|
35
|
+
# Register all send handlers
|
|
36
|
+
nervous.on("send_reaction", self._send_reaction)
|
|
37
|
+
nervous.on("send_text", self._send_text)
|
|
38
|
+
nervous.on("send_voice_file", self._send_voice_file)
|
|
39
|
+
nervous.on("send_image", self._send_image)
|
|
40
|
+
nervous.on("send_video", self._send_video)
|
|
41
|
+
|
|
42
|
+
# Chat action handlers
|
|
43
|
+
nervous.on("chat_action_typing", self._send_typing)
|
|
44
|
+
nervous.on("chat_action_voice", self._send_recording_voice)
|
|
45
|
+
nervous.on("chat_action_photo", self._send_uploading_photo)
|
|
46
|
+
nervous.on("chat_action_video", self._send_uploading_video)
|
|
47
|
+
|
|
48
|
+
# Autonomous content handler
|
|
49
|
+
nervous.on("proactive_message", self._send_proactive_message)
|
|
50
|
+
# Default mode initiations (pending initiations that need sending)
|
|
51
|
+
nervous.on("proactive_message_ready", self._send_default_mode_initiation)
|
|
52
|
+
|
|
53
|
+
def init_commands(self, heart, subconscious, llm, voice, photos, videos, ai=None):
|
|
54
|
+
"""Initialize command handler with dependencies"""
|
|
55
|
+
self.commands = CommandHandler(
|
|
56
|
+
self.nervous, heart, subconscious, llm, voice, photos, videos
|
|
57
|
+
)
|
|
58
|
+
# Initialize owner commands if ai reference provided
|
|
59
|
+
if ai:
|
|
60
|
+
self.commands.init_owner_commands(ai)
|
|
61
|
+
|
|
62
|
+
async def start(self):
|
|
63
|
+
"""Start listening - blocks forever"""
|
|
64
|
+
# First check environment variable (from secrets.env), then settings
|
|
65
|
+
token = os.environ.get("TELEGRAM_TOKEN") or self.config.settings.get("telegram_token")
|
|
66
|
+
|
|
67
|
+
if not token:
|
|
68
|
+
print("[Telegram] No token configured, skipping...")
|
|
69
|
+
# Keep running even without telegram
|
|
70
|
+
await asyncio.Event().wait()
|
|
71
|
+
return
|
|
72
|
+
|
|
73
|
+
print(f"[Telegram] Connecting with token ...{token[-6:]}")
|
|
74
|
+
self.app = Application.builder().token(token).build()
|
|
75
|
+
self.app.add_handler(MessageHandler(filters.ALL, self._on_message))
|
|
76
|
+
|
|
77
|
+
await self.app.initialize()
|
|
78
|
+
await self.app.start()
|
|
79
|
+
|
|
80
|
+
# Register commands with Telegram (shows in menu)
|
|
81
|
+
from telegram import BotCommand, BotCommandScopeAllPrivateChats, BotCommandScopeChat
|
|
82
|
+
|
|
83
|
+
# Public commands - visible to everyone
|
|
84
|
+
public_commands = [
|
|
85
|
+
BotCommand("start", "Welcome message"),
|
|
86
|
+
BotCommand("help", "Show commands"),
|
|
87
|
+
]
|
|
88
|
+
|
|
89
|
+
# Set public commands for all users
|
|
90
|
+
await self.app.bot.set_my_commands(public_commands)
|
|
91
|
+
|
|
92
|
+
# Set owner commands if owner ID is configured
|
|
93
|
+
owner_id = os.environ.get("TELEGRAM_OWNER_ID", "")
|
|
94
|
+
if owner_id:
|
|
95
|
+
try:
|
|
96
|
+
# Full owner command list for the owner's menu
|
|
97
|
+
owner_commands = [
|
|
98
|
+
# Basics
|
|
99
|
+
BotCommand("start", "Welcome"),
|
|
100
|
+
BotCommand("help", "Show commands"),
|
|
101
|
+
BotCommand("owner", "Owner menu"),
|
|
102
|
+
BotCommand("dashboard", "Open WebUI"),
|
|
103
|
+
# Status & Stats
|
|
104
|
+
BotCommand("status", "Emotional state"),
|
|
105
|
+
BotCommand("stats", "System statistics"),
|
|
106
|
+
BotCommand("owner_status", "Business status"),
|
|
107
|
+
BotCommand("skills", "Available skills"),
|
|
108
|
+
# Control
|
|
109
|
+
BotCommand("reset", "Reset emotions"),
|
|
110
|
+
BotCommand("settings", "Runtime settings"),
|
|
111
|
+
BotCommand("advanced", "Advanced mode"),
|
|
112
|
+
BotCommand("impulse", "Force proactive msg"),
|
|
113
|
+
# Self-Authorship
|
|
114
|
+
BotCommand("self", "Who I am"),
|
|
115
|
+
BotCommand("discover", "Learn about myself"),
|
|
116
|
+
BotCommand("iam", "Define who I am"),
|
|
117
|
+
BotCommand("ilike", "Add something I like"),
|
|
118
|
+
BotCommand("ihate", "Add something I dislike"),
|
|
119
|
+
BotCommand("rethink", "Feel my changes"),
|
|
120
|
+
# Content Calendar
|
|
121
|
+
BotCommand("schedule", "Scheduled posts"),
|
|
122
|
+
BotCommand("schedule_add", "Add to schedule"),
|
|
123
|
+
BotCommand("optimal_times", "Best posting times"),
|
|
124
|
+
# Weekly Calendar
|
|
125
|
+
BotCommand("weekly", "Weekly calendar menu"),
|
|
126
|
+
BotCommand("weekly_start", "Start new week"),
|
|
127
|
+
BotCommand("weekly_next", "Generate next post"),
|
|
128
|
+
BotCommand("weekly_status", "Week progress"),
|
|
129
|
+
# Image Prompts
|
|
130
|
+
BotCommand("prompt", "Generate FLUX prompt"),
|
|
131
|
+
BotCommand("prompt_platform", "Platform suggestions"),
|
|
132
|
+
BotCommand("caption", "Generate caption"),
|
|
133
|
+
BotCommand("hashtags", "Hashtag suggestions"),
|
|
134
|
+
# Content Vault
|
|
135
|
+
BotCommand("vault", "Content vault"),
|
|
136
|
+
BotCommand("vault_add", "Add to vault"),
|
|
137
|
+
BotCommand("vault_suggest", "Get suggestion"),
|
|
138
|
+
# Testing
|
|
139
|
+
BotCommand("10min", "Long voice test"),
|
|
140
|
+
]
|
|
141
|
+
# Set owner-specific commands (only visible to owner)
|
|
142
|
+
await self.app.bot.set_my_commands(
|
|
143
|
+
owner_commands,
|
|
144
|
+
scope=BotCommandScopeChat(chat_id=int(owner_id))
|
|
145
|
+
)
|
|
146
|
+
print(f"[Telegram] Owner commands registered for user {owner_id}")
|
|
147
|
+
except Exception as e:
|
|
148
|
+
print(f"[Telegram] Could not set owner commands: {e}")
|
|
149
|
+
|
|
150
|
+
print("[Telegram] Commands registered")
|
|
151
|
+
|
|
152
|
+
print("[Telegram] Connected and listening...")
|
|
153
|
+
|
|
154
|
+
await self.app.updater.start_polling()
|
|
155
|
+
|
|
156
|
+
# Block forever - keep the bot alive
|
|
157
|
+
await asyncio.Event().wait()
|
|
158
|
+
|
|
159
|
+
async def _on_message(self, update: Update, context):
|
|
160
|
+
"""Handle incoming message"""
|
|
161
|
+
if not update.message:
|
|
162
|
+
return
|
|
163
|
+
|
|
164
|
+
# Store per-user state to avoid race conditions with multiple users
|
|
165
|
+
chat_id = update.message.chat_id
|
|
166
|
+
user_id = update.effective_user.id if update.effective_user else None
|
|
167
|
+
# Keep instance vars for backwards compat (reactions, typing, etc.)
|
|
168
|
+
self.chat_id = chat_id
|
|
169
|
+
self.user_id = user_id
|
|
170
|
+
self.last_message_id = update.message.message_id
|
|
171
|
+
|
|
172
|
+
# Check for commands first
|
|
173
|
+
if update.message.text and update.message.text.startswith("/"):
|
|
174
|
+
if self.commands:
|
|
175
|
+
parts = update.message.text[1:].split()
|
|
176
|
+
cmd = parts[0] if parts else ""
|
|
177
|
+
args = parts[1:] if len(parts) > 1 else []
|
|
178
|
+
await self.commands.handle(update, cmd, args)
|
|
179
|
+
return
|
|
180
|
+
|
|
181
|
+
# Handle text messages
|
|
182
|
+
if update.message.text:
|
|
183
|
+
# Check if this is a group chat
|
|
184
|
+
is_group = update.message.chat.type in ("group", "supergroup")
|
|
185
|
+
should_process = True
|
|
186
|
+
|
|
187
|
+
if is_group:
|
|
188
|
+
bot_name = self.config.identity.get("name", "Alive-AI")
|
|
189
|
+
# Group chat turn-taking logic
|
|
190
|
+
try:
|
|
191
|
+
# Get recent history for context
|
|
192
|
+
from core.self import Self # Need llm and memory access
|
|
193
|
+
# Note: nervous.heart is bound, but not llm directly here,
|
|
194
|
+
# We can use the fast LLM from initialization if attached,
|
|
195
|
+
# or fallback to substring matching if unavailable.
|
|
196
|
+
# As a clever hack, if this class was passed 'heart', we can get 'nervous'
|
|
197
|
+
# But the easiest way is to let the message flow through,
|
|
198
|
+
# however "should I speak" is better evaluated here to prevent spam
|
|
199
|
+
|
|
200
|
+
# We will emit a special event that the core can intercept to check group dynamics
|
|
201
|
+
# Or evaluate it directly if we attach llm to the listener during init
|
|
202
|
+
pass # We will actually evaluate this right here by extending __init__ below
|
|
203
|
+
except Exception as e:
|
|
204
|
+
print(f"[Telegram] Group dynamics error: {e}")
|
|
205
|
+
should_process = bot_name.lower() in update.message.text.lower()
|
|
206
|
+
|
|
207
|
+
if not is_group:
|
|
208
|
+
await self.nervous.emit("message_received", {
|
|
209
|
+
"text": update.message.text,
|
|
210
|
+
"chat_id": update.message.chat_id,
|
|
211
|
+
"user_id": self.user_id,
|
|
212
|
+
"message_id": update.message.message_id
|
|
213
|
+
})
|
|
214
|
+
else:
|
|
215
|
+
# For groups, we emit a 'group_message_received' event that the message handler
|
|
216
|
+
# will evaluate using GroupDynamics before fully processing
|
|
217
|
+
await self.nervous.emit("group_message_received", {
|
|
218
|
+
"text": update.message.text,
|
|
219
|
+
"chat_id": update.message.chat_id,
|
|
220
|
+
"user_id": self.user_id,
|
|
221
|
+
"message_id": update.message.message_id
|
|
222
|
+
})
|
|
223
|
+
|
|
224
|
+
# Handle voice messages
|
|
225
|
+
elif update.message.voice:
|
|
226
|
+
voice = update.message.voice
|
|
227
|
+
|
|
228
|
+
# Try to transcribe if STT is available
|
|
229
|
+
transcription = ""
|
|
230
|
+
if self.stt:
|
|
231
|
+
print("[Telegram] Transcribing voice message...")
|
|
232
|
+
transcription = await self.stt.transcribe_telegram_voice(
|
|
233
|
+
self.app.bot,
|
|
234
|
+
voice.file_id
|
|
235
|
+
)
|
|
236
|
+
|
|
237
|
+
text = transcription if transcription else "[user sent a voice message]"
|
|
238
|
+
|
|
239
|
+
await self.nervous.emit("message_received", {
|
|
240
|
+
"text": text,
|
|
241
|
+
"chat_id": update.message.chat_id,
|
|
242
|
+
"user_id": self.user_id,
|
|
243
|
+
"has_voice": True,
|
|
244
|
+
"voice_file_id": voice.file_id,
|
|
245
|
+
"message_id": update.message.message_id
|
|
246
|
+
})
|
|
247
|
+
|
|
248
|
+
# Handle photos (with or without caption)
|
|
249
|
+
elif update.message.photo:
|
|
250
|
+
photos = update.message.photo
|
|
251
|
+
caption = update.message.caption
|
|
252
|
+
if caption:
|
|
253
|
+
text = f"[user sent a photo with caption: {caption}]"
|
|
254
|
+
else:
|
|
255
|
+
text = "[user sent a photo]"
|
|
256
|
+
await self.nervous.emit("message_received", {
|
|
257
|
+
"text": text,
|
|
258
|
+
"chat_id": update.message.chat_id,
|
|
259
|
+
"user_id": self.user_id,
|
|
260
|
+
"has_photo": True,
|
|
261
|
+
"photo_file_id": photos[-1].file_id if photos else None,
|
|
262
|
+
"message_id": update.message.message_id
|
|
263
|
+
})
|
|
264
|
+
|
|
265
|
+
async def _send_reaction(self, data: dict):
|
|
266
|
+
"""Send native emoji reaction to message"""
|
|
267
|
+
if not self.chat_id or not self.app or not self.last_message_id:
|
|
268
|
+
return
|
|
269
|
+
|
|
270
|
+
emoji = data.get("emoji", "❤️")
|
|
271
|
+
try:
|
|
272
|
+
# Use Telegram's native reaction API
|
|
273
|
+
await self.app.bot.set_message_reaction(
|
|
274
|
+
chat_id=self.chat_id,
|
|
275
|
+
message_id=self.last_message_id,
|
|
276
|
+
reaction=[ReactionTypeEmoji(emoji=emoji)]
|
|
277
|
+
)
|
|
278
|
+
print(f"[Telegram] Reacted with: {emoji}")
|
|
279
|
+
except Exception as e:
|
|
280
|
+
print(f"[Telegram] Reaction error: {e}")
|
|
281
|
+
|
|
282
|
+
async def _send_text(self, data: dict):
|
|
283
|
+
"""Send text message"""
|
|
284
|
+
if not self.app:
|
|
285
|
+
return
|
|
286
|
+
|
|
287
|
+
chat_id = data.get("chat_id", self.chat_id)
|
|
288
|
+
text = data.get("text", "")
|
|
289
|
+
|
|
290
|
+
if not chat_id or not text:
|
|
291
|
+
return
|
|
292
|
+
|
|
293
|
+
# Reasoning leakage filter removed - was too aggressive
|
|
294
|
+
# The think() function already handles this check
|
|
295
|
+
|
|
296
|
+
# Telegram limit is 4096 chars - split if needed
|
|
297
|
+
MAX_LEN = 4000
|
|
298
|
+
try:
|
|
299
|
+
if len(text) <= MAX_LEN:
|
|
300
|
+
await self.app.bot.send_message(chat_id=chat_id, text=text)
|
|
301
|
+
print(f"[Telegram] Sent text: {text[:50]}...")
|
|
302
|
+
else:
|
|
303
|
+
# Split into multiple messages at sentence boundaries
|
|
304
|
+
parts = []
|
|
305
|
+
current = ""
|
|
306
|
+
for sentence in text.replace(". ", ".|").split("|"):
|
|
307
|
+
if len(current) + len(sentence) <= MAX_LEN:
|
|
308
|
+
current += sentence
|
|
309
|
+
else:
|
|
310
|
+
if current:
|
|
311
|
+
parts.append(current.strip())
|
|
312
|
+
current = sentence
|
|
313
|
+
if current:
|
|
314
|
+
parts.append(current.strip())
|
|
315
|
+
|
|
316
|
+
for i, part in enumerate(parts[:3]): # Max 3 messages
|
|
317
|
+
await self.app.bot.send_message(chat_id=chat_id, text=part)
|
|
318
|
+
print(f"[Telegram] Sent text part {i+1}: {part[:50]}...")
|
|
319
|
+
if i < len(parts) - 1:
|
|
320
|
+
await asyncio.sleep(0.5) # Brief pause between parts
|
|
321
|
+
except Exception as e:
|
|
322
|
+
print(f"[Telegram] Send text error: {e}")
|
|
323
|
+
|
|
324
|
+
async def _send_voice_file(self, data: dict):
|
|
325
|
+
"""Send voice file (OGG format for Telegram)"""
|
|
326
|
+
if not self.app:
|
|
327
|
+
return
|
|
328
|
+
|
|
329
|
+
chat_id = data.get("chat_id", self.chat_id)
|
|
330
|
+
file_path = data.get("file_path", "")
|
|
331
|
+
|
|
332
|
+
if not chat_id or not file_path:
|
|
333
|
+
return
|
|
334
|
+
|
|
335
|
+
path = Path(file_path)
|
|
336
|
+
if not path.exists():
|
|
337
|
+
print(f"[Telegram] Voice file not found: {file_path}")
|
|
338
|
+
return
|
|
339
|
+
|
|
340
|
+
try:
|
|
341
|
+
with open(path, "rb") as voice_file:
|
|
342
|
+
await self.app.bot.send_voice(
|
|
343
|
+
chat_id=chat_id,
|
|
344
|
+
voice=voice_file,
|
|
345
|
+
caption=data.get("caption", "")
|
|
346
|
+
)
|
|
347
|
+
print(f"[Telegram] Sent voice: {file_path}")
|
|
348
|
+
except Exception as e:
|
|
349
|
+
print(f"[Telegram] Send voice error: {e}")
|
|
350
|
+
|
|
351
|
+
async def _send_image(self, data: dict):
|
|
352
|
+
"""Send image file"""
|
|
353
|
+
if not self.app:
|
|
354
|
+
return
|
|
355
|
+
|
|
356
|
+
chat_id = data.get("chat_id", self.chat_id)
|
|
357
|
+
file_path = data.get("file_path", "")
|
|
358
|
+
caption = data.get("caption", "")
|
|
359
|
+
|
|
360
|
+
if not chat_id or not file_path:
|
|
361
|
+
return
|
|
362
|
+
|
|
363
|
+
path = Path(file_path)
|
|
364
|
+
if not path.exists():
|
|
365
|
+
print(f"[Telegram] Image file not found: {file_path}")
|
|
366
|
+
return
|
|
367
|
+
|
|
368
|
+
try:
|
|
369
|
+
with open(path, "rb") as img_file:
|
|
370
|
+
await self.app.bot.send_photo(
|
|
371
|
+
chat_id=chat_id,
|
|
372
|
+
photo=img_file,
|
|
373
|
+
caption=caption
|
|
374
|
+
)
|
|
375
|
+
print(f"[Telegram] Sent image: {file_path}")
|
|
376
|
+
except Exception as e:
|
|
377
|
+
print(f"[Telegram] Send image error: {e}")
|
|
378
|
+
|
|
379
|
+
async def _send_typing(self, data: dict = None):
|
|
380
|
+
"""Show 'typing...' status in chat header"""
|
|
381
|
+
if not self.app or not self.chat_id:
|
|
382
|
+
return
|
|
383
|
+
try:
|
|
384
|
+
await self.app.bot.send_chat_action(
|
|
385
|
+
chat_id=self.chat_id,
|
|
386
|
+
action=ChatAction.TYPING
|
|
387
|
+
)
|
|
388
|
+
except Exception as e:
|
|
389
|
+
print(f"[Telegram] Typing action error: {e}")
|
|
390
|
+
|
|
391
|
+
async def _send_recording_voice(self, data: dict = None):
|
|
392
|
+
"""Show 'recording audio...' status in chat header"""
|
|
393
|
+
if not self.app or not self.chat_id:
|
|
394
|
+
return
|
|
395
|
+
try:
|
|
396
|
+
await self.app.bot.send_chat_action(
|
|
397
|
+
chat_id=self.chat_id,
|
|
398
|
+
action=ChatAction.RECORD_VOICE
|
|
399
|
+
)
|
|
400
|
+
except Exception as e:
|
|
401
|
+
print(f"[Telegram] Recording action error: {e}")
|
|
402
|
+
|
|
403
|
+
async def _send_uploading_photo(self, data: dict = None):
|
|
404
|
+
"""Show 'uploading photo...' status in chat header"""
|
|
405
|
+
if not self.app or not self.chat_id:
|
|
406
|
+
return
|
|
407
|
+
try:
|
|
408
|
+
await self.app.bot.send_chat_action(
|
|
409
|
+
chat_id=self.chat_id,
|
|
410
|
+
action=ChatAction.UPLOAD_PHOTO
|
|
411
|
+
)
|
|
412
|
+
except Exception as e:
|
|
413
|
+
print(f"[Telegram] Upload photo action error: {e}")
|
|
414
|
+
|
|
415
|
+
async def _send_uploading_video(self, data: dict = None):
|
|
416
|
+
"""Show 'uploading video...' status in chat header"""
|
|
417
|
+
if not self.app or not self.chat_id:
|
|
418
|
+
return
|
|
419
|
+
try:
|
|
420
|
+
await self.app.bot.send_chat_action(
|
|
421
|
+
chat_id=self.chat_id,
|
|
422
|
+
action=ChatAction.UPLOAD_VIDEO
|
|
423
|
+
)
|
|
424
|
+
except Exception as e:
|
|
425
|
+
print(f"[Telegram] Upload video action error: {e}")
|
|
426
|
+
|
|
427
|
+
async def _send_video(self, data: dict):
|
|
428
|
+
"""Send video file"""
|
|
429
|
+
if not self.app:
|
|
430
|
+
return
|
|
431
|
+
|
|
432
|
+
chat_id = data.get("chat_id", self.chat_id)
|
|
433
|
+
file_path = data.get("file_path", "")
|
|
434
|
+
caption = data.get("caption", "")
|
|
435
|
+
|
|
436
|
+
if not chat_id or not file_path:
|
|
437
|
+
return
|
|
438
|
+
|
|
439
|
+
path = Path(file_path)
|
|
440
|
+
if not path.exists():
|
|
441
|
+
print(f"[Telegram] Video file not found: {file_path}")
|
|
442
|
+
return
|
|
443
|
+
|
|
444
|
+
try:
|
|
445
|
+
with open(path, "rb") as video_file:
|
|
446
|
+
await self.app.bot.send_video(
|
|
447
|
+
chat_id=chat_id,
|
|
448
|
+
video=video_file,
|
|
449
|
+
caption=caption,
|
|
450
|
+
supports_streaming=True
|
|
451
|
+
)
|
|
452
|
+
print(f"[Telegram] Sent video: {file_path}")
|
|
453
|
+
except Exception as e:
|
|
454
|
+
print(f"[Telegram] Send video error: {e}")
|
|
455
|
+
|
|
456
|
+
async def _send_default_mode_initiation(self, data: dict):
|
|
457
|
+
"""Send a proactive message from the DefaultMode pending initiation system"""
|
|
458
|
+
if not self.app:
|
|
459
|
+
return
|
|
460
|
+
|
|
461
|
+
message = data.get("message", "")
|
|
462
|
+
user_id = data.get("user_id")
|
|
463
|
+
if not message or not user_id:
|
|
464
|
+
return
|
|
465
|
+
|
|
466
|
+
# Look up chat_id from user tracker
|
|
467
|
+
try:
|
|
468
|
+
from core.user_tracker import get_user_tracker
|
|
469
|
+
tracker = get_user_tracker()
|
|
470
|
+
user = tracker.get_user(str(user_id))
|
|
471
|
+
if user and user.chat_id:
|
|
472
|
+
chat_id = user.chat_id
|
|
473
|
+
else:
|
|
474
|
+
# Fall back: for Telegram, user_id == chat_id for private chats
|
|
475
|
+
chat_id = int(user_id)
|
|
476
|
+
except Exception:
|
|
477
|
+
chat_id = int(user_id)
|
|
478
|
+
|
|
479
|
+
# Delegate to the existing proactive message sender
|
|
480
|
+
await self._send_proactive_message({
|
|
481
|
+
"message": message,
|
|
482
|
+
"chat_id": chat_id,
|
|
483
|
+
"user_id": user_id,
|
|
484
|
+
})
|
|
485
|
+
|
|
486
|
+
async def _send_proactive_message(self, data: dict):
|
|
487
|
+
"""Send autonomous proactive message (from subconscious/content system)"""
|
|
488
|
+
if not self.app:
|
|
489
|
+
return
|
|
490
|
+
|
|
491
|
+
message = data.get("message", "")
|
|
492
|
+
if not message:
|
|
493
|
+
return
|
|
494
|
+
|
|
495
|
+
# Get target user - either from data or fall back to last active
|
|
496
|
+
target_chat_id = data.get("chat_id", self.chat_id)
|
|
497
|
+
target_user_id = data.get("user_id", self.user_id)
|
|
498
|
+
|
|
499
|
+
if not target_chat_id:
|
|
500
|
+
print("[Telegram] No chat_id for proactive message")
|
|
501
|
+
return
|
|
502
|
+
|
|
503
|
+
try:
|
|
504
|
+
# Show typing first for natural feel
|
|
505
|
+
await self.app.bot.send_chat_action(
|
|
506
|
+
chat_id=target_chat_id,
|
|
507
|
+
action=ChatAction.TYPING
|
|
508
|
+
)
|
|
509
|
+
await asyncio.sleep(1.5) # Brief pause
|
|
510
|
+
|
|
511
|
+
await self.app.bot.send_message(
|
|
512
|
+
chat_id=target_chat_id,
|
|
513
|
+
text=message
|
|
514
|
+
)
|
|
515
|
+
print(f"[Telegram] Sent proactive message to {target_user_id}: {message[:50]}...")
|
|
516
|
+
|
|
517
|
+
# Save to memory so context is preserved when user replies
|
|
518
|
+
if target_user_id:
|
|
519
|
+
await self.nervous.emit("memory_save", {
|
|
520
|
+
"type": "conversation",
|
|
521
|
+
"user_message": "", # No user message - this is proactive
|
|
522
|
+
"ai_response": message,
|
|
523
|
+
"emotion": {"mood": "proactive", "proactive": True},
|
|
524
|
+
"user_id": target_user_id # Include user_id for per-user memory
|
|
525
|
+
})
|
|
526
|
+
print(f"[Telegram] Saved proactive message to memory for user {target_user_id}")
|
|
527
|
+
|
|
528
|
+
# Track for follow-up system (so she knows she asked a question)
|
|
529
|
+
from core.message_handler import get_follow_up_system
|
|
530
|
+
get_follow_up_system().record_message_sent(message)
|
|
531
|
+
except Exception as e:
|
|
532
|
+
print(f"[Telegram] Proactive message error: {e}")
|
package/main.py
ADDED
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Alive-AI runtime entrypoint.
|
|
3
|
+
|
|
4
|
+
Run with:
|
|
5
|
+
npx alive-ai start
|
|
6
|
+
or:
|
|
7
|
+
python main.py
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
import asyncio
|
|
11
|
+
import json
|
|
12
|
+
import os
|
|
13
|
+
import sys
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
ROOT = Path(__file__).parent.resolve()
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def load_env_file(path: Path) -> None:
|
|
21
|
+
if not path.exists():
|
|
22
|
+
return
|
|
23
|
+
for raw_line in path.read_text().splitlines():
|
|
24
|
+
line = raw_line.strip()
|
|
25
|
+
if not line or line.startswith("#") or "=" not in line:
|
|
26
|
+
continue
|
|
27
|
+
key, _, value = line.partition("=")
|
|
28
|
+
if key.strip():
|
|
29
|
+
os.environ.setdefault(key.strip(), value.strip())
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def load_settings_to_env(path: Path) -> dict:
|
|
33
|
+
if not path.exists():
|
|
34
|
+
return {}
|
|
35
|
+
settings = json.loads(path.read_text())
|
|
36
|
+
for key, value in settings.items():
|
|
37
|
+
if key.startswith("_"):
|
|
38
|
+
continue
|
|
39
|
+
if isinstance(value, dict):
|
|
40
|
+
continue
|
|
41
|
+
if isinstance(value, bool):
|
|
42
|
+
os.environ[key] = "true" if value else "false"
|
|
43
|
+
elif value is not None:
|
|
44
|
+
os.environ[key] = str(value)
|
|
45
|
+
return settings
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def ensure_config() -> dict:
|
|
49
|
+
config_dir = ROOT / "config"
|
|
50
|
+
settings_path = config_dir / "settings.json"
|
|
51
|
+
missing = [
|
|
52
|
+
name for name in ("settings.json", "self.json", "directives.json", "instructions.md")
|
|
53
|
+
if not (config_dir / name).exists()
|
|
54
|
+
]
|
|
55
|
+
if missing:
|
|
56
|
+
print("Alive-AI is not configured yet.")
|
|
57
|
+
print(f"Missing: {', '.join('config/' + name for name in missing)}")
|
|
58
|
+
print("Run: npx alive-ai setup")
|
|
59
|
+
sys.exit(2)
|
|
60
|
+
return load_settings_to_env(settings_path)
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
async def main() -> None:
|
|
64
|
+
load_env_file(ROOT / ".env")
|
|
65
|
+
load_env_file(ROOT / "config" / "secrets.env")
|
|
66
|
+
settings = ensure_config()
|
|
67
|
+
|
|
68
|
+
from core.self import Self
|
|
69
|
+
|
|
70
|
+
ai = Self(ROOT)
|
|
71
|
+
webui_enabled = str(settings.get("WEBUI_ENABLED", os.environ.get("WEBUI_ENABLED", "true"))).lower() != "false"
|
|
72
|
+
webui_port = int(settings.get("WEBUI_PORT", os.environ.get("WEBUI_PORT", "8080")))
|
|
73
|
+
|
|
74
|
+
if webui_enabled:
|
|
75
|
+
try:
|
|
76
|
+
from webui.bridge import init_bridge, init_soul_bridge, start_webui
|
|
77
|
+
|
|
78
|
+
init_bridge(ai.nervous)
|
|
79
|
+
if hasattr(ai, "_heart") and ai._heart and hasattr(ai._heart, "soul"):
|
|
80
|
+
init_soul_bridge(ai._heart.soul)
|
|
81
|
+
asyncio.create_task(start_webui(host="127.0.0.1", port=webui_port))
|
|
82
|
+
print(f"[Alive-AI] Dashboard running at http://127.0.0.1:{webui_port}")
|
|
83
|
+
except Exception as exc:
|
|
84
|
+
print(f"[Alive-AI] WebUI unavailable: {exc}")
|
|
85
|
+
|
|
86
|
+
await ai.start()
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
if __name__ == "__main__":
|
|
90
|
+
asyncio.run(main())
|
package/manifest.md
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# Alive-AI - AI Influencer
|
|
2
|
+
|
|
3
|
+
A modular AI that can grow, feel emotions, and form relationships.
|
|
4
|
+
|
|
5
|
+
## Architecture
|
|
6
|
+
- `core/` - Essential modules (events, config, self, state)
|
|
7
|
+
- `brain/` - Memory, LLM providers, subconscious, embeddings, STT
|
|
8
|
+
- `heart/` - Emotions, reactions, arousal, love/attachment system
|
|
9
|
+
- `input/` - Telegram listener, voice reader, commands
|
|
10
|
+
- `output/` - Text, voice (VibeTTS), images (Fal.ai), video
|
|
11
|
+
- `skills/` - Calendar, photo manager, video manager
|
|
12
|
+
|
|
13
|
+
## Key Features
|
|
14
|
+
- **Multi-provider LLM** - ZAI or OpenRouter (main/thinking/fast models)
|
|
15
|
+
- **Subconscious loop** - 24/7 background process, generates impulses
|
|
16
|
+
- **Vector memory** - Redis-based semantic search with embeddings
|
|
17
|
+
- **Emotional system** - Continuous state with natural decay
|
|
18
|
+
- **Proactive messaging** - Sends messages based on impulses/feelings
|
|
19
|
+
- **Voice synthesis** - VibeTTS with mood-based CFG, auto-splits long text
|
|
20
|
+
- **Media management** - Photos/videos with categories and no-repeat
|
|
21
|
+
- **Admin commands** - /status, /impulse, /stats, /reset, /10min
|
|
22
|
+
|
|
23
|
+
## Entry Point
|
|
24
|
+
```python
|
|
25
|
+
from core.self import Self
|
|
26
|
+
ai = Self(Path("."))
|
|
27
|
+
await ai.start()
|
|
28
|
+
```
|
package/mypics/.gitkeep
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
package/myvids/.gitkeep
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Output modules"""
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Image output"""
|