fix(telegram): simplify command responses and enrich status output (#565)
Co-authored-by: Letta Code <noreply@letta.com>
This commit is contained in:
@@ -26,6 +26,16 @@ import { resolveDailyLimits, checkDailyLimit, type GroupModeConfig } from './gro
|
|||||||
import { createLogger } from '../logger.js';
|
import { createLogger } from '../logger.js';
|
||||||
|
|
||||||
const log = createLogger('Telegram');
|
const log = createLogger('Telegram');
|
||||||
|
const KNOWN_TELEGRAM_COMMANDS = new Set([
|
||||||
|
'status',
|
||||||
|
'model',
|
||||||
|
'heartbeat',
|
||||||
|
'reset',
|
||||||
|
'cancel',
|
||||||
|
'setconv',
|
||||||
|
'help',
|
||||||
|
'start',
|
||||||
|
]);
|
||||||
|
|
||||||
function getTelegramErrorReason(err: unknown): string {
|
function getTelegramErrorReason(err: unknown): string {
|
||||||
if (err && typeof err === 'object') {
|
if (err && typeof err === 'object') {
|
||||||
@@ -244,6 +254,9 @@ export class TelegramAdapter implements ChannelAdapter {
|
|||||||
"*LettaBot* - AI assistant with persistent memory\n\n" +
|
"*LettaBot* - AI assistant with persistent memory\n\n" +
|
||||||
"*Commands:*\n" +
|
"*Commands:*\n" +
|
||||||
"/status - Show current status\n" +
|
"/status - Show current status\n" +
|
||||||
|
"/model - Show current model and recommendations\n" +
|
||||||
|
"/reset - Reset conversation\n" +
|
||||||
|
"/cancel - Cancel active run\n" +
|
||||||
"/help - Show this message\n\n" +
|
"/help - Show this message\n\n" +
|
||||||
"Just send me a message to get started!",
|
"Just send me a message to get started!",
|
||||||
{ parse_mode: 'Markdown' }
|
{ parse_mode: 'Markdown' }
|
||||||
@@ -254,7 +267,15 @@ export class TelegramAdapter implements ChannelAdapter {
|
|||||||
this.bot.command('status', async (ctx) => {
|
this.bot.command('status', async (ctx) => {
|
||||||
if (this.onCommand) {
|
if (this.onCommand) {
|
||||||
const result = await this.onCommand('status', String(ctx.chat.id));
|
const result = await this.onCommand('status', String(ctx.chat.id));
|
||||||
await ctx.reply(result || 'No status available');
|
const replyToMessageId =
|
||||||
|
'message' in ctx && ctx.message
|
||||||
|
? String(ctx.message.message_id)
|
||||||
|
: undefined;
|
||||||
|
await this.sendMessage({
|
||||||
|
chatId: String(ctx.chat.id),
|
||||||
|
text: result || 'No status available',
|
||||||
|
replyToMessageId,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -269,14 +290,32 @@ export class TelegramAdapter implements ChannelAdapter {
|
|||||||
this.bot.command('reset', async (ctx) => {
|
this.bot.command('reset', async (ctx) => {
|
||||||
if (this.onCommand) {
|
if (this.onCommand) {
|
||||||
const result = await this.onCommand('reset', String(ctx.chat.id));
|
const result = await this.onCommand('reset', String(ctx.chat.id));
|
||||||
await ctx.reply(result || 'Reset complete');
|
const replyToMessageId =
|
||||||
|
'message' in ctx && ctx.message
|
||||||
|
? String(ctx.message.message_id)
|
||||||
|
: undefined;
|
||||||
|
await this.sendMessage({
|
||||||
|
chatId: String(ctx.chat.id),
|
||||||
|
text: result || 'Reset complete',
|
||||||
|
replyToMessageId,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
this.bot.command('cancel', async (ctx) => {
|
this.bot.command('cancel', async (ctx) => {
|
||||||
if (this.onCommand) {
|
if (this.onCommand) {
|
||||||
const result = await this.onCommand('cancel', String(ctx.chat.id));
|
const result = await this.onCommand('cancel', String(ctx.chat.id));
|
||||||
if (result) await ctx.reply(result);
|
if (result) {
|
||||||
|
const replyToMessageId =
|
||||||
|
'message' in ctx && ctx.message
|
||||||
|
? String(ctx.message.message_id)
|
||||||
|
: undefined;
|
||||||
|
await this.sendMessage({
|
||||||
|
chatId: String(ctx.chat.id),
|
||||||
|
text: result,
|
||||||
|
replyToMessageId,
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -285,7 +324,15 @@ export class TelegramAdapter implements ChannelAdapter {
|
|||||||
if (this.onCommand) {
|
if (this.onCommand) {
|
||||||
const args = ctx.match?.trim() || undefined;
|
const args = ctx.match?.trim() || undefined;
|
||||||
const result = await this.onCommand('model', String(ctx.chat.id), args);
|
const result = await this.onCommand('model', String(ctx.chat.id), args);
|
||||||
await ctx.reply(result || 'No model info available');
|
const replyToMessageId =
|
||||||
|
'message' in ctx && ctx.message
|
||||||
|
? String(ctx.message.message_id)
|
||||||
|
: undefined;
|
||||||
|
await this.sendMessage({
|
||||||
|
chatId: String(ctx.chat.id),
|
||||||
|
text: result || 'No model info available',
|
||||||
|
replyToMessageId,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -294,7 +341,15 @@ export class TelegramAdapter implements ChannelAdapter {
|
|||||||
if (this.onCommand) {
|
if (this.onCommand) {
|
||||||
const args = ctx.match?.trim() || undefined;
|
const args = ctx.match?.trim() || undefined;
|
||||||
const result = await this.onCommand('setconv', String(ctx.chat.id), args);
|
const result = await this.onCommand('setconv', String(ctx.chat.id), args);
|
||||||
await ctx.reply(result || 'Failed to set conversation');
|
const replyToMessageId =
|
||||||
|
'message' in ctx && ctx.message
|
||||||
|
? String(ctx.message.message_id)
|
||||||
|
: undefined;
|
||||||
|
await this.sendMessage({
|
||||||
|
chatId: String(ctx.chat.id),
|
||||||
|
text: result || 'Failed to set conversation',
|
||||||
|
replyToMessageId,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -305,7 +360,18 @@ export class TelegramAdapter implements ChannelAdapter {
|
|||||||
const text = ctx.message.text;
|
const text = ctx.message.text;
|
||||||
|
|
||||||
if (!userId) return;
|
if (!userId) return;
|
||||||
if (text.startsWith('/')) return; // Skip other commands
|
if (text.startsWith('/')) {
|
||||||
|
const commandToken = text.slice(1).trim().split(/\s+/)[0] || '';
|
||||||
|
const commandName = commandToken.toLowerCase().split('@')[0];
|
||||||
|
if (!KNOWN_TELEGRAM_COMMANDS.has(commandName)) {
|
||||||
|
await this.sendMessage({
|
||||||
|
chatId: String(chatId),
|
||||||
|
text: `Unknown command: /${commandName || '(empty)'}\nTry /help.`,
|
||||||
|
replyToMessageId: String(ctx.message.message_id),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// Group gating (runs AFTER pairing middleware)
|
// Group gating (runs AFTER pairing middleware)
|
||||||
const gating = this.applyGroupGating(ctx);
|
const gating = this.applyGroupGating(ctx);
|
||||||
|
|||||||
80
src/core/bot.status.test.ts
Normal file
80
src/core/bot.status.test.ts
Normal file
@@ -0,0 +1,80 @@
|
|||||||
|
import { mkdtempSync, rmSync, writeFileSync } from 'node:fs';
|
||||||
|
import { tmpdir } from 'node:os';
|
||||||
|
import { join } from 'node:path';
|
||||||
|
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
|
||||||
|
import { MockChannelAdapter } from '../test/mock-channel.js';
|
||||||
|
import { LettaBot } from './bot.js';
|
||||||
|
|
||||||
|
describe('LettaBot /status command', () => {
|
||||||
|
let dataDir: string;
|
||||||
|
let workingDir: string;
|
||||||
|
const originalDataDir = process.env.DATA_DIR;
|
||||||
|
const originalBaseUrl = process.env.LETTA_BASE_URL;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
dataDir = mkdtempSync(join(tmpdir(), 'lettabot-data-'));
|
||||||
|
workingDir = mkdtempSync(join(tmpdir(), 'lettabot-work-'));
|
||||||
|
|
||||||
|
process.env.DATA_DIR = dataDir;
|
||||||
|
delete process.env.LETTA_BASE_URL;
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
if (originalDataDir === undefined) {
|
||||||
|
delete process.env.DATA_DIR;
|
||||||
|
} else {
|
||||||
|
process.env.DATA_DIR = originalDataDir;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (originalBaseUrl === undefined) {
|
||||||
|
delete process.env.LETTA_BASE_URL;
|
||||||
|
} else {
|
||||||
|
process.env.LETTA_BASE_URL = originalBaseUrl;
|
||||||
|
}
|
||||||
|
|
||||||
|
rmSync(dataDir, { recursive: true, force: true });
|
||||||
|
rmSync(workingDir, { recursive: true, force: true });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('includes conversation and runtime fields', async () => {
|
||||||
|
writeFileSync(
|
||||||
|
join(dataDir, 'lettabot-agent.json'),
|
||||||
|
JSON.stringify(
|
||||||
|
{
|
||||||
|
version: 2,
|
||||||
|
agents: {
|
||||||
|
LettaBot: {
|
||||||
|
agentId: 'agent-test-123',
|
||||||
|
conversationId: 'conv-shared-123',
|
||||||
|
conversations: {
|
||||||
|
telegram: 'conv-tg-1',
|
||||||
|
},
|
||||||
|
createdAt: '2026-01-01T00:00:00.000Z',
|
||||||
|
lastUsedAt: '2026-01-01T00:00:01.000Z',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
null,
|
||||||
|
2,
|
||||||
|
),
|
||||||
|
'utf-8',
|
||||||
|
);
|
||||||
|
|
||||||
|
const bot = new LettaBot({
|
||||||
|
workingDir,
|
||||||
|
allowedTools: [],
|
||||||
|
memfs: true,
|
||||||
|
});
|
||||||
|
const adapter = new MockChannelAdapter();
|
||||||
|
bot.registerChannel(adapter);
|
||||||
|
|
||||||
|
const response = await adapter.simulateMessage('/status');
|
||||||
|
|
||||||
|
expect(response).toContain('Agent ID: `agent-test-123`');
|
||||||
|
expect(response).toContain('Conversation ID: `conv-shared-123`');
|
||||||
|
expect(response).toContain('Conversation keys: telegram');
|
||||||
|
expect(response).toContain('Memfs: on');
|
||||||
|
expect(response).toContain('Server: https://api.letta.com');
|
||||||
|
expect(response).toContain('Channels: mock');
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -672,12 +672,27 @@ export class LettaBot implements AgentSession {
|
|||||||
switch (command) {
|
switch (command) {
|
||||||
case 'status': {
|
case 'status': {
|
||||||
const info = this.store.getInfo();
|
const info = this.store.getInfo();
|
||||||
|
const memfsState = this.config.memfs === true
|
||||||
|
? 'on'
|
||||||
|
: this.config.memfs === false
|
||||||
|
? 'off'
|
||||||
|
: 'unknown';
|
||||||
|
const serverUrl = info.baseUrl || process.env.LETTA_BASE_URL || 'https://api.letta.com';
|
||||||
|
const conversationId = info.conversationId || '(none)';
|
||||||
|
const conversationKeys = info.conversations
|
||||||
|
? Object.keys(info.conversations).sort()
|
||||||
|
: [];
|
||||||
|
const channels = Array.from(this.channels.keys());
|
||||||
const lines = [
|
const lines = [
|
||||||
`*Status*`,
|
`*Status*`,
|
||||||
`Agent ID: \`${info.agentId || '(none)'}\``,
|
`Agent ID: \`${info.agentId || '(none)'}\``,
|
||||||
|
`Conversation ID: \`${conversationId}\``,
|
||||||
|
`Conversation keys: ${conversationKeys.length > 0 ? conversationKeys.join(', ') : '(none)'}`,
|
||||||
|
`Memfs: ${memfsState}`,
|
||||||
|
`Server: ${serverUrl}`,
|
||||||
`Created: ${info.createdAt || 'N/A'}`,
|
`Created: ${info.createdAt || 'N/A'}`,
|
||||||
`Last used: ${info.lastUsedAt || 'N/A'}`,
|
`Last used: ${info.lastUsedAt || 'N/A'}`,
|
||||||
`Channels: ${Array.from(this.channels.keys()).join(', ')}`,
|
`Channels: ${channels.length > 0 ? channels.join(', ') : '(none)'}`,
|
||||||
];
|
];
|
||||||
return lines.join('\n');
|
return lines.join('\n');
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user