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';
|
||||
|
||||
const log = createLogger('Telegram');
|
||||
const KNOWN_TELEGRAM_COMMANDS = new Set([
|
||||
'status',
|
||||
'model',
|
||||
'heartbeat',
|
||||
'reset',
|
||||
'cancel',
|
||||
'setconv',
|
||||
'help',
|
||||
'start',
|
||||
]);
|
||||
|
||||
function getTelegramErrorReason(err: unknown): string {
|
||||
if (err && typeof err === 'object') {
|
||||
@@ -244,6 +254,9 @@ export class TelegramAdapter implements ChannelAdapter {
|
||||
"*LettaBot* - AI assistant with persistent memory\n\n" +
|
||||
"*Commands:*\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" +
|
||||
"Just send me a message to get started!",
|
||||
{ parse_mode: 'Markdown' }
|
||||
@@ -254,7 +267,15 @@ export class TelegramAdapter implements ChannelAdapter {
|
||||
this.bot.command('status', async (ctx) => {
|
||||
if (this.onCommand) {
|
||||
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) => {
|
||||
if (this.onCommand) {
|
||||
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) => {
|
||||
if (this.onCommand) {
|
||||
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) {
|
||||
const args = ctx.match?.trim() || undefined;
|
||||
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) {
|
||||
const args = ctx.match?.trim() || undefined;
|
||||
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;
|
||||
|
||||
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)
|
||||
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) {
|
||||
case 'status': {
|
||||
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 = [
|
||||
`*Status*`,
|
||||
`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'}`,
|
||||
`Last used: ${info.lastUsedAt || 'N/A'}`,
|
||||
`Channels: ${Array.from(this.channels.keys()).join(', ')}`,
|
||||
`Channels: ${channels.length > 0 ? channels.join(', ') : '(none)'}`,
|
||||
];
|
||||
return lines.join('\n');
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user