alive-ai 0.1.5 → 0.1.6
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/README.md +12 -7
- package/brain/memory/manager.py +1 -1
- package/brain/memory/vector_store.py +54 -7
- package/cli/index.js +106 -2
- package/config/settings.example.json +6 -0
- package/core/message_handler.py +23 -7
- package/docs/index.html +2 -2
- package/input/terminal/listener.py +1 -0
- package/package.json +1 -1
- package/pyproject.toml +1 -1
package/README.md
CHANGED
|
@@ -54,7 +54,7 @@ alive-ai init my-ai
|
|
|
54
54
|
| --- | --- |
|
|
55
55
|
| `npx alive-ai@latest init my-ai` | Scaffold a clean local Alive-AI project. |
|
|
56
56
|
| `npx . setup` | Guided onboarding for local config, providers, Telegram, voice, images, and memory. |
|
|
57
|
-
| `npx . doctor` | Check OS, Node, Python, uv, ffmpeg,
|
|
57
|
+
| `npx . doctor` | Check OS, Node, Python, uv, ffmpeg, OpenMind, and Redis only when Redis is enabled. |
|
|
58
58
|
| `npx . doctor --fix` | Ask `y/N` for each missing installable tool and run the platform installer if approved. |
|
|
59
59
|
| `npx . chat` | Start the real runtime with split-pane terminal chat and logs. |
|
|
60
60
|
| `npx . chat --plain` | Start raw terminal chat without the TUI. |
|
|
@@ -66,7 +66,7 @@ alive-ai init my-ai
|
|
|
66
66
|
|
|
67
67
|
`start` and `chat` check npm for a newer Alive-AI version. You can update, skip once, or skip that specific version. Stop terminal chat with `/exit` or `Ctrl+C`.
|
|
68
68
|
|
|
69
|
-
`doctor --fix` is conservative: it prints the exact install command before running anything and asks separately for each missing tool. On macOS it uses Homebrew, on Windows it uses winget, and on Linux it supports apt, dnf, and pacman where possible.
|
|
69
|
+
`doctor --fix` is conservative: it prints the exact install command before running anything and asks separately for each missing tool. On macOS it uses Homebrew, on Windows it uses winget, and on Linux it supports apt, dnf, and pacman where possible. Redis is optional; doctor only checks or fixes it when `REDIS_VECTOR_MEMORY_ENABLED` is true.
|
|
70
70
|
|
|
71
71
|
If you use Docker:
|
|
72
72
|
|
|
@@ -96,7 +96,7 @@ myvids/
|
|
|
96
96
|
|
|
97
97
|
The setup accepts `skip` for optional keys and `local` for Ollama.
|
|
98
98
|
|
|
99
|
-
Startup config is loaded from
|
|
99
|
+
Startup config is loaded from:
|
|
100
100
|
|
|
101
101
|
```text
|
|
102
102
|
.env
|
|
@@ -104,7 +104,7 @@ config/secrets.env
|
|
|
104
104
|
config/settings.json
|
|
105
105
|
```
|
|
106
106
|
|
|
107
|
-
|
|
107
|
+
`config/settings.json` is the runtime source of truth created by setup. `.env` and `config/secrets.env` are read first for compatibility, then simple values from `config/settings.json` are exported into the process environment.
|
|
108
108
|
|
|
109
109
|
| Setup item | Options |
|
|
110
110
|
| --- | --- |
|
|
@@ -113,6 +113,7 @@ Shell environment variables win over `.env`/`config/secrets.env`. Runtime settin
|
|
|
113
113
|
| Voice | `gtts` local/free default, Google TTS, VibeVoice, or `skip`. |
|
|
114
114
|
| Images | Fal.ai API key or `skip`. Local media folders still work without image generation. |
|
|
115
115
|
| Memory | Built-in local memory, OpenMind cloud, or OpenMind local. |
|
|
116
|
+
| Redis vector cache | Optional. Leave it off when using OpenMind unless you specifically want a local Redis Stack vector index. |
|
|
116
117
|
|
|
117
118
|
Minimum useful paths:
|
|
118
119
|
|
|
@@ -185,6 +186,8 @@ Modes:
|
|
|
185
186
|
|
|
186
187
|
OpenMind does not replace Alive-AI's emotional state. It adds durable semantic recall across tools and machines.
|
|
187
188
|
|
|
189
|
+
Redis is not required when OpenMind is enabled. Alive-AI always keeps file-backed working, episodic, semantic, and emotional memory in the project `data/` folder. Redis Stack is an optional local vector cache for users who want it.
|
|
190
|
+
|
|
188
191
|
Cloud setup:
|
|
189
192
|
|
|
190
193
|
```text
|
|
@@ -222,9 +225,9 @@ Comfortable local setup:
|
|
|
222
225
|
| RAM | 16 GB for 3B-4B local models |
|
|
223
226
|
| RAM for bigger models | 32 GB for 7B+ local models, Redis, voice, and long sessions |
|
|
224
227
|
| Disk | 10 GB+, more if you keep local models/media |
|
|
225
|
-
| Optional tools | `uv`, `ffmpeg`, Docker, Ollama |
|
|
228
|
+
| Optional tools | `uv`, `ffmpeg`, Docker, Ollama, Redis Stack |
|
|
226
229
|
|
|
227
|
-
`npx . start` creates `.alive-ai/venv` and installs Python dependencies. System-level packages such as Node, Python, Ollama, Docker, and ffmpeg
|
|
230
|
+
`npx . start` creates `.alive-ai/venv` and installs Python dependencies. System-level packages such as Node, Python, Ollama, Docker, and ffmpeg can be checked with `npx . doctor`. Use `npx . doctor --fix` when you want guided installers.
|
|
228
231
|
|
|
229
232
|
The CLI prefers Python 3.12, 3.11, then 3.13 before falling back to the system `python3`. When `uv` is installed, Alive-AI now passes the selected Python explicitly so `uv` does not silently choose a newer interpreter.
|
|
230
233
|
|
|
@@ -262,7 +265,8 @@ https://vindepemarte.github.io/alive-ai/
|
|
|
262
265
|
Docker is optional. It is useful when you want Redis Stack for vector search:
|
|
263
266
|
|
|
264
267
|
```bash
|
|
265
|
-
|
|
268
|
+
# In config/settings.json, set REDIS_VECTOR_MEMORY_ENABLED to true.
|
|
269
|
+
npx . doctor --fix
|
|
266
270
|
npx . start
|
|
267
271
|
```
|
|
268
272
|
|
|
@@ -287,6 +291,7 @@ Implemented:
|
|
|
287
291
|
- [x] Split-pane terminal chat with logs
|
|
288
292
|
- [x] Local WebUI dashboard with live state streaming
|
|
289
293
|
- [x] Optional hybrid OpenMind cloud/local semantic memory
|
|
294
|
+
- [x] Optional Redis Stack vector cache with setup and doctor checks
|
|
290
295
|
- [x] npm/npx CLI scaffold, setup, doctor, demo, chat, and start commands
|
|
291
296
|
- [x] Update prompt and project uninstall command
|
|
292
297
|
- [x] `doctor --fix` guided system dependency installer
|
package/brain/memory/manager.py
CHANGED
|
@@ -39,7 +39,7 @@ class Memory:
|
|
|
39
39
|
self.vector_store = None
|
|
40
40
|
self.openmind = None
|
|
41
41
|
self.bot_id = bot_id.lower()
|
|
42
|
-
if embedding_service:
|
|
42
|
+
if embedding_service and VectorMemoryStore.enabled():
|
|
43
43
|
self.vector_store = VectorMemoryStore(embedding_service, user_id=user_id, bot_id=bot_id)
|
|
44
44
|
if self.vector_store.connect():
|
|
45
45
|
print(f"[Memory] Vector store ready for user {user_id} on bot {bot_id}! {self.vector_store.count()} memories")
|
|
@@ -4,15 +4,41 @@ Redis-based vector storage for semantic memory search
|
|
|
4
4
|
"""
|
|
5
5
|
|
|
6
6
|
import json
|
|
7
|
+
import os
|
|
7
8
|
import redis
|
|
9
|
+
import time
|
|
8
10
|
from datetime import datetime
|
|
9
11
|
from typing import List, Dict, Optional, Any
|
|
10
12
|
from pathlib import Path
|
|
11
13
|
import numpy as np
|
|
12
14
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
15
|
+
from core.settings import get as settings_get
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def _truthy(value) -> bool:
|
|
19
|
+
return str(value).strip().lower() in ("1", "true", "yes", "on")
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def redis_vector_memory_enabled() -> bool:
|
|
23
|
+
return _truthy(settings_get("REDIS_VECTOR_MEMORY_ENABLED", os.environ.get("REDIS_VECTOR_MEMORY_ENABLED", "false")))
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def redis_host() -> str:
|
|
27
|
+
return str(settings_get("REDIS_HOST", os.environ.get("REDIS_HOST", "127.0.0.1")) or "127.0.0.1")
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def redis_port() -> int:
|
|
31
|
+
try:
|
|
32
|
+
return int(settings_get("REDIS_PORT", os.environ.get("REDIS_PORT", "6379")) or 6379)
|
|
33
|
+
except (TypeError, ValueError):
|
|
34
|
+
return 6379
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def redis_retry_seconds() -> float:
|
|
38
|
+
try:
|
|
39
|
+
return float(settings_get("REDIS_RETRY_SECONDS", os.environ.get("REDIS_RETRY_SECONDS", "60")) or 60)
|
|
40
|
+
except (TypeError, ValueError):
|
|
41
|
+
return 60.0
|
|
16
42
|
|
|
17
43
|
# Memory archive path - detect Docker vs local development
|
|
18
44
|
_docker_archive = Path("/data/memory_archive")
|
|
@@ -40,8 +66,17 @@ class VectorMemoryStore:
|
|
|
40
66
|
self.dimension = dimension
|
|
41
67
|
self.user_id = user_id
|
|
42
68
|
self.bot_id = bot_id.lower()
|
|
69
|
+
self.host = redis_host()
|
|
70
|
+
self.port = redis_port()
|
|
71
|
+
self.retry_seconds = redis_retry_seconds()
|
|
43
72
|
self.redis = None
|
|
44
73
|
self._connected = False
|
|
74
|
+
self._last_connect_attempt = 0.0
|
|
75
|
+
self._failure_logged = False
|
|
76
|
+
|
|
77
|
+
@staticmethod
|
|
78
|
+
def enabled() -> bool:
|
|
79
|
+
return redis_vector_memory_enabled()
|
|
45
80
|
|
|
46
81
|
@staticmethod
|
|
47
82
|
def _decode(val):
|
|
@@ -50,21 +85,33 @@ class VectorMemoryStore:
|
|
|
50
85
|
|
|
51
86
|
def connect(self) -> bool:
|
|
52
87
|
"""Connect to Redis and create index if needed"""
|
|
88
|
+
if not self.enabled():
|
|
89
|
+
self._connected = False
|
|
90
|
+
return False
|
|
91
|
+
|
|
92
|
+
now = time.monotonic()
|
|
93
|
+
if self._last_connect_attempt and now - self._last_connect_attempt < self.retry_seconds:
|
|
94
|
+
return False
|
|
95
|
+
self._last_connect_attempt = now
|
|
96
|
+
|
|
53
97
|
try:
|
|
54
98
|
self.redis = redis.Redis(
|
|
55
|
-
host=
|
|
56
|
-
port=
|
|
99
|
+
host=self.host,
|
|
100
|
+
port=self.port,
|
|
57
101
|
decode_responses=False # binary-safe for embeddings
|
|
58
102
|
)
|
|
59
103
|
self.redis.ping()
|
|
60
104
|
self._connected = True
|
|
61
|
-
|
|
105
|
+
self._failure_logged = False
|
|
106
|
+
print(f"[VectorStore] Connected to Redis at {self.host}:{self.port}")
|
|
62
107
|
|
|
63
108
|
# Create vector index if not exists
|
|
64
109
|
self._create_index()
|
|
65
110
|
return True
|
|
66
111
|
except Exception as e:
|
|
67
|
-
|
|
112
|
+
if not self._failure_logged:
|
|
113
|
+
print(f"[VectorStore] Redis unavailable at {self.host}:{self.port}; vector cache disabled for now ({e})")
|
|
114
|
+
self._failure_logged = True
|
|
68
115
|
self._connected = False
|
|
69
116
|
return False
|
|
70
117
|
|
package/cli/index.js
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
|
|
4
4
|
const fs = require("fs");
|
|
5
5
|
const http = require("http");
|
|
6
|
+
const net = require("net");
|
|
6
7
|
const os = require("os");
|
|
7
8
|
const path = require("path");
|
|
8
9
|
const readline = require("readline");
|
|
@@ -294,6 +295,45 @@ function copyUpdateRecursive(src, dest, baseDest = dest) {
|
|
|
294
295
|
fs.copyFileSync(src, dest);
|
|
295
296
|
}
|
|
296
297
|
|
|
298
|
+
function mergeMissingConfig(target, defaults) {
|
|
299
|
+
let changed = false;
|
|
300
|
+
for (const [key, value] of Object.entries(defaults)) {
|
|
301
|
+
if (key.startsWith("_")) continue;
|
|
302
|
+
if (!(key in target)) {
|
|
303
|
+
target[key] = value;
|
|
304
|
+
changed = true;
|
|
305
|
+
continue;
|
|
306
|
+
}
|
|
307
|
+
if (
|
|
308
|
+
value &&
|
|
309
|
+
typeof value === "object" &&
|
|
310
|
+
!Array.isArray(value) &&
|
|
311
|
+
target[key] &&
|
|
312
|
+
typeof target[key] === "object" &&
|
|
313
|
+
!Array.isArray(target[key])
|
|
314
|
+
) {
|
|
315
|
+
changed = mergeMissingConfig(target[key], value) || changed;
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
return changed;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
function mergeProjectSettingsDefaults() {
|
|
322
|
+
const settingsPath = path.join(process.cwd(), "config", "settings.json");
|
|
323
|
+
const examplePath = path.join(process.cwd(), "config", "settings.example.json");
|
|
324
|
+
if (!fs.existsSync(settingsPath) || !fs.existsSync(examplePath)) return false;
|
|
325
|
+
try {
|
|
326
|
+
const settings = readJson(settingsPath);
|
|
327
|
+
const defaults = readJson(examplePath);
|
|
328
|
+
if (!mergeMissingConfig(settings, defaults)) return false;
|
|
329
|
+
writeJson(settingsPath, settings);
|
|
330
|
+
return true;
|
|
331
|
+
} catch (error) {
|
|
332
|
+
console.log(`Could not merge new config defaults: ${error.message}`);
|
|
333
|
+
return false;
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
|
|
297
337
|
async function updateProject(args) {
|
|
298
338
|
const assumeYes = hasFlag(args, "--yes") || hasFlag(args, "-y") || !process.stdin.isTTY;
|
|
299
339
|
if (!fs.existsSync(path.join(process.cwd(), "config")) || !fs.existsSync(path.join(process.cwd(), "main.py"))) {
|
|
@@ -309,8 +349,10 @@ async function updateProject(args) {
|
|
|
309
349
|
if (!fs.existsSync(src)) continue;
|
|
310
350
|
copyUpdateRecursive(src, path.join(process.cwd(), entry), process.cwd());
|
|
311
351
|
}
|
|
352
|
+
const mergedSettings = mergeProjectSettingsDefaults();
|
|
312
353
|
console.log(`Alive-AI project updated to ${packageVersion()}.`);
|
|
313
354
|
console.log("Preserved config/, data/, mypics/, myvids/, .alive-ai/, and .cache/.");
|
|
355
|
+
if (mergedSettings) console.log("Merged new config defaults into config/settings.json without overwriting your values.");
|
|
314
356
|
}
|
|
315
357
|
|
|
316
358
|
async function uninstallProject(args) {
|
|
@@ -428,6 +470,18 @@ async function setupProject(args) {
|
|
|
428
470
|
const openmindKey = openmindEnabled
|
|
429
471
|
? emptyIfSkipped(await ask("OpenMind API key (om_..., optional for unauthenticated local dev)", "", assumeYes))
|
|
430
472
|
: "";
|
|
473
|
+
const redisChoice = normalizeChoice(
|
|
474
|
+
await ask("Use optional Redis Stack vector cache? yes or no", "no", assumeYes),
|
|
475
|
+
"no"
|
|
476
|
+
);
|
|
477
|
+
const redisEnabled = ["yes", "y", "true", "1", "on"].includes(redisChoice);
|
|
478
|
+
const redisHost = redisEnabled
|
|
479
|
+
? emptyIfSkipped(await ask("Redis host", "127.0.0.1", assumeYes)) || "127.0.0.1"
|
|
480
|
+
: "127.0.0.1";
|
|
481
|
+
const redisPortAnswer = redisEnabled
|
|
482
|
+
? emptyIfSkipped(await ask("Redis port", "6379", assumeYes)) || "6379"
|
|
483
|
+
: "6379";
|
|
484
|
+
const redisPort = Number.parseInt(redisPortAnswer, 10) || 6379;
|
|
431
485
|
|
|
432
486
|
const settings = readJson(settingsExample);
|
|
433
487
|
settings.AGENT_NAME = displayName;
|
|
@@ -451,6 +505,9 @@ async function setupProject(args) {
|
|
|
451
505
|
settings.OPENMIND_MODE = openmindEnabled ? "hybrid" : "built-in";
|
|
452
506
|
settings.OPENMIND_BASE_URL = openmindBaseUrl || "https://theopenmind.pro";
|
|
453
507
|
settings.OPENMIND_API_KEY = openmindKey;
|
|
508
|
+
settings.REDIS_VECTOR_MEMORY_ENABLED = redisEnabled;
|
|
509
|
+
settings.REDIS_HOST = redisHost;
|
|
510
|
+
settings.REDIS_PORT = redisPort;
|
|
454
511
|
|
|
455
512
|
const self = readJson(selfExample);
|
|
456
513
|
self.who_i_am.name = displayName;
|
|
@@ -525,6 +582,13 @@ function packageManager() {
|
|
|
525
582
|
}
|
|
526
583
|
|
|
527
584
|
function installPlan(tool) {
|
|
585
|
+
if (tool === "redis") {
|
|
586
|
+
if (hasCommand("docker") && fs.existsSync(path.join(process.cwd(), "docker-compose.yml"))) {
|
|
587
|
+
return ["docker", "compose", "up", "-d", "redis"];
|
|
588
|
+
}
|
|
589
|
+
return null;
|
|
590
|
+
}
|
|
591
|
+
|
|
528
592
|
const manager = packageManager();
|
|
529
593
|
if (process.platform === "darwin") {
|
|
530
594
|
if (manager !== "brew") return null;
|
|
@@ -584,6 +648,9 @@ function installPlan(tool) {
|
|
|
584
648
|
}
|
|
585
649
|
|
|
586
650
|
function manualInstallHint(tool) {
|
|
651
|
+
if (tool === "redis") {
|
|
652
|
+
return "Redis Stack is optional. Either disable REDIS_VECTOR_MEMORY_ENABLED in config/settings.json, or install Docker and run `docker compose up -d redis`.";
|
|
653
|
+
}
|
|
587
654
|
if (process.platform === "darwin" && !hasCommand("brew")) {
|
|
588
655
|
return "Install Homebrew from https://brew.sh, then rerun `npx . doctor --fix`.";
|
|
589
656
|
}
|
|
@@ -643,6 +710,38 @@ function wantsOllama(settings) {
|
|
|
643
710
|
return provider === "ollama" || order.includes("ollama");
|
|
644
711
|
}
|
|
645
712
|
|
|
713
|
+
function truthy(value) {
|
|
714
|
+
return ["1", "true", "yes", "on"].includes(String(value || "").trim().toLowerCase());
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
function wantsRedis(settings) {
|
|
718
|
+
return truthy(settings.REDIS_VECTOR_MEMORY_ENABLED || process.env.REDIS_VECTOR_MEMORY_ENABLED);
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
function redisEndpoint(settings) {
|
|
722
|
+
const host = String(settings.REDIS_HOST || process.env.REDIS_HOST || "127.0.0.1").trim() || "127.0.0.1";
|
|
723
|
+
const port = Number.parseInt(settings.REDIS_PORT || process.env.REDIS_PORT || "6379", 10) || 6379;
|
|
724
|
+
return { host, port };
|
|
725
|
+
}
|
|
726
|
+
|
|
727
|
+
function checkTcp(host, port, timeoutMs = 1000) {
|
|
728
|
+
return new Promise((resolve) => {
|
|
729
|
+
const socket = new net.Socket();
|
|
730
|
+
let settled = false;
|
|
731
|
+
const finish = (ok) => {
|
|
732
|
+
if (settled) return;
|
|
733
|
+
settled = true;
|
|
734
|
+
socket.destroy();
|
|
735
|
+
resolve(ok);
|
|
736
|
+
};
|
|
737
|
+
socket.setTimeout(timeoutMs);
|
|
738
|
+
socket.once("connect", () => finish(true));
|
|
739
|
+
socket.once("error", () => finish(false));
|
|
740
|
+
socket.once("timeout", () => finish(false));
|
|
741
|
+
socket.connect(port, host);
|
|
742
|
+
});
|
|
743
|
+
}
|
|
744
|
+
|
|
646
745
|
async function doctor(args = []) {
|
|
647
746
|
const shouldFix = hasFlag(args, "--fix");
|
|
648
747
|
const assumeYes = hasFlag(args, "--yes") || hasFlag(args, "-y");
|
|
@@ -654,6 +753,9 @@ async function doctor(args = []) {
|
|
|
654
753
|
const node = process.version;
|
|
655
754
|
const nodeMajor = majorVersion(process.versions.node);
|
|
656
755
|
const settings = readProjectSettings();
|
|
756
|
+
const redisEnabled = wantsRedis(settings);
|
|
757
|
+
const redis = redisEndpoint(settings);
|
|
758
|
+
const redisReachable = redisEnabled ? await checkTcp(redis.host, redis.port, 1200) : false;
|
|
657
759
|
const venvPython = process.platform === "win32"
|
|
658
760
|
? path.join(process.cwd(), ".alive-ai", "venv", "Scripts", "python.exe")
|
|
659
761
|
: path.join(process.cwd(), ".alive-ai", "venv", "bin", "python");
|
|
@@ -667,7 +769,8 @@ async function doctor(args = []) {
|
|
|
667
769
|
}
|
|
668
770
|
console.log(` uv: ${uv || "missing, will use venv + pip"}`);
|
|
669
771
|
console.log(` ffmpeg: ${ffmpeg || "missing, voice conversion may be limited"}`);
|
|
670
|
-
console.log(` docker: ${docker || "missing, Redis
|
|
772
|
+
console.log(` docker: ${docker || (redisEnabled ? "missing, needed for local Redis Stack helper" : "missing, optional")}`);
|
|
773
|
+
console.log(` redis: ${redisEnabled ? `${redisReachable ? "reachable" : "unreachable"} (${redis.host}:${redis.port})` : "disabled"}`);
|
|
671
774
|
if (wantsOllama(settings)) {
|
|
672
775
|
console.log(` ollama: ${ollama || "missing, local LLM unavailable until installed"}`);
|
|
673
776
|
}
|
|
@@ -678,7 +781,8 @@ async function doctor(args = []) {
|
|
|
678
781
|
if (!python) missing.push({ id: "python", name: "Python 3.11+" });
|
|
679
782
|
if (!uv) missing.push({ id: "uv", name: "uv" });
|
|
680
783
|
if (!ffmpeg) missing.push({ id: "ffmpeg", name: "ffmpeg" });
|
|
681
|
-
if (!docker) missing.push({ id: "docker", name: "Docker" });
|
|
784
|
+
if (redisEnabled && !redisReachable && !docker) missing.push({ id: "docker", name: "Docker" });
|
|
785
|
+
if (redisEnabled && !redisReachable) missing.push({ id: "redis", name: "Redis Stack vector cache" });
|
|
682
786
|
if (wantsOllama(settings) && !ollama) missing.push({ id: "ollama", name: "Ollama" });
|
|
683
787
|
|
|
684
788
|
if (!python) {
|
|
@@ -9,6 +9,8 @@
|
|
|
9
9
|
"WEBUI_ENABLED": true,
|
|
10
10
|
"WEBUI_PORT": 8080,
|
|
11
11
|
"language": "en",
|
|
12
|
+
"MESSAGE_BATCH_DELAY_SECONDS": 3.5,
|
|
13
|
+
"TERMINAL_MESSAGE_BATCH_DELAY_SECONDS": 5.0,
|
|
12
14
|
"LLM_PROVIDER": "ollama",
|
|
13
15
|
"LLM_MAX_TOKENS": 500,
|
|
14
16
|
"LLM_CONTEXT_TOKENS": 4000,
|
|
@@ -40,6 +42,10 @@
|
|
|
40
42
|
"OPENMIND_MODE": "built-in",
|
|
41
43
|
"OPENMIND_BASE_URL": "https://theopenmind.pro",
|
|
42
44
|
"OPENMIND_API_KEY": "",
|
|
45
|
+
"REDIS_VECTOR_MEMORY_ENABLED": false,
|
|
46
|
+
"REDIS_HOST": "127.0.0.1",
|
|
47
|
+
"REDIS_PORT": 6379,
|
|
48
|
+
"REDIS_RETRY_SECONDS": 60,
|
|
43
49
|
"EMOTION_RATE_LOVE": 65,
|
|
44
50
|
"EMOTION_RATE_DESIRE": 45,
|
|
45
51
|
"EMOTION_RATE_AROUSAL": 40,
|
package/core/message_handler.py
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
"""Core: Message Handler — incoming messages, thinking, responses"""
|
|
2
|
-
import asyncio, random
|
|
2
|
+
import asyncio, os, random
|
|
3
3
|
from pathlib import Path
|
|
4
4
|
from datetime import datetime
|
|
5
5
|
from .thinking import build_mood_instruction, fallback_response
|
|
@@ -171,7 +171,7 @@ _MAX_USER_MEMORIES = 50 # Maximum cached user memories
|
|
|
171
171
|
_message_queue = {} # user_id -> list of messages
|
|
172
172
|
_batch_timers = {} # user_id -> timer task
|
|
173
173
|
_processing_locks = {} # user_id -> lock to prevent overlapping processing
|
|
174
|
-
_BATCH_DELAY = 3.5 #
|
|
174
|
+
_BATCH_DELAY = 3.5 # Default debounce for message batching
|
|
175
175
|
|
|
176
176
|
# Per-user pending media (prevents race condition when multiple users message simultaneously)
|
|
177
177
|
_pending_media = {} # user_id -> {"photo": ..., "video": ...}
|
|
@@ -191,6 +191,21 @@ def _feed_learning(sub, text: str):
|
|
|
191
191
|
print(f"[Learning] feedback error (non-fatal): {e}")
|
|
192
192
|
|
|
193
193
|
|
|
194
|
+
def _settings_float(key: str, default: float) -> float:
|
|
195
|
+
try:
|
|
196
|
+
from core.settings import get
|
|
197
|
+
value = get(key, os.environ.get(key, default))
|
|
198
|
+
return float(value)
|
|
199
|
+
except (TypeError, ValueError):
|
|
200
|
+
return default
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
def _batch_delay_for(data: dict) -> float:
|
|
204
|
+
if data.get("source") == "terminal":
|
|
205
|
+
return max(0.5, _settings_float("TERMINAL_MESSAGE_BATCH_DELAY_SECONDS", 5.0))
|
|
206
|
+
return max(0.5, _settings_float("MESSAGE_BATCH_DELAY_SECONDS", _BATCH_DELAY))
|
|
207
|
+
|
|
208
|
+
|
|
194
209
|
def _is_owner(user_id: str) -> bool:
|
|
195
210
|
"""Check if user is the owner (the operator)"""
|
|
196
211
|
from core.settings import get
|
|
@@ -354,21 +369,22 @@ async def handle_message(self, data: dict):
|
|
|
354
369
|
|
|
355
370
|
# Start new timer
|
|
356
371
|
queue_size = len(_message_queue[user_id])
|
|
372
|
+
batch_delay = _batch_delay_for(data)
|
|
357
373
|
if queue_size == 1:
|
|
358
|
-
print(f"[Batch] First message from {user_id}, waiting {
|
|
374
|
+
print(f"[Batch] First message from {user_id}, waiting {batch_delay:.1f}s...")
|
|
359
375
|
else:
|
|
360
376
|
print(f"[Batch] Message #{queue_size} from {user_id}, resetting timer...")
|
|
361
377
|
|
|
362
378
|
# Create timer task
|
|
363
379
|
_batch_timers[user_id] = asyncio.create_task(
|
|
364
|
-
_process_batch_after_delay(self, user_id, data)
|
|
380
|
+
_process_batch_after_delay(self, user_id, data, batch_delay)
|
|
365
381
|
)
|
|
366
382
|
|
|
367
383
|
|
|
368
|
-
async def _process_batch_after_delay(self, user_id: str, original_data: dict):
|
|
384
|
+
async def _process_batch_after_delay(self, user_id: str, original_data: dict, batch_delay: float):
|
|
369
385
|
"""Wait for batch delay, then process all queued messages together"""
|
|
370
386
|
try:
|
|
371
|
-
await asyncio.sleep(
|
|
387
|
+
await asyncio.sleep(batch_delay)
|
|
372
388
|
except asyncio.CancelledError:
|
|
373
389
|
# Timer was cancelled - new message came in
|
|
374
390
|
return
|
|
@@ -398,6 +414,7 @@ async def _process_batch_after_delay(self, user_id: str, original_data: dict):
|
|
|
398
414
|
"user_id": user_id,
|
|
399
415
|
"text": combined_text,
|
|
400
416
|
"chat_id": chat_id,
|
|
417
|
+
"source": original_data.get("source"),
|
|
401
418
|
"message_count": len(messages)
|
|
402
419
|
}
|
|
403
420
|
|
|
@@ -1437,4 +1454,3 @@ def get_aliveness_module_status() -> dict:
|
|
|
1437
1454
|
}
|
|
1438
1455
|
modules["modules_active"] = sum(v for v in modules.values() if isinstance(v, bool))
|
|
1439
1456
|
return modules
|
|
1440
|
-
|
package/docs/index.html
CHANGED
|
@@ -288,11 +288,11 @@ npx . chat</code></pre>
|
|
|
288
288
|
|
|
289
289
|
<section class="section" id="setup">
|
|
290
290
|
<h2>Setup Flow</h2>
|
|
291
|
-
<p>`npx . setup` asks for the minimum required configuration and lets optional systems be skipped. Use `local` for Ollama, `skip` for optional keys,
|
|
291
|
+
<p>`npx . setup` asks for the minimum required configuration and lets optional systems be skipped. Use `local` for Ollama, `skip` for optional keys, OpenMind cloud or local for shared long-term semantic memory, and leave Redis off unless you specifically want a local Redis Stack vector cache.</p>
|
|
292
292
|
<div class="steps">
|
|
293
293
|
<div class="step"><strong>Start local terminal chat</strong><span>`npx . chat` starts the same runtime with chat on the left, logs on the right, and the WebUI at `http://127.0.0.1:8080`.</span></div>
|
|
294
294
|
<div class="step"><strong>Start Telegram/runtime mode</strong><span>`npx . start` starts the configured input channel and validates Telegram before polling. Stop foreground runs with `Ctrl+C`.</span></div>
|
|
295
|
-
<div class="step"><strong>Install missing tools</strong><span>`npx . doctor --fix` asks `y/N` for each missing tool
|
|
295
|
+
<div class="step"><strong>Install missing tools</strong><span>`npx . doctor --fix` asks `y/N` for each missing tool. Redis is checked only when the Redis vector cache is enabled.</span></div>
|
|
296
296
|
<div class="step"><strong>Keep it updated</strong><span>`start` and `chat` check npm for newer versions. Use `npx . update` manually or `npx . uninstall` to remove local runtime files.</span></div>
|
|
297
297
|
<div class="step"><strong>Use OpenMind</strong><span>Choose `openmind-cloud` for `https://theopenmind.pro` or `openmind-local` for `http://127.0.0.1:3333`.</span></div>
|
|
298
298
|
<div class="step"><strong>Preview the dashboard</strong><span>`npx . demo` is keyless. The real runtime dashboard streams local state over SSE.</span></div>
|
package/package.json
CHANGED