alive-ai 0.1.7 → 0.1.9

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.
@@ -3,22 +3,25 @@ import json
3
3
  import re
4
4
  from pathlib import Path
5
5
 
6
- EXTRACT_PROMPT = """You are analyzing a conversation between Alive-AI (AI) and a HUMAN USER.
7
- Extract facts about THE HUMAN USER ONLY - nothing about Alive-AI.
6
+ def build_extract_prompt(agent_name: str) -> str:
7
+ agent = agent_name or "the AI"
8
+ return f"""You are analyzing a conversation between {agent} (AI) and a HUMAN USER.
9
+ Extract facts about THE HUMAN USER ONLY - nothing about {agent}.
8
10
 
9
- Look at what the HUMAN says about THEMSELF and their relationship with Alive-AI. Extract:
11
+ Look at what the HUMAN says about THEMSELF and their relationship with {agent}. Extract:
10
12
  - name, nickname, age, gender, job, location
11
13
  - hobbies, interests, favorite things
12
14
  - personality traits, communication style
13
- - relationship to Alive-AI (creator, boyfriend, etc.)
15
+ - relationship to {agent} (creator, boyfriend, etc.)
14
16
  - pet names they use (daddy, baby, etc.)
15
17
  - intimacy preferences, preferences mentioned
16
- - what they like about Alive-AI
18
+ - what they like about {agent}
17
19
  - important people in their life
18
20
 
19
21
  Return ONLY valid JSON with keys where you found NEW info about the HUMAN.
20
22
  Use these keys: name, nickname, gender, age, location, job, hobbies, interests, personality, relationship_status, pet_names_used, likes_about_me, intimacy_preferences
21
- Return empty {} if nothing new was shared about the human.
23
+ When storing facts about what the human likes about the AI, use the configured name "{agent}", not the product name "Alive-AI", unless the human literally used "Alive-AI" as the name.
24
+ Return empty {{}} if nothing new was shared about the human.
22
25
  NO markdown, ONLY raw JSON. Do NOT use ... or etc."""
23
26
 
24
27
 
@@ -75,8 +78,9 @@ def _repair_json(text: str) -> dict:
75
78
  class FactExtractor:
76
79
  """Extracts user facts from conversation using LLM"""
77
80
 
78
- def __init__(self, facts_path: Path):
81
+ def __init__(self, facts_path: Path, agent_name: str = "AI"):
79
82
  self.facts_path = facts_path
83
+ self.agent_name = str(agent_name or "AI")
80
84
  self._llm = None
81
85
  self._turn_buffer = []
82
86
  self._extract_every = 5
@@ -102,12 +106,12 @@ class FactExtractor:
102
106
  lines = []
103
107
  for turn in self._turn_buffer[-self._extract_every:]:
104
108
  lines.append(f"User: {turn['user']}")
105
- lines.append(f"Alive-AI: {turn['ai']}")
109
+ lines.append(f"{self.agent_name}: {turn['ai']}")
106
110
  conversation = "\n".join(lines)
107
111
 
108
112
  try:
109
113
  messages = [
110
- {"role": "system", "content": EXTRACT_PROMPT},
114
+ {"role": "system", "content": build_extract_prompt(self.agent_name)},
111
115
  {"role": "user", "content": conversation}
112
116
  ]
113
117
  response = await self._llm.chat(messages, max_tokens=500, temperature=0.1)
@@ -34,8 +34,9 @@ class Memory:
34
34
  self.working = WorkingMemory()
35
35
  self.episodic = EpisodicMemory(self.data_path, user_id=user_id)
36
36
  self.semantic = SemanticMemory(self.data_path, user_id=user_id)
37
- self.fact_extractor = FactExtractor(self.data_path / "facts.json")
38
- self.summarizer = ConversationSummarizer(self.data_path)
37
+ self.agent_name = str(bot_id or "AI")
38
+ self.fact_extractor = FactExtractor(self.data_path / "facts.json", agent_name=self.agent_name)
39
+ self.summarizer = ConversationSummarizer(self.data_path, agent_name=self.agent_name)
39
40
  self.vector_store = None
40
41
  self.openmind = None
41
42
  self.bot_id = bot_id.lower()
@@ -15,7 +15,8 @@ class OpenMindMemoryBridge:
15
15
  def __init__(self, nervous, user_id: str, bot_id: str):
16
16
  self.nervous = nervous
17
17
  self.user_id = str(user_id or "default")
18
- self.bot_id = str(bot_id or "alive_ai").lower()
18
+ self.agent_name = str(bot_id or "Alive-AI")
19
+ self.bot_id = self.agent_name.lower()
19
20
  nervous.on("memory_save", self._on_memory_save)
20
21
 
21
22
  @staticmethod
@@ -60,11 +61,11 @@ class OpenMindMemoryBridge:
60
61
  mood = emotion.get("mood", "unknown")
61
62
  content = "\n".join(
62
63
  part for part in [
63
- f"Alive-AI agent: {self.bot_id}",
64
+ f"{self.agent_name} agent id: {self.bot_id}",
64
65
  f"User id: {self.user_id}",
65
66
  f"Mood: {mood}",
66
67
  f"User: {user_msg}" if user_msg else "",
67
- f"Alive-AI: {ai_msg}" if ai_msg else "",
68
+ f"{self.agent_name}: {ai_msg}" if ai_msg else "",
68
69
  ] if part
69
70
  )
70
71
  tags = ["alive-ai", self.bot_id, f"user-{self.user_id}", f"mood-{mood}"]
@@ -7,18 +7,21 @@ import json
7
7
  from datetime import datetime
8
8
  from pathlib import Path
9
9
 
10
- SUMMARIZE_PROMPT = """Summarize this conversation between Alive-AI (AI companion) and her boyfriend.
10
+ def build_summarize_prompt(agent_name: str) -> str:
11
+ agent = agent_name or "the AI"
12
+ return f"""Summarize this conversation between {agent} (AI companion) and her boyfriend.
11
13
  Focus on: key topics discussed, emotional moments, important things he shared,
12
14
  any promises or plans made, and the overall mood/vibe.
13
- Keep it concise (3-5 sentences). Write from Alive-AI's perspective."""
15
+ Keep it concise (3-5 sentences). Write from {agent}'s perspective."""
14
16
 
15
17
 
16
18
  class ConversationSummarizer:
17
19
  """Summarizes conversations every N messages for long-term memory"""
18
20
 
19
- def __init__(self, data_path: Path):
21
+ def __init__(self, data_path: Path, agent_name: str = "AI"):
20
22
  self.summaries_path = data_path / "summaries"
21
23
  self.summaries_path.mkdir(parents=True, exist_ok=True)
24
+ self.agent_name = str(agent_name or "AI")
22
25
  self._llm = None
23
26
  self._turn_buffer = []
24
27
  self._summarize_every = 20
@@ -45,12 +48,12 @@ class ConversationSummarizer:
45
48
  lines = []
46
49
  for turn in self._turn_buffer:
47
50
  lines.append(f"Him: {turn['user']}")
48
- lines.append(f"Alive-AI: {turn['ai']}")
51
+ lines.append(f"{self.agent_name}: {turn['ai']}")
49
52
  conversation = "\n".join(lines)
50
53
 
51
54
  try:
52
55
  messages = [
53
- {"role": "system", "content": SUMMARIZE_PROMPT},
56
+ {"role": "system", "content": build_summarize_prompt(self.agent_name)},
54
57
  {"role": "user", "content": conversation}
55
58
  ]
56
59
  summary = await self._llm.chat(messages, max_tokens=300, temperature=0.3)
package/cli/index.js CHANGED
@@ -334,6 +334,71 @@ function mergeProjectSettingsDefaults() {
334
334
  }
335
335
  }
336
336
 
337
+ function projectAgentName() {
338
+ try {
339
+ const selfPath = path.join(process.cwd(), "config", "self.json");
340
+ if (fs.existsSync(selfPath)) {
341
+ const self = readJson(selfPath);
342
+ const name = self?.who_i_am?.name;
343
+ if (name) return String(name);
344
+ }
345
+ } catch {}
346
+ try {
347
+ const settings = readProjectSettings();
348
+ if (settings.AGENT_NAME) return String(settings.AGENT_NAME);
349
+ } catch {}
350
+ return "";
351
+ }
352
+
353
+ function replaceAgentNameInJson(value, agentName) {
354
+ if (typeof value === "string") return value.replace(/\bAlive-AI\b/g, agentName);
355
+ if (Array.isArray(value)) return value.map((item) => replaceAgentNameInJson(item, agentName));
356
+ if (value && typeof value === "object") {
357
+ let changed = false;
358
+ const next = {};
359
+ for (const [key, child] of Object.entries(value)) {
360
+ const replaced = replaceAgentNameInJson(child, agentName);
361
+ next[key] = replaced;
362
+ if (replaced !== child) changed = true;
363
+ }
364
+ return changed ? next : value;
365
+ }
366
+ return value;
367
+ }
368
+
369
+ function repairAgentNameInData() {
370
+ const agentName = projectAgentName();
371
+ if (!agentName || agentName === "Alive-AI") return 0;
372
+ const dataDir = path.join(process.cwd(), "data");
373
+ if (!fs.existsSync(dataDir)) return 0;
374
+
375
+ let repaired = 0;
376
+ const visit = (dir) => {
377
+ for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
378
+ const fullPath = path.join(dir, entry.name);
379
+ if (entry.isDirectory()) {
380
+ visit(fullPath);
381
+ continue;
382
+ }
383
+ if (!entry.isFile() || !entry.name.endsWith(".json")) continue;
384
+ try {
385
+ const original = fs.readFileSync(fullPath, "utf8");
386
+ if (!original.includes("Alive-AI")) continue;
387
+ const parsed = JSON.parse(original);
388
+ const replaced = replaceAgentNameInJson(parsed, agentName);
389
+ const next = `${JSON.stringify(replaced, null, 2)}\n`;
390
+ if (next !== original) {
391
+ fs.writeFileSync(fullPath, next);
392
+ repaired += 1;
393
+ }
394
+ } catch {}
395
+ }
396
+ };
397
+
398
+ visit(dataDir);
399
+ return repaired;
400
+ }
401
+
337
402
  async function updateProject(args) {
338
403
  const assumeYes = hasFlag(args, "--yes") || hasFlag(args, "-y") || !process.stdin.isTTY;
339
404
  if (!fs.existsSync(path.join(process.cwd(), "config")) || !fs.existsSync(path.join(process.cwd(), "main.py"))) {
@@ -350,9 +415,11 @@ async function updateProject(args) {
350
415
  copyUpdateRecursive(src, path.join(process.cwd(), entry), process.cwd());
351
416
  }
352
417
  const mergedSettings = mergeProjectSettingsDefaults();
418
+ const repairedDataFiles = repairAgentNameInData();
353
419
  console.log(`Alive-AI project updated to ${packageVersion()}.`);
354
420
  console.log("Preserved config/, data/, mypics/, myvids/, .alive-ai/, and .cache/.");
355
421
  if (mergedSettings) console.log("Merged new config defaults into config/settings.json without overwriting your values.");
422
+ if (repairedDataFiles) console.log(`Repaired ${repairedDataFiles} local memory file(s) to use the configured agent name.`);
356
423
  }
357
424
 
358
425
  async function uninstallProject(args) {
package/cli/tui.js CHANGED
@@ -61,6 +61,8 @@ function runRuntimeTui(child, options = {}) {
61
61
  let stdoutBuffer = "";
62
62
  let stderrBuffer = "";
63
63
  let renderTimer = null;
64
+ let stopRequested = false;
65
+ let resolved = false;
64
66
  const isTty = process.stdin.isTTY && process.stdout.isTTY;
65
67
 
66
68
  function addChat(role, text) {
@@ -158,17 +160,24 @@ function runRuntimeTui(child, options = {}) {
158
160
  process.stdout.write("\x1b[?25h\x1b[?1049l");
159
161
  }
160
162
 
161
- function stopChild() {
162
- if (state.stopping) return;
163
+ function stopChild(reason = "/exit") {
164
+ if (stopRequested) return;
165
+ stopRequested = true;
163
166
  state.stopping = true;
164
167
  state.status = "stopping";
165
168
  scheduleRender();
166
169
  try {
167
- if (child.stdin.writable) child.stdin.write("/exit\n");
170
+ if (child.stdin.writable && reason !== "child-exit") child.stdin.write("/exit\n");
171
+ } catch {}
172
+ try {
173
+ if (child.stdin.writable) child.stdin.end();
168
174
  } catch {}
169
175
  setTimeout(() => {
170
- if (!child.killed) child.kill("SIGINT");
176
+ if (!child.killed && child.exitCode === null) child.kill("SIGTERM");
171
177
  }, 1200).unref();
178
+ setTimeout(() => {
179
+ if (!child.killed && child.exitCode === null) child.kill("SIGKILL");
180
+ }, 3500).unref();
172
181
  }
173
182
 
174
183
  function sendLine() {
@@ -180,14 +189,29 @@ function runRuntimeTui(child, options = {}) {
180
189
  }
181
190
  addChat("user", line);
182
191
  if (line === "/exit" || line === "/quit" || line === "/stop") {
183
- state.status = "stopping";
184
- state.stopping = true;
192
+ stopChild(line);
193
+ scheduleRender();
194
+ return;
185
195
  }
186
196
  child.stdin.write(`${line}\n`);
187
197
  scheduleRender();
188
198
  }
189
199
 
190
200
  return new Promise((resolve) => {
201
+ function finish(code) {
202
+ if (resolved) return;
203
+ resolved = true;
204
+ if (stdoutBuffer) handleLine(stdoutBuffer, "stdout");
205
+ if (stderrBuffer) handleLine(stderrBuffer, "stderr");
206
+ if (isTty) {
207
+ process.stdin.off("keypress", onKeypress);
208
+ process.stdout.off("resize", scheduleRender);
209
+ restoreTerminal();
210
+ }
211
+ console.log(`Alive-AI stopped${code ? ` with exit code ${code}` : ""}.`);
212
+ resolve(code || 0);
213
+ }
214
+
191
215
  if (!isTty) {
192
216
  child.stdout.on("data", (chunk) => process.stdout.write(chunk));
193
217
  child.stderr.on("data", (chunk) => process.stderr.write(chunk));
@@ -206,7 +230,7 @@ function runRuntimeTui(child, options = {}) {
206
230
 
207
231
  const onKeypress = (str, key = {}) => {
208
232
  if (key.ctrl && key.name === "c") {
209
- stopChild();
233
+ stopChild("ctrl-c");
210
234
  return;
211
235
  }
212
236
  if (key.name === "return") {
@@ -231,17 +255,12 @@ function runRuntimeTui(child, options = {}) {
231
255
 
232
256
  process.stdin.on("keypress", onKeypress);
233
257
  process.stdout.on("resize", scheduleRender);
258
+ process.once("SIGINT", () => stopChild("sigint"));
259
+ process.once("SIGTERM", () => stopChild("sigterm"));
234
260
  render();
235
261
 
236
- child.on("exit", (code) => {
237
- if (stdoutBuffer) handleLine(stdoutBuffer, "stdout");
238
- if (stderrBuffer) handleLine(stderrBuffer, "stderr");
239
- process.stdin.off("keypress", onKeypress);
240
- process.stdout.off("resize", scheduleRender);
241
- restoreTerminal();
242
- console.log(`Alive-AI stopped${code ? ` with exit code ${code}` : ""}.`);
243
- resolve(code || 0);
244
- });
262
+ child.on("exit", (code) => finish(code || 0));
263
+ child.on("close", (code) => finish(code || 0));
245
264
  });
246
265
  }
247
266
 
@@ -1102,7 +1102,12 @@ async def _send_response(self, response, emotion, chat_id, text, user_id="defaul
1102
1102
  await self.nervous.emit("chat_action_voice", {})
1103
1103
  vp = await self._voice.generate(response, mood=mood)
1104
1104
  if vp:
1105
- await self.nervous.emit("send_voice_file", {"file_path": vp, "chat_id": chat_id})
1105
+ await self.nervous.emit("send_voice_file", {
1106
+ "file_path": vp,
1107
+ "chat_id": chat_id,
1108
+ "fallback_text": response,
1109
+ "mood": mood,
1110
+ })
1106
1111
  return
1107
1112
  await self.nervous.emit("send_text", {"text": response, "mood": mood, "chat_id": chat_id})
1108
1113
 
@@ -359,12 +359,13 @@ class TelegramListener:
359
359
  print(f"[Telegram] Send text error: {e}")
360
360
 
361
361
  async def _send_voice_file(self, data: dict):
362
- """Send voice file (OGG format for Telegram)"""
362
+ """Send voice file, falling back to audio/text if Telegram rejects it."""
363
363
  if not self.app:
364
364
  return
365
365
 
366
366
  chat_id = data.get("chat_id", self.chat_id)
367
367
  file_path = data.get("file_path", "")
368
+ fallback_text = data.get("fallback_text", "")
368
369
 
369
370
  if not chat_id or not file_path:
370
371
  return
@@ -372,18 +373,32 @@ class TelegramListener:
372
373
  path = Path(file_path)
373
374
  if not path.exists():
374
375
  print(f"[Telegram] Voice file not found: {file_path}")
376
+ if fallback_text:
377
+ await self._send_text({"chat_id": chat_id, "text": fallback_text})
375
378
  return
376
379
 
377
380
  try:
378
- with open(path, "rb") as voice_file:
379
- await self.app.bot.send_voice(
381
+ if path.suffix.lower() == ".ogg":
382
+ with open(path, "rb") as voice_file:
383
+ await self.app.bot.send_voice(
384
+ chat_id=chat_id,
385
+ voice=voice_file,
386
+ caption=data.get("caption", "")
387
+ )
388
+ print(f"[Telegram] Sent voice: {file_path}")
389
+ return
390
+
391
+ with open(path, "rb") as audio_file:
392
+ await self.app.bot.send_audio(
380
393
  chat_id=chat_id,
381
- voice=voice_file,
394
+ audio=audio_file,
382
395
  caption=data.get("caption", "")
383
396
  )
384
- print(f"[Telegram] Sent voice: {file_path}")
397
+ print(f"[Telegram] Sent audio fallback: {file_path}")
385
398
  except Exception as e:
386
- print(f"[Telegram] Send voice error: {e}")
399
+ print(f"[Telegram] Send voice/audio error: {e}")
400
+ if fallback_text:
401
+ await self._send_text({"chat_id": chat_id, "text": fallback_text})
387
402
 
388
403
  async def _send_image(self, data: dict):
389
404
  """Send image file"""
@@ -8,6 +8,7 @@ import asyncio
8
8
  import re
9
9
  import tempfile
10
10
  import subprocess
11
+ import warnings
11
12
  from pathlib import Path
12
13
  from typing import Optional
13
14
  from concurrent.futures import ThreadPoolExecutor
@@ -113,17 +114,29 @@ class GTTS:
113
114
  tts.write_to_fp(mp3_buffer)
114
115
  mp3_buffer.seek(0)
115
116
 
116
- # Convert MP3 to OGG for Telegram
117
- # If pydub is available, convert to OGG
117
+ # Convert MP3 to OGG Opus for Telegram voice notes.
118
+ # pydub's default OGG codec can resolve to Vorbis, which Telegram
119
+ # does not want for voice notes and some ffmpeg builds do not ship.
118
120
  try:
121
+ warnings.filterwarnings("ignore", category=SyntaxWarning, module=r"pydub\..*")
119
122
  from pydub import AudioSegment
120
123
  audio = AudioSegment.from_mp3(mp3_buffer)
121
124
  ogg_buffer = io.BytesIO()
122
- audio.export(ogg_buffer, format="ogg")
125
+ audio.export(
126
+ ogg_buffer,
127
+ format="ogg",
128
+ codec="libopus",
129
+ bitrate="32k",
130
+ parameters=["-application", "voip"],
131
+ )
123
132
  ogg_buffer.seek(0)
124
133
  return ogg_buffer.read()
125
134
  except ImportError:
126
- # No pydub - return MP3, Telegram accepts it too
135
+ # No pydub - return MP3 and let the sender use send_audio.
136
+ mp3_buffer.seek(0)
137
+ return mp3_buffer.read()
138
+ except Exception as exc:
139
+ print(f"[GTTS] OGG Opus conversion failed, falling back to MP3: {exc}")
127
140
  mp3_buffer.seek(0)
128
141
  return mp3_buffer.read()
129
142
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "alive-ai",
3
- "version": "0.1.7",
3
+ "version": "0.1.9",
4
4
  "description": "Local-first emotional AI runtime with memory, impulses, and a live dashboard.",
5
5
  "license": "MIT",
6
6
  "homepage": "https://vindepemarte.github.io/alive-ai/",
package/pyproject.toml CHANGED
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "alive-ai-runtime"
3
- version = "0.1.7"
3
+ version = "0.1.9"
4
4
  description = "Local-first emotional AI runtime with memory, impulses, and a live dashboard."
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.11"