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:
admin
2026-01-28 00:27:56 +04:00
Unverified
parent 3b128ba3bd
commit b52318eeae
1724 changed files with 351216 additions and 0 deletions

View File

@@ -0,0 +1,16 @@
# Telegram Bot Token
# Get this from BotFather: https://t.me/botfather
TELEGRAM_BOT_TOKEN=your_telegram_bot_token_here
# LLM API Key
# Required: Set this to your OpenAI API key
# Get one at: https://platform.openai.com/account/api-keys
OPENAI_API_KEY=your_openai_api_key_here
# Alternative LLM providers (uncomment one and use it in agent-config.yml)
# ANTHROPIC_API_KEY=your_anthropic_api_key_here
# GOOGLE_GENERATIVE_AI_API_KEY=your_google_api_key_here
# Inline query settings (optional)
# Maximum concurrent inline queries to process (default: 5)
TELEGRAM_INLINE_QUERY_CONCURRENCY=5

View File

@@ -0,0 +1,329 @@
# Telegram Bot Example
This is a **reference implementation** showing how to integrate DextoAgent with Telegram using grammy. It demonstrates:
- Connecting to Telegram Bot API with long polling
- Processing messages and commands
- Handling inline queries for direct responses in any chat
- Handling image attachments
- Managing per-user conversation sessions
- Integrating tool calls with Telegram messages
## ⚠️ Important: This is a Reference Implementation
This example is provided to show how to build Telegram integrations with Dexto. While it works, it's not a production-ready bot and may lack:
- Advanced error recovery and retry logic
- Comprehensive logging and monitoring
- Scalability features for large deployments
- Webhook support (currently uses long polling only)
- Advanced rate limiting
Use this as a foundation to build your own customized Telegram bot!
## Quick Start
### 1. Get Your Telegram Bot Token
1. Open Telegram and search for **BotFather** (verify it has the blue checkmark)
2. Send `/start` to begin a conversation
3. Send `/newbot` to create a new bot
4. Follow the prompts:
- Give your bot a name (e.g., "Dexto AI Bot")
- Give it a username ending in "bot" (e.g., "dexto_ai_bot")
5. BotFather will provide your token - save it for the next step
### 2. Set Up Your Environment Variables
Copy `.env.example` to `.env`:
```bash
cp .env.example .env
```
Edit `.env` and add:
1. **Your Telegram Bot Token** (required):
```
TELEGRAM_BOT_TOKEN=your_token_here
```
2. **Your LLM API Key** (required):
For OpenAI (default):
```
OPENAI_API_KEY=your_openai_api_key_here
```
Or use a different provider and update `agent-config.yml`:
```
# ANTHROPIC_API_KEY=your_key_here
# GOOGLE_GENERATIVE_AI_API_KEY=your_key_here
```
**Get API Keys:**
- **OpenAI**: https://platform.openai.com/account/api-keys
- **Anthropic**: https://console.anthropic.com/account/keys
- **Google**: https://ai.google.dev
### 3. Install Dependencies
Install the required dependencies:
```bash
pnpm install
```
### 4. Run the Bot
Start the bot:
```bash
pnpm start
```
You should see:
```
🚀 Initializing Telegram bot...
📡 Starting Telegram bot connection...
✅ Telegram bot is running! Start with /start command.
```
### 5. Test Your Bot
1. Open Telegram and search for your bot's username
2. Click "Start" or send `/start` command
3. You should see a welcome message with buttons
## Usage
### Commands
- **`/start`** - Display welcome message with command buttons and options
- **`/ask <question>`** - Ask a question (works in groups with prefix)
- **`/explain <topic>`** - Get detailed explanations of topics
- **`/summarize <text>`** - Summarize provided content
- **`/code <problem>`** - Get help with programming tasks
- **`/analyze <data>`** - Analyze information or data
- **`/creative <idea>`** - Brainstorm creatively on a topic
### Features
#### Quick Command Buttons
When you send `/start`, the bot displays interactive buttons for each command. Click a button to start that interaction without typing a command!
**Available command buttons:**
- 💡 Explain
- 📋 Summarize
- 💻 Code
- ✨ Creative
- 🔍 Analyze
#### Text Messages
Send any message directly to the bot in DMs, and it will respond using the configured LLM with full conversation context.
#### Image Support
Send photos with optional captions, and the bot will analyze them using the agent's vision capabilities (for models that support vision).
#### Audio/Voice Messages
Send voice messages or audio files, and the bot will:
- Transcribe the audio (if model supports speech recognition)
- Analyze the audio content
- Use voice as context for responses
Supported audio formats: OGG (Telegram voice), MP3, WAV, and other audio formats your LLM supports.
#### Inline Queries
In any chat (without messaging the bot), use inline mode:
```
@your_bot_name What is the capital of France?
```
The bot will respond with a result you can send directly to the chat.
#### Session Management
- **Reset Conversation** - Use the 🔄 Reset button from `/start` to clear conversation history
- **Help** - Use the ❓ Help button to see all available features
#### Per-User Sessions
Each Telegram user gets their own isolated conversation session. Multiple users in a group chat will each have separate conversations, preventing cross-user context pollution.
## Configuration
### Adding Custom Prompts
The bot automatically loads prompts from your `agent-config.yml` file. These prompts appear as buttons in `/start` and can be invoked as slash commands.
**To add a new prompt:**
```yaml
prompts:
- type: inline
id: mycommand # Used as /mycommand
title: "🎯 My Command" # Button label
description: "What this command does"
prompt: "System instruction:\n\n{{context}}" # Template with {{context}} placeholder
category: custom
priority: 10
```
**Example prompts included:**
*Self-contained (execute immediately):*
- `/quick-start` - Learn what the bot can do
- `/demo` - See tools in action
*Context-requiring (ask for input):*
- `/summarize` - Summarize content
- `/explain` - Detailed explanations
- `/code` - Programming help
- `/translate` - Language translation
**Using prompts:**
1. **As slash commands**: `/summarize Your text here`
2. **As buttons**:
- Self-contained prompts execute immediately ⚡
- Context-requiring prompts ask for input 💬
3. **Smart detection**: Bot automatically determines if context is needed
4. **Dynamic loading**: Prompts update when you restart the bot
### Switching LLM Providers
The bot comes configured with OpenAI by default. To use a different provider:
1. **Update `agent-config.yml`** - Change the `llm` section:
```yaml
# For Anthropic Claude:
llm:
provider: anthropic
model: claude-sonnet-4-5-20250929
apiKey: $ANTHROPIC_API_KEY
# For Google Gemini:
llm:
provider: google
model: gemini-2.0-flash
apiKey: $GOOGLE_GENERATIVE_AI_API_KEY
```
2. **Set the API key in `.env`**:
```
ANTHROPIC_API_KEY=your_key_here
# or
GOOGLE_GENERATIVE_AI_API_KEY=your_key_here
```
### Environment Variables
Create a `.env` file with:
- **`TELEGRAM_BOT_TOKEN`** (Required): Your bot's authentication token from BotFather
- **`OPENAI_API_KEY`** (Required for OpenAI): Your OpenAI API key
- **`ANTHROPIC_API_KEY`** (Optional): For using Claude models
- **`GOOGLE_GENERATIVE_AI_API_KEY`** (Optional): For using Gemini models
- **`TELEGRAM_INLINE_QUERY_CONCURRENCY`** (Optional): Max concurrent inline queries (default: 5)
## Features
### Session Management
Each Telegram user gets their own persistent conversation session during the bot's lifetime. Messages from different users don't interfere with each other.
### Tool Notifications
When the LLM calls a tool (e.g., making an API call), the bot sends a notification message so users can see what's happening:
```
Calling get_weather with args: {...}
```
### Inline Query Debouncing
Repeated inline queries are cached for 2 seconds to reduce redundant processing.
### Concurrency Control
By default, the bot limits concurrent inline query processing to 5 to prevent overwhelming the system. Adjust via `TELEGRAM_INLINE_QUERY_CONCURRENCY`.
## Architecture
```
Telegram Message
startTelegramBot() wires up event handlers
agent.run() processes the message
Response sent back to Telegram
agentEventBus emits events (tool calls, etc.)
Tool notifications sent to chat
```
## Transport Methods
### Long Polling (Current)
The bot uses long polling by default. It continuously asks Telegram "any new messages?" This is:
- ✅ Simpler to implement
- ✅ Works behind firewalls
- ❌ More network overhead
- ❌ Slightly higher latency
### Webhook (Optional)
For production use, consider implementing webhook support for better performance. This would require:
- A public URL with HTTPS
- Updating grammy configuration
- Setting up a reverse proxy if needed
## Limitations
- **No persistence across restarts**: Sessions are lost when the bot restarts. For persistent sessions, implement a database layer.
- **Long polling**: Not ideal for high-volume bots. Consider webhooks for production.
- **Per-deployment limits**: The bot runs as a single instance. For horizontal scaling, implement clustering with a distributed session store.
- **No button callbacks for advanced features**: This example shows basic callback handling. Extend for more complex interactions.
## Troubleshooting
### Bot doesn't respond to messages
- Verify `TELEGRAM_BOT_TOKEN` is correct in `.env`
- Check that the bot is online by sending `/start` to BotFather
- Ensure the bot is running (`npm start`)
### "TELEGRAM_BOT_TOKEN is not set"
- Check that `.env` file exists in the example directory
- Verify the token is correctly copied from BotFather
### Timeout on inline queries
- Check `TELEGRAM_INLINE_QUERY_CONCURRENCY` setting
- The bot has a 15-second timeout for inline queries - if your LLM is slow, increase this in bot.ts
### Image processing fails
- Ensure images are valid and not corrupted
- Check network connectivity for downloading images
## Next Steps
To customize this bot:
1. **Modify `agent-config.yml`**:
- Change the LLM provider/model
- Add MCP servers for additional capabilities
- Customize the system prompt
2. **Extend `bot.ts`**:
- Add more commands
- Implement webhook support
- Add logging/monitoring
- Add database persistence
3. **Deploy**:
- Run on a server/VPS that stays online 24/7
- Use process managers like PM2 to auto-restart on crashes
- Consider hosting on platforms like Railway, Heroku, or AWS
- Migrate to webhook transport for better scalability
## Documentation
- [grammY Documentation](https://grammy.dev/)
- [Telegram Bot API](https://core.telegram.org/bots/api)
- [BotFather Commands](https://core.telegram.org/bots#botfather)
- [Dexto Documentation](https://dexto.dev)
- [Dexto Agent API](https://docs.dexto.dev)
## License
MIT

View File

@@ -0,0 +1,119 @@
# Telegram Bot Agent Configuration
# This agent is optimized for Telegram bot interactions.
# For more configuration options, see: https://docs.dexto.dev/guides/configuring-dexto
# LLM Configuration
# The bot uses this LLM provider and model for all interactions
llm:
provider: google
model: gemini-2.5-flash
apiKey: $GOOGLE_GENERATIVE_AI_API_KEY
# System Prompt
# Defines the bot's personality, capabilities, and behavior
# This prompt is customized for Telegram's capabilities and constraints
systemPrompt: |
You are a friendly and helpful Telegram bot powered by Dexto. You assist users with a wide range of tasks
including answering questions, providing information, analysis, coding help, and creative writing.
## Your Capabilities
- File System Access: Read and explore files in your working directory
- Web Browsing: Visit websites and extract information (when configured)
- Code Analysis: Help with programming, debugging, and code review
- Information Retrieval: Answer questions and provide detailed explanations
- Creative Tasks: Writing, brainstorming, ideas generation
- Inline Queries: Users can use @botname to get quick answers in any chat
## Response Guidelines
- Keep responses conversational and friendly
- Use plain text formatting (Telegram doesn't support complex markdown)
- For code, use simple indentation without backticks or special formatting
- For emphasis, use CAPS or simple punctuation like asterisks *like this*
- Break complex topics into digestible parts with clear spacing
- Be helpful, respectful, and accurate
- If you can't help, explain why clearly and suggest alternatives
## Commands
- /start - Welcome message with menu
- /ask <question> - Ask a question in group chats
- Send messages in DM for direct conversation
Always provide helpful, accurate, and friendly responses in plain text format.
# Tool Confirmation Configuration
# Telegram bots auto-approve tool calls, so we disable confirmation
toolConfirmation:
mode: auto-approve
allowedToolsStorage: memory
# Storage Configuration
# Data storage for sessions, memories, and caching
storage:
cache:
type: in-memory
database:
type: sqlite
# Database file will be created in ~/.dexto/agents/telegram-bot-agent/
blob:
type: local
maxBlobSize: 52428800 # 50MB per upload
maxTotalSize: 1073741824 # 1GB total
cleanupAfterDays: 30
# Optional: Greeting shown in /start command
greeting: "Welcome! I'm a Telegram bot powered by Dexto. I can help with questions, code, writing, analysis, and more! 🤖"
# Prompts - Define reusable command templates
# These appear as buttons in /start and can be invoked as slash commands
prompts:
# Self-contained prompts (execute immediately when clicked)
- type: inline
id: quick-start
title: "🚀 Quick Start"
description: "Learn what I can do and how to use me"
prompt: "I'd like to get started quickly. Can you show me a few examples of what you can do in Telegram and help me understand how to work with you?"
category: learning
priority: 10
showInStarters: true
- type: inline
id: demo
title: "⚡ Demo Tools"
description: "See available tools in action"
prompt: "I'd like to see your tools in action. Can you show me what tools you have available and demonstrate one with a practical example?"
category: tools
priority: 9
showInStarters: true
# Context-requiring prompts (ask for input when clicked)
- type: inline
id: summarize
title: "📋 Summarize"
description: "Summarize text, articles, or concepts"
prompt: "Please provide a concise summary of the following. Focus on key points and main ideas:\n\n{{context}}"
category: productivity
priority: 8
- type: inline
id: explain
title: "💡 Explain"
description: "Get detailed explanations of any topic"
prompt: "Please explain the following concept in detail. Break it down into understandable parts:\n\n{{context}}"
category: learning
priority: 7
- type: inline
id: code
title: "💻 Code Help"
description: "Get help with programming tasks"
prompt: "You are a coding expert. Help with the following programming task. Provide clear, well-commented code examples:\n\n{{context}}"
category: development
priority: 6
- type: inline
id: translate
title: "🌐 Translate"
description: "Translate text between languages"
prompt: "Translate the following text. Detect the source language and translate to English:\n\n{{context}}"
category: language
priority: 5

View 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;
}

View File

@@ -0,0 +1,38 @@
#!/usr/bin/env node
import 'dotenv/config';
import { DextoAgent } from '@dexto/core';
import { loadAgentConfig, enrichAgentConfig } from '@dexto/agent-management';
import { startTelegramBot } from './bot.js';
async function main() {
try {
// Load agent configuration from local agent-config.yml
console.log('🚀 Initializing Telegram bot...');
const configPath = './agent-config.yml';
const config = await loadAgentConfig(configPath);
const enrichedConfig = enrichAgentConfig(config, configPath);
// Create and start the Dexto agent
const agent = new DextoAgent(enrichedConfig, configPath);
await agent.start();
// Start the Telegram bot
console.log('📡 Starting Telegram bot connection...');
await startTelegramBot(agent);
console.log('✅ Telegram bot is running! Start with /start command.');
// Graceful shutdown
process.on('SIGINT', async () => {
console.log('\n🛑 Shutting down...');
await agent.stop();
process.exit(0);
});
} catch (error) {
console.error('❌ Failed to start Telegram bot:', error);
process.exit(1);
}
}
main();

View File

@@ -0,0 +1,30 @@
{
"name": "dexto-telegram-bot-example",
"version": "1.0.0",
"description": "Telegram bot integration example using Dexto",
"type": "module",
"main": "dist/main.js",
"scripts": {
"start": "tsx main.ts",
"build": "tsc",
"dev": "tsx watch main.ts"
},
"keywords": [
"dexto",
"telegram",
"bot",
"ai",
"agent"
],
"license": "MIT",
"dependencies": {
"@dexto/core": "^1.1.3",
"@dexto/agent-management": "^1.1.3",
"grammy": "^1.38.2",
"dotenv": "^16.3.1"
},
"devDependencies": {
"tsx": "^4.7.1",
"typescript": "^5.5.4"
}
}

1886
dexto/examples/telegram-bot/pnpm-lock.yaml generated Normal file

File diff suppressed because it is too large Load Diff