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:
16
dexto/examples/telegram-bot/.env.example
Normal file
16
dexto/examples/telegram-bot/.env.example
Normal 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
|
||||
329
dexto/examples/telegram-bot/README.md
Normal file
329
dexto/examples/telegram-bot/README.md
Normal 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
|
||||
119
dexto/examples/telegram-bot/agent-config.yml
Normal file
119
dexto/examples/telegram-bot/agent-config.yml
Normal 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
|
||||
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;
|
||||
}
|
||||
38
dexto/examples/telegram-bot/main.ts
Normal file
38
dexto/examples/telegram-bot/main.ts
Normal 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();
|
||||
30
dexto/examples/telegram-bot/package.json
Normal file
30
dexto/examples/telegram-bot/package.json
Normal 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
1886
dexto/examples/telegram-bot/pnpm-lock.yaml
generated
Normal file
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user