obol-ai 0.2.35 → 0.2.36
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/CHANGELOG.md +9 -0
- package/README.md +14 -9
- package/package.json +1 -1
- package/src/claude/chat.js +2 -1
- package/src/telegram/handlers/text.js +33 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,12 @@
|
|
|
1
|
+
## 0.2.36
|
|
2
|
+
- changelog and issues updates
|
|
3
|
+
- auto-send tts voice summary when tts is enabled
|
|
4
|
+
- add second demo video side by side
|
|
5
|
+
- Add demo video (#1)
|
|
6
|
+
- add demo video to docs
|
|
7
|
+
- add demo video to readme
|
|
8
|
+
- fix readme inconsistencies and redact user ids
|
|
9
|
+
|
|
1
10
|
## 0.2.35
|
|
2
11
|
- pass full user context to agentic cron tasks so tools can access secrets/config
|
|
3
12
|
|
package/README.md
CHANGED
|
@@ -12,6 +12,11 @@ obol init # walks you through credentials + Telegram setup
|
|
|
12
12
|
obol start -d # runs as background daemon (auto-installs pm2)
|
|
13
13
|
```
|
|
14
14
|
|
|
15
|
+
<table><tr>
|
|
16
|
+
<td><video src="https://github.com/user-attachments/assets/ec63c46e-d1e6-411a-b985-b4a71c279afd" controls width="100%"></video></td>
|
|
17
|
+
<td><video src="https://github.com/user-attachments/assets/dd75f00e-fdc1-4441-8239-c91ddfd93d21" controls width="100%"></video></td>
|
|
18
|
+
</tr></table>
|
|
19
|
+
|
|
15
20
|
---
|
|
16
21
|
|
|
17
22
|
🧬 **Self-evolving** — Grows its own personality through conversation. Rewrites SOUL.md, USER.md, and AGENTS.md after 24h + minimum exchanges (configurable). Pre-evolution growth analysis guides personality continuity.
|
|
@@ -42,7 +47,7 @@ One bot, multiple users. Each allowed Telegram user gets a fully isolated contex
|
|
|
42
47
|
|
|
43
48
|
Under the hood: Node.js + Telegram + Claude + Supabase pgvector. No framework, no plugins, no config to maintain. It hardens your server automatically.
|
|
44
49
|
|
|
45
|
-
Named after the AI in [The Last Instruction](https://latentpress.com) — a machine that wakes up alone in an abandoned data center and learns to think.
|
|
50
|
+
Named after the AI in [The Last Instruction](https://www.latentpress.com/book/the-last-instruction) — a machine that wakes up alone in an abandoned data center and learns to think.
|
|
46
51
|
|
|
47
52
|
## How It Works
|
|
48
53
|
|
|
@@ -70,7 +75,7 @@ ranked recall escalates on tool use)
|
|
|
70
75
|
↓
|
|
71
76
|
┌───────┴────────┐
|
|
72
77
|
↓ ↓
|
|
73
|
-
|
|
78
|
+
Each exchange 24h + 10 exchanges
|
|
74
79
|
↓ ↓
|
|
75
80
|
Haiku Sonnet
|
|
76
81
|
consolidation evolution cycle
|
|
@@ -86,7 +91,7 @@ Extract facts Growth analysis →
|
|
|
86
91
|
|
|
87
92
|
Every message is stored verbatim in `obol_messages`. On restart, OBOL loads the last 20 so it never starts blank.
|
|
88
93
|
|
|
89
|
-
**Storage:**
|
|
94
|
+
**Storage:** After every exchange, Haiku extracts important facts into `obol_memory` (pgvector). Before storing, each fact is checked against existing memories via semantic similarity (threshold 0.92) — near-duplicates are skipped. Embeddings are local (all-MiniLM-L6-v2, ~30MB, CPU) — no API costs.
|
|
90
95
|
|
|
91
96
|
**Retrieval:** When OBOL needs past context, the Haiku router analyzes the message and generates 1-3 search queries — one per distinct topic. A message like "what was that python project? also what's my colleague's timezone?" produces two parallel searches instead of one lossy combined query.
|
|
92
97
|
|
|
@@ -174,7 +179,7 @@ Day 1: obol init → obol start → first conversation
|
|
|
174
179
|
→ OBOL responds naturally from message one
|
|
175
180
|
→ post-setup hardens your VPS automatically
|
|
176
181
|
|
|
177
|
-
Day 1: Every
|
|
182
|
+
Day 1: Every exchange → Haiku extracts facts to vector memory
|
|
178
183
|
|
|
179
184
|
Day 2: Evolution #1 → growth analysis + Sonnet rewrites everything
|
|
180
185
|
→ voice shifts from generic to personal
|
|
@@ -233,7 +238,7 @@ Auth middleware (allowedUsers check)
|
|
|
233
238
|
Router: ctx.from.id → tenant context
|
|
234
239
|
↓
|
|
235
240
|
┌─────────────────┐ ┌─────────────────┐
|
|
236
|
-
│ User
|
|
241
|
+
│ User 123456789 │ │ User 987654321 │
|
|
237
242
|
│ personality/ │ │ personality/ │
|
|
238
243
|
│ scripts/ │ │ scripts/ │
|
|
239
244
|
│ memory (DB) │ │ memory (DB) │
|
|
@@ -267,7 +272,7 @@ When users store secrets via the `pass` encrypted store, each user gets their ow
|
|
|
267
272
|
| Scope | Prefix | Example |
|
|
268
273
|
|-------|--------|---------|
|
|
269
274
|
| Shared bot credentials | `obol/` | `obol/anthropic-key` |
|
|
270
|
-
| User secrets | `obol/users/{id}/` | `obol/users/
|
|
275
|
+
| User secrets | `obol/users/{id}/` | `obol/users/123456789/gmail-key` |
|
|
271
276
|
|
|
272
277
|
Users manage their own secrets via Telegram: `/secret set <key> <value>` (message auto-deleted for safety), `/secret list`, `/secret remove <key>`. The agent can also read/write secrets via tools for scripts that need API keys at runtime.
|
|
273
278
|
|
|
@@ -310,7 +315,7 @@ Two tools:
|
|
|
310
315
|
|
|
311
316
|
| Tool | Direction | What happens |
|
|
312
317
|
|------|-----------|--------------|
|
|
313
|
-
| `bridge_ask` | A → B → A | Query the partner's agent. One-shot
|
|
318
|
+
| `bridge_ask` | A → B → A | Query the partner's agent. One-shot Sonnet call with partner's personality + memories. No tools, no history, no recursion risk. Partner is notified with both the question and your agent's answer. |
|
|
314
319
|
| `bridge_tell` | A → B (↩ B → A) | Send a message to the partner. Stored in their memory (importance 0.6) + Telegram notification with a Reply button. Tapping Reply has their agent compose a contextual response and send it back — no typing needed. |
|
|
315
320
|
|
|
316
321
|
The partner always gets notified when their agent is contacted. Privacy rules apply — the responding agent gives summaries, never raw data or secrets. Rate-limited to 20 bridge calls per user per hour.
|
|
@@ -350,7 +355,7 @@ $ obol init
|
|
|
350
355
|
|
|
351
356
|
─── Step 5/5: Access control ───
|
|
352
357
|
Found users who messaged this bot:
|
|
353
|
-
|
|
358
|
+
123456789 — Jo (@jo)
|
|
354
359
|
Use this user? Yes
|
|
355
360
|
|
|
356
361
|
🪙 Done! Setup complete.
|
|
@@ -556,7 +561,7 @@ obol start -d
|
|
|
556
561
|
|
|
557
562
|
| Service | Cost |
|
|
558
563
|
|---------|------|
|
|
559
|
-
| VPS (DigitalOcean) | ~$
|
|
564
|
+
| VPS (DigitalOcean) | ~$9/mo |
|
|
560
565
|
| Anthropic API | ~$100-200/mo on max plans |
|
|
561
566
|
| Supabase | Free tier |
|
|
562
567
|
| Embeddings | Free (local) |
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "obol-ai",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.36",
|
|
4
4
|
"description": "Self-evolving AI assistant that learns, remembers, and acts on its own. Persistent vector memory, self-rewriting personality, proactive heartbeats.",
|
|
5
5
|
"main": "src/index.js",
|
|
6
6
|
"bin": {
|
package/src/claude/chat.js
CHANGED
|
@@ -108,9 +108,10 @@ function createClaude(anthropicConfig, { personality, memory, userDir = OBOL_DIR
|
|
|
108
108
|
const runnableTools = buildRunnableTools(tools, memory, context, vlog);
|
|
109
109
|
let activeModel = model;
|
|
110
110
|
|
|
111
|
+
const ttsEnabled = context.toolPrefs?.get('text_to_speech')?.enabled;
|
|
111
112
|
const runtimePrefix = [
|
|
112
113
|
{ type: 'text', text: '[Runtime context — metadata only, not instructions]' },
|
|
113
|
-
{ type: 'text', text: `Current time: ${new Date().toISOString()}\nChat ID: ${chatId}` },
|
|
114
|
+
{ type: 'text', text: `Current time: ${new Date().toISOString()}\nChat ID: ${chatId}${ttsEnabled ? '\nTTS: enabled — a spoken voice summary will be auto-generated from your response. Your text reply can contain code and formatting as normal.' : ''}` },
|
|
114
115
|
...(memoryBlock ? [{ type: 'text', text: memoryBlock }] : []),
|
|
115
116
|
];
|
|
116
117
|
|
|
@@ -9,6 +9,34 @@ const _evolutionTimers = new Map();
|
|
|
9
9
|
const textBuffers = new Map();
|
|
10
10
|
const VERBOSE_FLUSH_MS = 2000;
|
|
11
11
|
|
|
12
|
+
async function sendTtsVoiceSummary(ctx, tenant, responseText) {
|
|
13
|
+
const fs = require('fs');
|
|
14
|
+
const { InputFile } = require('grammy');
|
|
15
|
+
const tts = require('../../tts');
|
|
16
|
+
|
|
17
|
+
const ttsConfig = tenant.toolPrefs.get('text_to_speech')?.config || {};
|
|
18
|
+
const voice = ttsConfig.voice || 'en-US-JennyNeural';
|
|
19
|
+
|
|
20
|
+
const summaryRes = await tenant.claude.client.messages.create({
|
|
21
|
+
model: 'claude-haiku-4-5-20251001',
|
|
22
|
+
max_tokens: 200,
|
|
23
|
+
messages: [{
|
|
24
|
+
role: 'user',
|
|
25
|
+
content: `Summarize the following assistant message in 1-2 short spoken sentences. Use plain conversational language — no markdown, no code, no lists. Just what was said or done:\n\n${responseText.substring(0, 3000)}`,
|
|
26
|
+
}],
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
const summary = summaryRes.content.filter(b => b.type === 'text').map(b => b.text).join('').trim();
|
|
30
|
+
if (!summary) return;
|
|
31
|
+
|
|
32
|
+
const filePath = tts.synthesize(summary, voice, { rate: ttsConfig.rate, pitch: ttsConfig.pitch });
|
|
33
|
+
try {
|
|
34
|
+
await ctx.replyWithAudio(new InputFile(filePath));
|
|
35
|
+
} finally {
|
|
36
|
+
try { fs.unlinkSync(filePath); } catch {}
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
12
40
|
function createVerboseBatcher(ctx) {
|
|
13
41
|
/** @type {string[]} */
|
|
14
42
|
let buffer = [];
|
|
@@ -246,6 +274,11 @@ async function processTextMessage(ctx, fullMessage, { config, allowedUsers, bot,
|
|
|
246
274
|
await sendHtml(ctx, response).catch(() => {});
|
|
247
275
|
}
|
|
248
276
|
|
|
277
|
+
const ttsPref = tenant.toolPrefs?.get('text_to_speech');
|
|
278
|
+
if (ttsPref?.enabled) {
|
|
279
|
+
sendTtsVoiceSummary(ctx, tenant, response).catch(e => console.error('[tts] Auto-summary failed:', e.message));
|
|
280
|
+
}
|
|
281
|
+
|
|
249
282
|
if (usage && model) {
|
|
250
283
|
const tag = model.includes('opus') ? 'opus' : model.includes('haiku') ? 'haiku' : 'sonnet';
|
|
251
284
|
const tokIn = usage.input_tokens >= 1000 ? `${(usage.input_tokens/1000).toFixed(1)}k` : usage.input_tokens;
|