feat: Add intelligent auto-router and enhanced integrations
- Add intelligent-router.sh hook for automatic agent routing - Add AUTO-TRIGGER-SUMMARY.md documentation - Add FINAL-INTEGRATION-SUMMARY.md documentation - Complete Prometheus integration (6 commands + 4 tools) - Complete Dexto integration (12 commands + 5 tools) - Enhanced Ralph with access to all agents - Fix /clawd command (removed disable-model-invocation) - Update hooks.json to v5 with intelligent routing - 291 total skills now available - All 21 commands with automatic routing 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
583
dexto/examples/telegram-bot/bot.ts
Normal file
583
dexto/examples/telegram-bot/bot.ts
Normal file
@@ -0,0 +1,583 @@
|
||||
#!/usr/bin/env node
|
||||
import 'dotenv/config';
|
||||
import { Bot, InlineKeyboard } from 'grammy';
|
||||
|
||||
// Type for inline query result article (matching what we create)
|
||||
type InlineQueryResultArticle = {
|
||||
type: 'article';
|
||||
id: string;
|
||||
title: string;
|
||||
input_message_content: { message_text: string };
|
||||
description: string;
|
||||
};
|
||||
import * as https from 'https';
|
||||
import { DextoAgent, logger } from '@dexto/core';
|
||||
|
||||
const token = process.env.TELEGRAM_BOT_TOKEN;
|
||||
|
||||
// Concurrency cap and debounce cache for inline queries
|
||||
const MAX_CONCURRENT_INLINE_QUERIES = process.env.TELEGRAM_INLINE_QUERY_CONCURRENCY
|
||||
? Number(process.env.TELEGRAM_INLINE_QUERY_CONCURRENCY)
|
||||
: 5;
|
||||
let currentInlineQueries = 0;
|
||||
const INLINE_QUERY_DEBOUNCE_INTERVAL = 2000; // ms
|
||||
const INLINE_QUERY_CACHE_MAX_SIZE = 1000;
|
||||
const inlineQueryCache: Record<string, { timestamp: number; results: InlineQueryResultArticle[] }> =
|
||||
{};
|
||||
|
||||
// Cleanup old cache entries to prevent unbounded growth
|
||||
function cleanupInlineQueryCache(): void {
|
||||
const now = Date.now();
|
||||
const keys = Object.keys(inlineQueryCache);
|
||||
|
||||
// Remove expired entries
|
||||
for (const key of keys) {
|
||||
if (now - inlineQueryCache[key]!.timestamp > INLINE_QUERY_DEBOUNCE_INTERVAL) {
|
||||
delete inlineQueryCache[key];
|
||||
}
|
||||
}
|
||||
|
||||
// If still over limit, remove oldest entries
|
||||
const remainingKeys = Object.keys(inlineQueryCache);
|
||||
if (remainingKeys.length > INLINE_QUERY_CACHE_MAX_SIZE) {
|
||||
const sortedKeys = remainingKeys.sort(
|
||||
(a, b) => inlineQueryCache[a]!.timestamp - inlineQueryCache[b]!.timestamp
|
||||
);
|
||||
const toRemove = sortedKeys.slice(0, remainingKeys.length - INLINE_QUERY_CACHE_MAX_SIZE);
|
||||
for (const key of toRemove) {
|
||||
delete inlineQueryCache[key];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Cache for prompts loaded from DextoAgent
|
||||
let cachedPrompts: Record<string, import('@dexto/core').PromptInfo> = {};
|
||||
|
||||
// Helper to detect MIME type from file extension
|
||||
function getMimeTypeFromPath(filePath: string): string {
|
||||
const ext = filePath.split('.').pop()?.toLowerCase() || '';
|
||||
const mimeTypes: Record<string, string> = {
|
||||
jpg: 'image/jpeg',
|
||||
jpeg: 'image/jpeg',
|
||||
png: 'image/png',
|
||||
gif: 'image/gif',
|
||||
webp: 'image/webp',
|
||||
ogg: 'audio/ogg',
|
||||
mp3: 'audio/mpeg',
|
||||
wav: 'audio/wav',
|
||||
m4a: 'audio/mp4',
|
||||
};
|
||||
return mimeTypes[ext] || 'application/octet-stream';
|
||||
}
|
||||
|
||||
// Helper to download a file URL and convert it to base64
|
||||
async function downloadFileAsBase64(
|
||||
fileUrl: string,
|
||||
filePath?: string
|
||||
): Promise<{ base64: string; mimeType: string }> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const MAX_BYTES = 5 * 1024 * 1024; // 5 MB hard cap
|
||||
let downloadedBytes = 0;
|
||||
|
||||
const req = https.get(fileUrl, (res) => {
|
||||
if (res.statusCode && res.statusCode >= 400) {
|
||||
res.resume();
|
||||
return reject(
|
||||
new Error(`Failed to download file: ${res.statusCode} ${res.statusMessage}`)
|
||||
);
|
||||
}
|
||||
const chunks: Buffer[] = [];
|
||||
res.on('data', (chunk) => {
|
||||
downloadedBytes += chunk.length;
|
||||
if (downloadedBytes > MAX_BYTES) {
|
||||
res.resume();
|
||||
req.destroy(new Error('Attachment exceeds 5 MB limit'));
|
||||
return;
|
||||
}
|
||||
chunks.push(chunk);
|
||||
});
|
||||
res.on('end', () => {
|
||||
if (req.destroyed) return;
|
||||
const buffer = Buffer.concat(chunks);
|
||||
let contentType =
|
||||
(res.headers['content-type'] as string) || 'application/octet-stream';
|
||||
|
||||
// If server returns generic octet-stream, try to detect from file path
|
||||
if (contentType === 'application/octet-stream' && filePath) {
|
||||
contentType = getMimeTypeFromPath(filePath);
|
||||
}
|
||||
|
||||
resolve({ base64: buffer.toString('base64'), mimeType: contentType });
|
||||
});
|
||||
res.on('error', (err) => {
|
||||
if (!req.destroyed) {
|
||||
reject(err);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
req.on('error', reject);
|
||||
|
||||
req.setTimeout(30000, () => {
|
||||
if (!req.destroyed) {
|
||||
req.destroy(new Error('File download timed out'));
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// Helper to load prompts from DextoAgent
|
||||
async function loadPrompts(agent: DextoAgent): Promise<void> {
|
||||
try {
|
||||
cachedPrompts = await agent.listPrompts();
|
||||
const count = Object.keys(cachedPrompts).length;
|
||||
logger.info(`📝 Loaded ${count} prompts from DextoAgent`, 'green');
|
||||
} catch (error) {
|
||||
logger.error(`Failed to load prompts: ${error instanceof Error ? error.message : error}`);
|
||||
cachedPrompts = {};
|
||||
}
|
||||
}
|
||||
|
||||
// Insert initTelegramBot to wire up a TelegramBot given pre-initialized services
|
||||
export async function startTelegramBot(agent: DextoAgent) {
|
||||
if (!token) {
|
||||
throw new Error('TELEGRAM_BOT_TOKEN is not set');
|
||||
}
|
||||
|
||||
const agentEventBus = agent.agentEventBus;
|
||||
|
||||
// Load prompts from DextoAgent at startup
|
||||
await loadPrompts(agent);
|
||||
|
||||
// Create and start Telegram Bot
|
||||
const bot = new Bot(token);
|
||||
logger.info('Telegram bot started', 'green');
|
||||
|
||||
// Helper to get or create session for a Telegram user
|
||||
// Each Telegram user gets their own persistent session
|
||||
function getTelegramSessionId(userId: number): string {
|
||||
return `telegram-${userId}`;
|
||||
}
|
||||
|
||||
// /start command with command buttons
|
||||
bot.command('start', async (ctx) => {
|
||||
const keyboard = new InlineKeyboard();
|
||||
|
||||
// Get config prompts (most useful for general tasks)
|
||||
const configPrompts = Object.entries(cachedPrompts)
|
||||
.filter(([_, info]) => info.source === 'config')
|
||||
.slice(0, 6); // Limit to 6 prompts for cleaner UI
|
||||
|
||||
// Add prompt buttons in rows of 2
|
||||
for (let i = 0; i < configPrompts.length; i += 2) {
|
||||
const [name1, info1] = configPrompts[i]!;
|
||||
const button1 = info1.title || name1;
|
||||
keyboard.text(button1, `prompt_${name1}`);
|
||||
|
||||
if (i + 1 < configPrompts.length) {
|
||||
const [name2, info2] = configPrompts[i + 1]!;
|
||||
const button2 = info2.title || name2;
|
||||
keyboard.text(button2, `prompt_${name2}`);
|
||||
}
|
||||
keyboard.row();
|
||||
}
|
||||
|
||||
// Add utility buttons
|
||||
keyboard.text('🔄 Reset', 'reset').text('❓ Help', 'help');
|
||||
|
||||
const helpText =
|
||||
'*Welcome to Dexto AI Bot!* 🤖\n\n' +
|
||||
'I can help you with various tasks. Here are your options:\n\n' +
|
||||
'**Direct Chat:**\n' +
|
||||
"• Send any text, image, or audio and I'll respond\n\n" +
|
||||
'**Slash Commands:**\n' +
|
||||
'• `/ask <question>` - Ask anything\n' +
|
||||
'• Use any loaded prompt as a command (e.g., `/summarize`, `/explain`)\n\n' +
|
||||
'**Quick buttons above** - Click to activate a prompt mode!';
|
||||
|
||||
await ctx.reply(helpText, {
|
||||
parse_mode: 'Markdown',
|
||||
reply_markup: keyboard,
|
||||
});
|
||||
});
|
||||
|
||||
// Dynamic command handlers for all prompts
|
||||
for (const [promptName, promptInfo] of Object.entries(cachedPrompts)) {
|
||||
// Register each prompt as a slash command
|
||||
bot.command(promptName, async (ctx) => {
|
||||
const userContext = ctx.match?.trim() || '';
|
||||
|
||||
if (!ctx.from) {
|
||||
logger.error(`Telegram /${promptName} command received without from field`);
|
||||
return;
|
||||
}
|
||||
|
||||
const sessionId = getTelegramSessionId(ctx.from.id);
|
||||
|
||||
try {
|
||||
await ctx.replyWithChatAction('typing');
|
||||
|
||||
// Use agent.resolvePrompt to get the prompt text with context
|
||||
const result = await agent.resolvePrompt(promptName, {
|
||||
context: userContext,
|
||||
});
|
||||
|
||||
// If prompt has placeholders and no context provided, ask for it
|
||||
if (!result.text.trim() && !userContext) {
|
||||
await ctx.reply(
|
||||
`Please provide context for this prompt.\n\nExample: \`/${promptName} your text here\``,
|
||||
{ parse_mode: 'Markdown' }
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// Generate response using the resolved prompt
|
||||
const response = await agent.generate(result.text, sessionId);
|
||||
await ctx.reply(response.content || '🤖 No response generated');
|
||||
} catch (err) {
|
||||
logger.error(
|
||||
`Error handling /${promptName} command: ${err instanceof Error ? err.message : err}`
|
||||
);
|
||||
const errorMessage = err instanceof Error ? err.message : 'Unknown error';
|
||||
await ctx.reply(`Error: ${errorMessage}`);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Handle button callbacks (prompt buttons and actions)
|
||||
bot.on('callback_query:data', async (ctx) => {
|
||||
const action = ctx.callbackQuery.data;
|
||||
const sessionId = getTelegramSessionId(ctx.callbackQuery.from.id);
|
||||
|
||||
try {
|
||||
// Handle prompt buttons (e.g., prompt_summarize, prompt_explain)
|
||||
if (action.startsWith('prompt_')) {
|
||||
const promptName = action.substring(7); // Remove 'prompt_' prefix
|
||||
const promptInfo = cachedPrompts[promptName];
|
||||
|
||||
if (!promptInfo) {
|
||||
await ctx.answerCallbackQuery({ text: 'Prompt not found' });
|
||||
return;
|
||||
}
|
||||
|
||||
await ctx.answerCallbackQuery({
|
||||
text: `Executing ${promptInfo.title || promptName}...`,
|
||||
});
|
||||
|
||||
try {
|
||||
await ctx.replyWithChatAction('typing');
|
||||
|
||||
// Try to resolve and execute the prompt directly
|
||||
const result = await agent.resolvePrompt(promptName, {});
|
||||
|
||||
// If prompt resolved to empty (requires context), ask for input
|
||||
if (!result.text.trim()) {
|
||||
const description =
|
||||
promptInfo.description || `Use ${promptInfo.title || promptName}`;
|
||||
await ctx.reply(
|
||||
`Send your text, image, or audio for *${promptInfo.title || promptName}*:`,
|
||||
{
|
||||
parse_mode: 'Markdown',
|
||||
reply_markup: {
|
||||
force_reply: true,
|
||||
selective: true,
|
||||
input_field_placeholder: description,
|
||||
},
|
||||
}
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// Prompt is self-contained, execute it directly
|
||||
const response = await agent.generate(result.text, sessionId);
|
||||
await ctx.reply(response.content || '🤖 No response generated');
|
||||
} catch (error) {
|
||||
logger.error(
|
||||
`Error executing prompt ${promptName}: ${error instanceof Error ? error.message : error}`
|
||||
);
|
||||
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
|
||||
await ctx.reply(`❌ Error: ${errorMessage}`);
|
||||
}
|
||||
} else if (action === 'reset') {
|
||||
await agent.resetConversation(sessionId);
|
||||
await ctx.answerCallbackQuery({ text: '✅ Conversation reset' });
|
||||
await ctx.reply('🔄 Conversation has been reset.');
|
||||
} else if (action === 'help') {
|
||||
// Build dynamic help text showing available prompts
|
||||
const promptNames = Object.keys(cachedPrompts).slice(0, 10);
|
||||
const promptList = promptNames.map((name) => `\`/${name}\``).join(', ');
|
||||
|
||||
const helpText =
|
||||
'**Available Features:**\n' +
|
||||
'🎤 *Voice Messages* - Send audio for transcription\n' +
|
||||
'🖼️ *Images* - Send photos for analysis\n' +
|
||||
'📝 *Text* - Any question or request\n\n' +
|
||||
'**Slash Commands** (use any prompt):\n' +
|
||||
`${promptList}\n\n` +
|
||||
'**Quick Tip:** Use the buttons from /start for faster interaction!';
|
||||
|
||||
await ctx.answerCallbackQuery();
|
||||
await ctx.reply(helpText, { parse_mode: 'Markdown' });
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error(
|
||||
`Error handling callback query: ${error instanceof Error ? error.message : error}`
|
||||
);
|
||||
await ctx.answerCallbackQuery({ text: '❌ Error occurred' });
|
||||
try {
|
||||
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
|
||||
await ctx.reply(`Error: ${errorMessage}`);
|
||||
} catch (e) {
|
||||
logger.error(
|
||||
`Failed to send error message for callback query: ${e instanceof Error ? e.message : e}`
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Group chat slash command: /ask <your question>
|
||||
bot.command('ask', async (ctx) => {
|
||||
const question = ctx.match;
|
||||
if (!question) {
|
||||
await ctx.reply('Please provide a question, e.g. `/ask How do I ...?`', {
|
||||
parse_mode: 'Markdown',
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (!ctx.from) {
|
||||
logger.error('Telegram /ask command received without from field');
|
||||
return;
|
||||
}
|
||||
const sessionId = getTelegramSessionId(ctx.from.id);
|
||||
try {
|
||||
await ctx.replyWithChatAction('typing');
|
||||
const response = await agent.generate(question, sessionId);
|
||||
await ctx.reply(response.content || '🤖 No response generated');
|
||||
} catch (err) {
|
||||
logger.error(
|
||||
`Error handling /ask command: ${err instanceof Error ? err.message : err}`
|
||||
);
|
||||
const errorMessage = err instanceof Error ? err.message : 'Unknown error';
|
||||
await ctx.reply(`Error: ${errorMessage}`);
|
||||
}
|
||||
});
|
||||
|
||||
// Inline query handler (for @botname query in any chat)
|
||||
bot.on('inline_query', async (ctx) => {
|
||||
const query = ctx.inlineQuery.query;
|
||||
if (!query) {
|
||||
return;
|
||||
}
|
||||
|
||||
const userId = ctx.inlineQuery.from.id;
|
||||
const queryText = query.trim();
|
||||
const cacheKey = `${userId}:${queryText}`;
|
||||
const now = Date.now();
|
||||
|
||||
// Debounce: return cached results if query repeated within interval
|
||||
const cached = inlineQueryCache[cacheKey];
|
||||
if (cached && now - cached.timestamp < INLINE_QUERY_DEBOUNCE_INTERVAL) {
|
||||
await ctx.answerInlineQuery(cached.results);
|
||||
return;
|
||||
}
|
||||
|
||||
// Concurrency cap
|
||||
if (currentInlineQueries >= MAX_CONCURRENT_INLINE_QUERIES) {
|
||||
// Too many concurrent inline queries; respond with empty list
|
||||
await ctx.answerInlineQuery([]);
|
||||
return;
|
||||
}
|
||||
|
||||
currentInlineQueries++;
|
||||
try {
|
||||
const sessionId = getTelegramSessionId(userId);
|
||||
const queryTimeout = 15000; // 15 seconds timeout
|
||||
const responsePromise = agent.generate(query, sessionId);
|
||||
|
||||
const response = await Promise.race([
|
||||
responsePromise,
|
||||
new Promise<{ content: string }>((_, reject) =>
|
||||
setTimeout(() => reject(new Error('Query timed out')), queryTimeout)
|
||||
),
|
||||
]);
|
||||
|
||||
const resultText = response.content || 'No response';
|
||||
const results = [
|
||||
{
|
||||
type: 'article' as const,
|
||||
id: ctx.inlineQuery.id,
|
||||
title: 'AI Answer',
|
||||
input_message_content: { message_text: resultText },
|
||||
description: resultText.substring(0, 100),
|
||||
},
|
||||
];
|
||||
|
||||
// Cache the results (cleanup old entries first to prevent unbounded growth)
|
||||
cleanupInlineQueryCache();
|
||||
inlineQueryCache[cacheKey] = { timestamp: now, results };
|
||||
await ctx.answerInlineQuery(results);
|
||||
} catch (error) {
|
||||
logger.error(
|
||||
`Error handling inline query: ${error instanceof Error ? error.message : error}`
|
||||
);
|
||||
// Inform user about the error through inline results
|
||||
try {
|
||||
await ctx.answerInlineQuery([
|
||||
{
|
||||
type: 'article' as const,
|
||||
id: ctx.inlineQuery.id,
|
||||
title: 'Error processing query',
|
||||
input_message_content: {
|
||||
message_text: `Sorry, I encountered an error: ${error instanceof Error ? error.message : 'Unknown error'}`,
|
||||
},
|
||||
description: 'Error occurred while processing your request',
|
||||
},
|
||||
]);
|
||||
} catch (e) {
|
||||
logger.error(
|
||||
`Failed to send inline query error: ${e instanceof Error ? e.message : e}`
|
||||
);
|
||||
}
|
||||
} finally {
|
||||
currentInlineQueries--;
|
||||
}
|
||||
});
|
||||
|
||||
// Message handler with image + audio support and tool notifications
|
||||
bot.on('message', async (ctx) => {
|
||||
let userText = ctx.message.text || ctx.message.caption || '';
|
||||
let imageDataInput: { image: string; mimeType: string } | undefined;
|
||||
let fileDataInput: { data: string; mimeType: string; filename?: string } | undefined;
|
||||
let isAudioMessage = false;
|
||||
|
||||
try {
|
||||
// Detect and process images
|
||||
if (ctx.message.photo && ctx.message.photo.length > 0) {
|
||||
const photo = ctx.message.photo[ctx.message.photo.length - 1]!;
|
||||
const file = await ctx.api.getFile(photo.file_id);
|
||||
const fileUrl = `https://api.telegram.org/file/bot${token}/${file.file_path}`;
|
||||
const { base64, mimeType } = await downloadFileAsBase64(fileUrl, file.file_path);
|
||||
imageDataInput = { image: base64, mimeType };
|
||||
userText = ctx.message.caption || ''; // Use caption if available
|
||||
}
|
||||
|
||||
// Detect and process audio/voice messages
|
||||
if (ctx.message.voice) {
|
||||
isAudioMessage = true;
|
||||
const voice = ctx.message.voice;
|
||||
const file = await ctx.api.getFile(voice.file_id);
|
||||
const fileUrl = `https://api.telegram.org/file/bot${token}/${file.file_path}`;
|
||||
const { base64, mimeType } = await downloadFileAsBase64(fileUrl, file.file_path);
|
||||
|
||||
// Telegram voice messages are always OGG format
|
||||
// Detect from file path, but fallback to audio/ogg
|
||||
const audioMimeType = mimeType.startsWith('audio/') ? mimeType : 'audio/ogg';
|
||||
|
||||
fileDataInput = {
|
||||
data: base64,
|
||||
mimeType: audioMimeType,
|
||||
filename: 'audio.ogg',
|
||||
};
|
||||
|
||||
// Add context if audio-only (no caption)
|
||||
if (!userText) {
|
||||
userText = '(User sent an audio message for transcription and analysis)';
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
logger.error(
|
||||
`Failed to process attached media in Telegram bot: ${err instanceof Error ? err.message : err}`
|
||||
);
|
||||
try {
|
||||
const errorMessage = err instanceof Error ? err.message : 'Unknown error';
|
||||
if (isAudioMessage) {
|
||||
await ctx.reply(`🎤 Error processing audio: ${errorMessage}`);
|
||||
} else {
|
||||
await ctx.reply(`🖼️ Error processing image: ${errorMessage}`);
|
||||
}
|
||||
} catch (sendError) {
|
||||
logger.error(
|
||||
`Failed to send error message to user: ${sendError instanceof Error ? sendError.message : sendError}`
|
||||
);
|
||||
}
|
||||
return; // Stop processing if media handling fails
|
||||
}
|
||||
|
||||
// Validate that we have something to process
|
||||
if (!userText && !imageDataInput && !fileDataInput) return;
|
||||
|
||||
// Get session for this user
|
||||
// ctx.from can be undefined for channel posts or anonymous admin messages
|
||||
if (!ctx.from) {
|
||||
logger.debug(
|
||||
'Telegram message without user context (channel post or anonymous admin); skipping'
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const sessionId = getTelegramSessionId(ctx.from.id);
|
||||
|
||||
// Subscribe for toolCall events
|
||||
const toolCallHandler = (payload: {
|
||||
toolName: string;
|
||||
args: unknown;
|
||||
callId?: string;
|
||||
sessionId: string;
|
||||
}) => {
|
||||
// Filter by sessionId to avoid cross-session leakage
|
||||
if (payload.sessionId !== sessionId) return;
|
||||
ctx.reply(`🔧 Calling *${payload.toolName}*`, { parse_mode: 'Markdown' }).catch((e) =>
|
||||
logger.warn(`Failed to notify tool call: ${e}`)
|
||||
);
|
||||
};
|
||||
agentEventBus.on('llm:tool-call', toolCallHandler);
|
||||
|
||||
try {
|
||||
await ctx.replyWithChatAction('typing');
|
||||
|
||||
// Build content array from message and attachments
|
||||
const content: import('@dexto/core').ContentPart[] = [];
|
||||
if (userText) {
|
||||
content.push({ type: 'text', text: userText });
|
||||
}
|
||||
if (imageDataInput) {
|
||||
content.push({
|
||||
type: 'image',
|
||||
image: imageDataInput.image,
|
||||
mimeType: imageDataInput.mimeType,
|
||||
});
|
||||
}
|
||||
if (fileDataInput) {
|
||||
content.push({
|
||||
type: 'file',
|
||||
data: fileDataInput.data,
|
||||
mimeType: fileDataInput.mimeType,
|
||||
filename: fileDataInput.filename,
|
||||
});
|
||||
}
|
||||
|
||||
const response = await agent.generate(content, sessionId);
|
||||
|
||||
await ctx.reply(response.content || '🤖 No response generated');
|
||||
|
||||
// Log token usage if available (optional analytics)
|
||||
if (response.usage) {
|
||||
logger.debug(
|
||||
`Session ${sessionId} - Tokens: input=${response.usage.inputTokens}, output=${response.usage.outputTokens}`
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error(
|
||||
`Error handling Telegram message: ${error instanceof Error ? error.message : error}`
|
||||
);
|
||||
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
|
||||
await ctx.reply(`❌ Error: ${errorMessage}`);
|
||||
} finally {
|
||||
agentEventBus.off('llm:tool-call', toolCallHandler);
|
||||
}
|
||||
});
|
||||
|
||||
// Start the bot
|
||||
bot.start();
|
||||
return bot;
|
||||
}
|
||||
Reference in New Issue
Block a user