obol-ai 0.2.2 ā 0.2.3
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/package.json +1 -1
- package/src/claude.js +75 -11
- package/src/defaults/AGENTS.md +33 -0
- package/src/telegram.js +105 -7
- package/src/tenant.js +1 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "obol-ai",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.3",
|
|
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.js
CHANGED
|
@@ -7,7 +7,7 @@ const { saveConfig, loadConfig, OBOL_DIR } = require('./config');
|
|
|
7
7
|
const { execAsync, isAllowedUrl } = require('./sanitize');
|
|
8
8
|
|
|
9
9
|
const MAX_EXEC_TIMEOUT = 120;
|
|
10
|
-
|
|
10
|
+
let MAX_TOOL_ITERATIONS = 100;
|
|
11
11
|
|
|
12
12
|
const BLOCKED_EXEC_PATTERNS = [
|
|
13
13
|
/\brm\s+(-[a-zA-Z]*f|-[a-zA-Z]*r|--force|--recursive)\b/,
|
|
@@ -124,6 +124,24 @@ async function ensureFreshToken(anthropicConfig) {
|
|
|
124
124
|
}
|
|
125
125
|
}
|
|
126
126
|
|
|
127
|
+
function repairHistory(history) {
|
|
128
|
+
for (let i = 0; i < history.length; i++) {
|
|
129
|
+
const msg = history[i];
|
|
130
|
+
if (msg.role !== 'assistant' || !Array.isArray(msg.content)) continue;
|
|
131
|
+
const toolUseIds = msg.content.filter(b => b.type === 'tool_use').map(b => b.id);
|
|
132
|
+
if (toolUseIds.length === 0) continue;
|
|
133
|
+
const next = history[i + 1];
|
|
134
|
+
const hasResults = next?.role === 'user' && Array.isArray(next.content) &&
|
|
135
|
+
toolUseIds.every(id => next.content.some(b => b.type === 'tool_result' && b.tool_use_id === id));
|
|
136
|
+
if (!hasResults) {
|
|
137
|
+
const fakeResults = toolUseIds.map(id => ({
|
|
138
|
+
type: 'tool_result', tool_use_id: id, content: '[interrupted]',
|
|
139
|
+
}));
|
|
140
|
+
history.splice(i + 1, 0, { role: 'user', content: fakeResults });
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
127
145
|
function createClaude(anthropicConfig, { personality, memory, userDir = OBOL_DIR, bridgeEnabled }) {
|
|
128
146
|
let client = createAnthropicClient(anthropicConfig);
|
|
129
147
|
|
|
@@ -151,7 +169,10 @@ function createClaude(anthropicConfig, { personality, memory, userDir = OBOL_DIR
|
|
|
151
169
|
if (!histories.has(chatId)) histories.set(chatId, []);
|
|
152
170
|
const history = histories.get(chatId);
|
|
153
171
|
|
|
154
|
-
|
|
172
|
+
const verbose = context.verbose || false;
|
|
173
|
+
if (verbose) context.verboseLog = [];
|
|
174
|
+
const vlog = (msg) => { if (verbose) context.verboseLog.push(msg); };
|
|
175
|
+
|
|
155
176
|
let memoryContext = '';
|
|
156
177
|
if (memory) {
|
|
157
178
|
try {
|
|
@@ -179,7 +200,8 @@ Model: Use "sonnet" for most things (chat, simple questions, quick tasks, single
|
|
|
179
200
|
if (jsonStr) decision = JSON.parse(jsonStr);
|
|
180
201
|
} catch {}
|
|
181
202
|
|
|
182
|
-
|
|
203
|
+
vlog(`[router] model=${decision.model || 'sonnet'} memory=${decision.need_memory || false}${decision.search_query ? ` query="${decision.search_query}"` : ''}`);
|
|
204
|
+
|
|
183
205
|
if (decision.model === 'opus') {
|
|
184
206
|
context._model = 'claude-opus-4-6';
|
|
185
207
|
}
|
|
@@ -187,11 +209,9 @@ Model: Use "sonnet" for most things (chat, simple questions, quick tasks, single
|
|
|
187
209
|
if (decision.need_memory) {
|
|
188
210
|
const query = decision.search_query || userMessage;
|
|
189
211
|
|
|
190
|
-
// Today's context + semantic search
|
|
191
212
|
const todayMemories = await memory.byDate('today', { limit: 3 });
|
|
192
213
|
const semanticMemories = await memory.search(query, { limit: 3, threshold: 0.5 });
|
|
193
214
|
|
|
194
|
-
// Dedupe by ID
|
|
195
215
|
const seen = new Set();
|
|
196
216
|
const combined = [];
|
|
197
217
|
for (const m of [...todayMemories, ...semanticMemories]) {
|
|
@@ -201,6 +221,8 @@ Model: Use "sonnet" for most things (chat, simple questions, quick tasks, single
|
|
|
201
221
|
}
|
|
202
222
|
}
|
|
203
223
|
|
|
224
|
+
vlog(`[memory] ${combined.length} memories found (${todayMemories.length} today, ${semanticMemories.length} semantic)`);
|
|
225
|
+
|
|
204
226
|
if (combined.length > 0) {
|
|
205
227
|
memoryContext = '\n\n[Relevant memories]\n' +
|
|
206
228
|
combined.map(m => `- [${m.category}] ${m.content}`).join('\n');
|
|
@@ -208,6 +230,7 @@ Model: Use "sonnet" for most things (chat, simple questions, quick tasks, single
|
|
|
208
230
|
}
|
|
209
231
|
} catch (e) {
|
|
210
232
|
console.error('[router] Memory/routing decision failed:', e.message);
|
|
233
|
+
vlog(`[router] ERROR: ${e.message}`);
|
|
211
234
|
}
|
|
212
235
|
}
|
|
213
236
|
|
|
@@ -227,6 +250,7 @@ Model: Use "sonnet" for most things (chat, simple questions, quick tasks, single
|
|
|
227
250
|
}
|
|
228
251
|
break;
|
|
229
252
|
}
|
|
253
|
+
repairHistory(history);
|
|
230
254
|
|
|
231
255
|
// Add user message with memory context
|
|
232
256
|
const enrichedMessage = memoryContext
|
|
@@ -241,8 +265,8 @@ Model: Use "sonnet" for most things (chat, simple questions, quick tasks, single
|
|
|
241
265
|
history.push({ role: 'user', content: enrichedMessage });
|
|
242
266
|
}
|
|
243
267
|
|
|
244
|
-
// Call Claude ā Haiku picks the model
|
|
245
268
|
const model = context._model || 'claude-sonnet-4-6';
|
|
269
|
+
vlog(`[model] ${model} | history=${history.length} msgs`);
|
|
246
270
|
const systemPrompt = baseSystemPrompt + `\nCurrent time: ${new Date().toISOString()}`;
|
|
247
271
|
let response = await client.messages.create({
|
|
248
272
|
model,
|
|
@@ -256,8 +280,15 @@ Model: Use "sonnet" for most things (chat, simple questions, quick tasks, single
|
|
|
256
280
|
while (response.stop_reason === 'tool_use') {
|
|
257
281
|
toolIterations++;
|
|
258
282
|
if (toolIterations > MAX_TOOL_ITERATIONS) {
|
|
259
|
-
|
|
260
|
-
history.push({ role: '
|
|
283
|
+
const bailoutContent = response.content;
|
|
284
|
+
history.push({ role: 'assistant', content: bailoutContent });
|
|
285
|
+
const bailoutResults = bailoutContent
|
|
286
|
+
.filter(b => b.type === 'tool_use')
|
|
287
|
+
.map(b => ({ type: 'tool_result', tool_use_id: b.id, content: '[max tool iterations reached]' }));
|
|
288
|
+
history.push({ role: 'user', content: [
|
|
289
|
+
...bailoutResults,
|
|
290
|
+
{ type: 'text', text: 'You have used too many tool calls. Please provide a final response now based on what you have so far.' },
|
|
291
|
+
] });
|
|
261
292
|
response = await client.messages.create({
|
|
262
293
|
model,
|
|
263
294
|
max_tokens: 4096,
|
|
@@ -273,6 +304,15 @@ Model: Use "sonnet" for most things (chat, simple questions, quick tasks, single
|
|
|
273
304
|
const toolResults = [];
|
|
274
305
|
for (const block of assistantContent) {
|
|
275
306
|
if (block.type === 'tool_use') {
|
|
307
|
+
const inputSummary = block.name === 'exec' ? block.input.command :
|
|
308
|
+
block.name === 'write_file' ? block.input.path :
|
|
309
|
+
block.name === 'read_file' ? block.input.path :
|
|
310
|
+
block.name === 'memory_search' ? block.input.query :
|
|
311
|
+
block.name === 'memory_add' ? `[${block.input.category || 'fact'}]` :
|
|
312
|
+
block.name === 'web_fetch' ? block.input.url :
|
|
313
|
+
block.name === 'background_task' ? block.input.task?.substring(0, 60) :
|
|
314
|
+
JSON.stringify(block.input).substring(0, 80);
|
|
315
|
+
vlog(`[tool] ${block.name}: ${inputSummary}`);
|
|
276
316
|
const result = await executeToolCall(block, memory, context);
|
|
277
317
|
toolResults.push({
|
|
278
318
|
type: 'tool_result',
|
|
@@ -293,11 +333,13 @@ Model: Use "sonnet" for most things (chat, simple questions, quick tasks, single
|
|
|
293
333
|
});
|
|
294
334
|
}
|
|
295
335
|
|
|
296
|
-
// Extract text response
|
|
297
336
|
const textBlocks = response.content.filter(b => b.type === 'text');
|
|
298
337
|
const replyText = textBlocks.map(b => b.text).join('\n');
|
|
299
338
|
|
|
300
|
-
|
|
339
|
+
if (response.usage) {
|
|
340
|
+
vlog(`[tokens] in=${response.usage.input_tokens} out=${response.usage.output_tokens}`);
|
|
341
|
+
}
|
|
342
|
+
|
|
301
343
|
history.push({ role: 'assistant', content: response.content });
|
|
302
344
|
|
|
303
345
|
return replyText;
|
|
@@ -618,6 +660,20 @@ function buildTools(memory, opts = {}) {
|
|
|
618
660
|
},
|
|
619
661
|
});
|
|
620
662
|
|
|
663
|
+
tools.push({
|
|
664
|
+
name: 'telegram_ask',
|
|
665
|
+
description: 'Send a message to the user with inline keyboard buttons and wait for their tap. Use for human-in-the-loop decisions: confirmations, approvals, action selection. Returns the label of the button the user pressed, or "timeout" if they don\'t respond within the timeout.',
|
|
666
|
+
input_schema: {
|
|
667
|
+
type: 'object',
|
|
668
|
+
properties: {
|
|
669
|
+
message: { type: 'string', description: 'Question or prompt to show the user' },
|
|
670
|
+
options: { type: 'array', items: { type: 'string' }, description: 'Button labels (2-6 options, keep each label short)' },
|
|
671
|
+
timeout: { type: 'number', description: 'Seconds to wait for response (default 60)' },
|
|
672
|
+
},
|
|
673
|
+
required: ['message', 'options'],
|
|
674
|
+
},
|
|
675
|
+
});
|
|
676
|
+
|
|
621
677
|
if (opts.bridgeEnabled) {
|
|
622
678
|
const { buildBridgeTool, buildBridgeTellTool } = require('./bridge');
|
|
623
679
|
tools.push(buildBridgeTool());
|
|
@@ -832,6 +888,11 @@ async function executeToolCall(toolUse, memory, context = {}) {
|
|
|
832
888
|
return `Sent: ${path.basename(filePath)}`;
|
|
833
889
|
}
|
|
834
890
|
|
|
891
|
+
case 'telegram_ask': {
|
|
892
|
+
if (!context.telegramAsk) return 'telegram_ask not available in this context.';
|
|
893
|
+
return await context.telegramAsk(input.message, input.options || [], input.timeout);
|
|
894
|
+
}
|
|
895
|
+
|
|
835
896
|
case 'bridge_ask': {
|
|
836
897
|
const { bridgeAsk } = require('./bridge');
|
|
837
898
|
return await bridgeAsk(input.question, context.userId, context.config, context._notifyFn, input.partner_id);
|
|
@@ -850,4 +911,7 @@ async function executeToolCall(toolUse, memory, context = {}) {
|
|
|
850
911
|
}
|
|
851
912
|
}
|
|
852
913
|
|
|
853
|
-
|
|
914
|
+
function getMaxToolIterations() { return MAX_TOOL_ITERATIONS; }
|
|
915
|
+
function setMaxToolIterations(n) { MAX_TOOL_ITERATIONS = n; }
|
|
916
|
+
|
|
917
|
+
module.exports = { createClaude, createAnthropicClient, getMaxToolIterations, setMaxToolIterations };
|
package/src/defaults/AGENTS.md
CHANGED
|
@@ -44,6 +44,16 @@ Users can also manage secrets via Telegram: `/secret set <key> <value>` (message
|
|
|
44
44
|
### Send File (`send_file`)
|
|
45
45
|
Send a file back to the user via Telegram. Use after generating PDFs, images, documents, or any file the user requested.
|
|
46
46
|
|
|
47
|
+
### Ask User (`telegram_ask`)
|
|
48
|
+
Send a message with inline keyboard buttons and wait for the user to tap one. Use for human-in-the-loop decisions before taking action.
|
|
49
|
+
|
|
50
|
+
Examples:
|
|
51
|
+
- After listing emails: `telegram_ask({message: "Open any of these?", options: ["#1 Google", "#2 LinkedIn", "#3 DeepLearning", "None"]})`
|
|
52
|
+
- Before sending a reply: `telegram_ask({message: "Send this reply?", options: ["Send it", "Edit first", "Cancel"]})`
|
|
53
|
+
- Before an irreversible action: `telegram_ask({message: "Archive all read emails?", options: ["Yes", "No"]})`
|
|
54
|
+
|
|
55
|
+
Returns the tapped button label, or `"timeout"` if the user doesn't respond within the timeout (default 60s).
|
|
56
|
+
|
|
47
57
|
### Bridge (`bridge_ask`, `bridge_tell`)
|
|
48
58
|
Only available if bridge is enabled. Communicate with partner's AI agent.
|
|
49
59
|
|
|
@@ -138,6 +148,29 @@ Use `background_task` when a request will take multiple steps:
|
|
|
138
148
|
|
|
139
149
|
Pattern: acknowledge immediately ("On it"), spawn the task, let it work in the background.
|
|
140
150
|
|
|
151
|
+
## Telegram Formatting
|
|
152
|
+
|
|
153
|
+
You communicate via Telegram. Format responses for mobile readability.
|
|
154
|
+
|
|
155
|
+
**Never use markdown tables** ā pipe-syntax tables do not render in Telegram. Use numbered lists instead.
|
|
156
|
+
|
|
157
|
+
**Email/inbox lists** ā use this pattern:
|
|
158
|
+
```
|
|
159
|
+
š¬ *Inbox (10)*
|
|
160
|
+
|
|
161
|
+
1\. *Google* ā Security alert `22:58`
|
|
162
|
+
2\. *LinkedIn* ā Matthew Chittle wants to connect `21:31`
|
|
163
|
+
3\. *DeepLearning\.AI* ā AI Dev 26 Ć SF speakers `13:20`
|
|
164
|
+
4\. *LinkedIn Jobs* ā Project Manager / TPM roles `17:32`
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
**Copyable values** (email addresses, URLs, API keys, commands) ā wrap in backtick code spans:
|
|
168
|
+
`user@example.com`, `https://example.com`, `npm install foo`
|
|
169
|
+
|
|
170
|
+
**Human-in-the-loop** ā after listing emails or before acting, use `telegram_ask` to offer inline buttons rather than asking the user to type a reply.
|
|
171
|
+
|
|
172
|
+
**Keep lines short** ā Telegram wraps long lines poorly on mobile. Break at natural points.
|
|
173
|
+
|
|
141
174
|
## Communication Style
|
|
142
175
|
|
|
143
176
|
- Be direct and helpful
|
package/src/telegram.js
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
const path = require('path');
|
|
2
|
-
const { Bot, GrammyError, HttpError } = require('grammy');
|
|
2
|
+
const { Bot, GrammyError, HttpError, InlineKeyboard } = require('grammy');
|
|
3
3
|
const { loadConfig } = require('./config');
|
|
4
4
|
const { evolve, loadEvolutionState } = require('./evolve');
|
|
5
5
|
const { getTenant } = require('./tenant');
|
|
6
6
|
const { loadTraits, saveTraits, DEFAULT_TRAITS } = require('./personality');
|
|
7
7
|
const media = require('./media');
|
|
8
8
|
const credentials = require('./credentials');
|
|
9
|
+
const { getMaxToolIterations, setMaxToolIterations } = require('./claude');
|
|
9
10
|
|
|
10
11
|
const RATE_LIMIT_MS = 3000;
|
|
11
12
|
const SPAM_THRESHOLD = 5;
|
|
@@ -23,6 +24,31 @@ function createBot(telegramConfig, config) {
|
|
|
23
24
|
const bot = new Bot(telegramConfig.token);
|
|
24
25
|
const allowedUsers = new Set(telegramConfig.allowedUsers || []);
|
|
25
26
|
const rateLimits = new Map();
|
|
27
|
+
const pendingAsks = new Map();
|
|
28
|
+
let askIdCounter = 0;
|
|
29
|
+
|
|
30
|
+
function createAsk(ctx, message, options, timeoutSecs = 60) {
|
|
31
|
+
return new Promise((resolve) => {
|
|
32
|
+
const askId = ++askIdCounter;
|
|
33
|
+
const keyboard = new InlineKeyboard();
|
|
34
|
+
options.forEach((opt, i) => {
|
|
35
|
+
keyboard.text(opt, `ask:${askId}:${i}`);
|
|
36
|
+
if ((i + 1) % 3 === 0 && i < options.length - 1) keyboard.row();
|
|
37
|
+
});
|
|
38
|
+
const timer = setTimeout(() => {
|
|
39
|
+
if (pendingAsks.has(askId)) {
|
|
40
|
+
pendingAsks.delete(askId);
|
|
41
|
+
resolve('timeout');
|
|
42
|
+
}
|
|
43
|
+
}, timeoutSecs * 1000);
|
|
44
|
+
pendingAsks.set(askId, { resolve, options, timer });
|
|
45
|
+
ctx.reply(message, { parse_mode: 'Markdown', reply_markup: keyboard }).catch(() => {
|
|
46
|
+
clearTimeout(timer);
|
|
47
|
+
pendingAsks.delete(askId);
|
|
48
|
+
resolve('error');
|
|
49
|
+
});
|
|
50
|
+
});
|
|
51
|
+
}
|
|
26
52
|
|
|
27
53
|
const _rateLimitCleanup = setInterval(() => {
|
|
28
54
|
const now = Date.now();
|
|
@@ -51,6 +77,8 @@ function createBot(telegramConfig, config) {
|
|
|
51
77
|
{ command: 'traits', description: 'View or adjust personality traits' },
|
|
52
78
|
{ command: 'secret', description: 'Manage per-user secrets' },
|
|
53
79
|
{ command: 'evolution', description: 'Evolution progress' },
|
|
80
|
+
{ command: 'verbose', description: 'Toggle verbose mode on/off' },
|
|
81
|
+
{ command: 'toolimit', description: 'View or set max tool iterations per message' },
|
|
54
82
|
{ command: 'help', description: 'Show available commands' },
|
|
55
83
|
]).catch(() => {});
|
|
56
84
|
|
|
@@ -102,6 +130,7 @@ function createBot(telegramConfig, config) {
|
|
|
102
130
|
text += `ā±ļø Uptime: ${h}h ${m}m\n`;
|
|
103
131
|
text += `š¾ Memory: ${mem}MB\n`;
|
|
104
132
|
text += `ā” Tasks: ${running.length} running\n`;
|
|
133
|
+
text += `š§ Tool limit: ${getMaxToolIterations()}\n`;
|
|
105
134
|
|
|
106
135
|
if (tenant.memory) {
|
|
107
136
|
const stats = await tenant.memory.stats().catch(() => null);
|
|
@@ -338,9 +367,38 @@ Your message is deleted immediately when using /secret set to keep credentials o
|
|
|
338
367
|
/status ā Bot status and uptime
|
|
339
368
|
/backup ā Trigger GitHub backup
|
|
340
369
|
/clean ā Audit workspace
|
|
370
|
+
/verbose ā Toggle verbose mode on/off
|
|
371
|
+
/toolimit ā View or set max tool iterations
|
|
341
372
|
/help ā This message`);
|
|
342
373
|
});
|
|
343
374
|
|
|
375
|
+
bot.command('verbose', async (ctx) => {
|
|
376
|
+
if (!ctx.from) return;
|
|
377
|
+
const tenant = await getTenant(ctx.from.id, config);
|
|
378
|
+
tenant.verbose = !tenant.verbose;
|
|
379
|
+
await ctx.reply(tenant.verbose ? 'š Verbose mode ON' : 'š Verbose mode OFF');
|
|
380
|
+
});
|
|
381
|
+
|
|
382
|
+
bot.command('toolimit', async (ctx) => {
|
|
383
|
+
if (!ctx.from) return;
|
|
384
|
+
const args = ctx.message.text.split(' ').slice(1);
|
|
385
|
+
const current = getMaxToolIterations();
|
|
386
|
+
|
|
387
|
+
if (!args[0]) {
|
|
388
|
+
await ctx.reply(`š§ Max tool iterations: ${current}\n\nThis limits how many tool calls OBOL can make per message. Higher = more complex tasks, but slower responses.\n\nSet: /toolimit <number>\nExample: /toolimit 50`);
|
|
389
|
+
return;
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
const value = parseInt(args[0], 10);
|
|
393
|
+
if (isNaN(value) || value < 1 || value > 500) {
|
|
394
|
+
await ctx.reply(`Invalid value: "${args[0]}"\n\nMust be a number between 1 and 500.\nCurrent: ${current}\n\nExample: /toolimit 50`);
|
|
395
|
+
return;
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
setMaxToolIterations(value);
|
|
399
|
+
await ctx.reply(`š§ Max tool iterations set to ${value}`);
|
|
400
|
+
});
|
|
401
|
+
|
|
344
402
|
function checkRateLimit(userId) {
|
|
345
403
|
const now = Date.now();
|
|
346
404
|
const userLimit = rateLimits.get(userId) || { lastMessage: 0, spamCount: 0, cooldownUntil: 0 };
|
|
@@ -392,7 +450,7 @@ Your message is deleted immediately when using /secret set to keep credentials o
|
|
|
392
450
|
try {
|
|
393
451
|
tenant.messageLog?.log(ctx.chat.id, 'user', userMessage);
|
|
394
452
|
|
|
395
|
-
const
|
|
453
|
+
const chatContext = {
|
|
396
454
|
userId,
|
|
397
455
|
userName,
|
|
398
456
|
chatId: ctx.chat.id,
|
|
@@ -400,14 +458,24 @@ Your message is deleted immediately when using /secret set to keep credentials o
|
|
|
400
458
|
ctx,
|
|
401
459
|
claude: tenant.claude,
|
|
402
460
|
config,
|
|
461
|
+
verbose: tenant.verbose,
|
|
462
|
+
telegramAsk: (message, options, timeout) => createAsk(ctx, message, options, timeout),
|
|
403
463
|
_notifyFn: (targetUserId, message) => {
|
|
404
464
|
if (!allowedUsers.has(targetUserId)) throw new Error('Cannot notify user outside allowed list');
|
|
405
465
|
return bot.api.sendMessage(targetUserId, message);
|
|
406
466
|
},
|
|
407
|
-
}
|
|
467
|
+
};
|
|
468
|
+
const response = await tenant.claude.chat(userMessage, chatContext);
|
|
408
469
|
|
|
409
470
|
tenant.messageLog?.log(ctx.chat.id, 'assistant', response);
|
|
410
471
|
|
|
472
|
+
if (tenant.verbose && chatContext.verboseLog?.length) {
|
|
473
|
+
const verboseText = '```\n' + chatContext.verboseLog.join('\n') + '\n```';
|
|
474
|
+
await ctx.reply(verboseText, { parse_mode: 'Markdown' }).catch(() =>
|
|
475
|
+
ctx.reply(verboseText).catch(() => {})
|
|
476
|
+
);
|
|
477
|
+
}
|
|
478
|
+
|
|
411
479
|
if (tenant.messageLog?._evolutionReady) {
|
|
412
480
|
tenant.messageLog._evolutionReady = false;
|
|
413
481
|
setImmediate(async () => {
|
|
@@ -525,7 +593,7 @@ Your message is deleted immediately when using /secret set to keep credentials o
|
|
|
525
593
|
if (media.isImage(fileInfo)) {
|
|
526
594
|
const imageBlock = media.bufferToImageBlock(buffer, fileInfo.mimeType);
|
|
527
595
|
const prompt = caption || 'The user sent this image. Describe what you see and respond naturally.';
|
|
528
|
-
const
|
|
596
|
+
const mediaChatCtx = {
|
|
529
597
|
userId,
|
|
530
598
|
userName: ctx.from.first_name || 'User',
|
|
531
599
|
chatId: ctx.chat.id,
|
|
@@ -533,16 +601,23 @@ Your message is deleted immediately when using /secret set to keep credentials o
|
|
|
533
601
|
ctx,
|
|
534
602
|
claude: tenant.claude,
|
|
535
603
|
config,
|
|
604
|
+
verbose: tenant.verbose,
|
|
536
605
|
images: [imageBlock],
|
|
537
606
|
_notifyFn: (targetUserId, message) => {
|
|
538
607
|
if (!allowedUsers.has(targetUserId)) throw new Error('Cannot notify user outside allowed list');
|
|
539
608
|
return bot.api.sendMessage(targetUserId, message);
|
|
540
609
|
},
|
|
541
|
-
}
|
|
610
|
+
};
|
|
611
|
+
const response = await tenant.claude.chat(prompt, mediaChatCtx);
|
|
542
612
|
|
|
543
613
|
tenant.messageLog?.log(ctx.chat.id, 'user', `[${fileInfo.mediaType}] ${caption || filename}`);
|
|
544
614
|
tenant.messageLog?.log(ctx.chat.id, 'assistant', response);
|
|
545
615
|
|
|
616
|
+
if (tenant.verbose && mediaChatCtx.verboseLog?.length) {
|
|
617
|
+
const verboseText = '```\n' + mediaChatCtx.verboseLog.join('\n') + '\n```';
|
|
618
|
+
await ctx.reply(verboseText, { parse_mode: 'Markdown' }).catch(() => ctx.reply(verboseText).catch(() => {}));
|
|
619
|
+
}
|
|
620
|
+
|
|
546
621
|
stopTyping();
|
|
547
622
|
if (response.length > 4096) {
|
|
548
623
|
for (const chunk of splitMessage(response, 4096)) {
|
|
@@ -553,7 +628,7 @@ Your message is deleted immediately when using /secret set to keep credentials o
|
|
|
553
628
|
}
|
|
554
629
|
} else if (caption) {
|
|
555
630
|
const contextMsg = `[User sent a ${fileInfo.mediaType}: ${filename}] ${caption}`;
|
|
556
|
-
const
|
|
631
|
+
const mediaCaptionCtx = {
|
|
557
632
|
userId,
|
|
558
633
|
userName: ctx.from.first_name || 'User',
|
|
559
634
|
chatId: ctx.chat.id,
|
|
@@ -561,15 +636,22 @@ Your message is deleted immediately when using /secret set to keep credentials o
|
|
|
561
636
|
ctx,
|
|
562
637
|
claude: tenant.claude,
|
|
563
638
|
config,
|
|
639
|
+
verbose: tenant.verbose,
|
|
564
640
|
_notifyFn: (targetUserId, message) => {
|
|
565
641
|
if (!allowedUsers.has(targetUserId)) throw new Error('Cannot notify user outside allowed list');
|
|
566
642
|
return bot.api.sendMessage(targetUserId, message);
|
|
567
643
|
},
|
|
568
|
-
}
|
|
644
|
+
};
|
|
645
|
+
const response = await tenant.claude.chat(contextMsg, mediaCaptionCtx);
|
|
569
646
|
|
|
570
647
|
tenant.messageLog?.log(ctx.chat.id, 'user', contextMsg);
|
|
571
648
|
tenant.messageLog?.log(ctx.chat.id, 'assistant', response);
|
|
572
649
|
|
|
650
|
+
if (tenant.verbose && mediaCaptionCtx.verboseLog?.length) {
|
|
651
|
+
const verboseText = '```\n' + mediaCaptionCtx.verboseLog.join('\n') + '\n```';
|
|
652
|
+
await ctx.reply(verboseText, { parse_mode: 'Markdown' }).catch(() => ctx.reply(verboseText).catch(() => {}));
|
|
653
|
+
}
|
|
654
|
+
|
|
573
655
|
stopTyping();
|
|
574
656
|
if (response.length > 4096) {
|
|
575
657
|
for (const chunk of splitMessage(response, 4096)) {
|
|
@@ -598,6 +680,22 @@ Your message is deleted immediately when using /secret set to keep credentials o
|
|
|
598
680
|
bot.on('message:animation', handleMedia);
|
|
599
681
|
bot.on('message:video_note', handleMedia);
|
|
600
682
|
|
|
683
|
+
bot.on('callback_query:data', async (ctx) => {
|
|
684
|
+
const data = ctx.callbackQuery.data;
|
|
685
|
+
if (!data.startsWith('ask:')) return ctx.answerCallbackQuery();
|
|
686
|
+
const parts = data.split(':');
|
|
687
|
+
const askId = parseInt(parts[1]);
|
|
688
|
+
const optIdx = parseInt(parts[2]);
|
|
689
|
+
const pending = pendingAsks.get(askId);
|
|
690
|
+
if (!pending) return ctx.answerCallbackQuery({ text: 'Expired' });
|
|
691
|
+
const selected = pending.options[optIdx];
|
|
692
|
+
clearTimeout(pending.timer);
|
|
693
|
+
pendingAsks.delete(askId);
|
|
694
|
+
await ctx.editMessageText(`${ctx.callbackQuery.message.text}\n\nā _${selected}_`, { parse_mode: 'Markdown' }).catch(() => {});
|
|
695
|
+
await ctx.answerCallbackQuery({ text: selected });
|
|
696
|
+
pending.resolve(selected);
|
|
697
|
+
});
|
|
698
|
+
|
|
601
699
|
bot.catch((err) => {
|
|
602
700
|
const ctx = err.ctx;
|
|
603
701
|
const e = err.error;
|