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:
19
dexto/examples/discord-bot/.env.example
Normal file
19
dexto/examples/discord-bot/.env.example
Normal file
@@ -0,0 +1,19 @@
|
||||
# Discord Bot Token
|
||||
# Get this from Discord Developer Portal: https://discord.com/developers/applications
|
||||
DISCORD_BOT_TOKEN=your_discord_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
|
||||
|
||||
# Rate limiting settings (optional)
|
||||
# Enable/disable rate limiting per user (default: true)
|
||||
DISCORD_RATE_LIMIT_ENABLED=true
|
||||
|
||||
# Cooldown in seconds between messages from the same user (default: 5)
|
||||
DISCORD_RATE_LIMIT_SECONDS=5
|
||||
249
dexto/examples/discord-bot/README.md
Normal file
249
dexto/examples/discord-bot/README.md
Normal file
@@ -0,0 +1,249 @@
|
||||
# Discord Bot Example
|
||||
|
||||
This is a **reference implementation** showing how to integrate DextoAgent with Discord using discord.js. It demonstrates:
|
||||
- Connecting to Discord's WebSocket API
|
||||
- Processing messages and commands
|
||||
- Handling image attachments
|
||||
- Managing per-user conversation sessions
|
||||
- Integrating tool calls with Discord messages
|
||||
|
||||
## ⚠️ Important: This is a Reference Implementation
|
||||
|
||||
This example is provided to show how to build Discord 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
|
||||
- Advanced permission management
|
||||
|
||||
Use this as a foundation to build your own customized Discord bot!
|
||||
|
||||
## Quick Start
|
||||
|
||||
### 1. Get Your Discord Bot Token
|
||||
|
||||
1. Go to [Discord Developer Portal](https://discord.com/developers/applications)
|
||||
2. Click "New Application" and give it a name
|
||||
3. In the sidebar, navigate to **Bot** → Click **Add Bot**
|
||||
4. Under the TOKEN section, click **Copy** (or **Reset Token** if you need a new one)
|
||||
5. Save this token - you'll need it in 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 Discord Bot Token** (required):
|
||||
```
|
||||
DISCORD_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. Invite Bot to Your Server
|
||||
|
||||
1. In Developer Portal, go to **OAuth2** → **URL Generator**
|
||||
2. Select scopes: `bot`
|
||||
3. Select permissions: `Send Messages`, `Read Messages`, `Read Message History`, `Attach Files`
|
||||
4. Copy the generated URL and visit it to invite your bot to your server
|
||||
|
||||
### 4. Install Dependencies
|
||||
|
||||
Install the required dependencies:
|
||||
|
||||
```bash
|
||||
pnpm install
|
||||
```
|
||||
|
||||
### 5. Run the Bot
|
||||
|
||||
Start the bot:
|
||||
|
||||
```bash
|
||||
pnpm start
|
||||
```
|
||||
|
||||
You should see:
|
||||
```
|
||||
🚀 Initializing Discord bot...
|
||||
Discord bot logged in as YourBotName#1234
|
||||
✅ Discord bot is running!
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
### In DMs
|
||||
Simply send a message to the bot - it will respond using the configured LLM.
|
||||
|
||||
### In Server Channels
|
||||
Use the `!ask` prefix:
|
||||
```
|
||||
!ask What is the capital of France?
|
||||
```
|
||||
|
||||
The bot will respond with the agent's response, splitting long messages to respect Discord's 2000-character limit.
|
||||
|
||||
### Image Support
|
||||
Send an image attachment with or without text, and the bot will process it using the agent's vision capabilities.
|
||||
|
||||
### Audio Support
|
||||
Send audio files (MP3, WAV, OGG, etc.), and the bot will:
|
||||
- Transcribe the audio (if model supports speech recognition)
|
||||
- Analyze the audio content
|
||||
- Use audio as context for responses
|
||||
|
||||
Simply attach an audio file to your message and the bot will process it using the agent's multimodal capabilities.
|
||||
|
||||
### Reset Conversation
|
||||
To start a fresh conversation session, DM the bot with:
|
||||
```
|
||||
/reset
|
||||
```
|
||||
|
||||
## Configuration
|
||||
|
||||
### 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:
|
||||
|
||||
- **`DISCORD_BOT_TOKEN`** (Required): Your bot's authentication token
|
||||
- **`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
|
||||
- **`DISCORD_RATE_LIMIT_ENABLED`** (Optional): Enable/disable rate limiting (default: true)
|
||||
- **`DISCORD_RATE_LIMIT_SECONDS`** (Optional): Cooldown between messages per user (default: 5)
|
||||
|
||||
## Features
|
||||
|
||||
### Rate Limiting
|
||||
By default, the bot enforces a 5-second cooldown per user to prevent spam. Adjust or disable via environment variables.
|
||||
|
||||
### 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 tool get_weather with args: {...}
|
||||
```
|
||||
|
||||
### Session Management
|
||||
Each Discord user gets their own persistent conversation session during the bot's lifetime. Messages from different users don't interfere with each other.
|
||||
|
||||
### Large Responses
|
||||
Responses longer than Discord's 2000-character limit are automatically split into multiple messages.
|
||||
|
||||
## Limitations
|
||||
|
||||
- **No persistence across restarts**: Sessions are lost when the bot restarts. For persistent sessions, implement a database layer.
|
||||
- **Simple message handling**: Only responds to text and images. Doesn't support all Discord features like reactions, threads, etc.
|
||||
- **Per-deployment limits**: The bot runs as a single instance. For horizontal scaling, implement clustering.
|
||||
|
||||
## Architecture
|
||||
|
||||
```
|
||||
Discord Message
|
||||
↓
|
||||
startDiscordBot() wires up event handlers
|
||||
↓
|
||||
agent.generate() processes the message
|
||||
↓
|
||||
Response sent back to Discord
|
||||
↓
|
||||
agentEventBus emits events (tool calls, etc.)
|
||||
↓
|
||||
Tool notifications sent to channel
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Bot doesn't respond to messages
|
||||
- Check that the bot has permission to send messages in the channel
|
||||
- Ensure `DISCORD_BOT_TOKEN` is correct in `.env`
|
||||
- Verify bot has `Message Content Intent` enabled in Developer Portal
|
||||
|
||||
### "DISCORD_BOT_TOKEN is not set"
|
||||
- Check that `.env` file exists in the example directory
|
||||
- Verify the token is correctly copied from Developer Portal
|
||||
|
||||
### Rate limiting errors
|
||||
- Check `DISCORD_RATE_LIMIT_SECONDS` setting
|
||||
- Set `DISCORD_RATE_LIMIT_ENABLED=false` to disable rate limiting
|
||||
|
||||
### Image processing fails
|
||||
- Ensure attachments are under 5MB
|
||||
- Check network connectivity for downloading attachments
|
||||
|
||||
## 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 new command handlers
|
||||
- Implement additional Discord features
|
||||
- Add logging/monitoring
|
||||
|
||||
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
|
||||
|
||||
## Documentation
|
||||
|
||||
- [Discord.js Documentation](https://discord.js.org/)
|
||||
- [Discord Developer Portal](https://discord.com/developers/applications)
|
||||
- [Dexto Documentation](https://dexto.dev)
|
||||
- [Dexto Agent API](https://docs.dexto.dev)
|
||||
|
||||
## License
|
||||
|
||||
MIT
|
||||
78
dexto/examples/discord-bot/agent-config.yml
Normal file
78
dexto/examples/discord-bot/agent-config.yml
Normal file
@@ -0,0 +1,78 @@
|
||||
# Discord Bot Agent Configuration
|
||||
# This agent is optimized for Discord 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: openai
|
||||
model: gpt-5-mini
|
||||
apiKey: $OPENAI_API_KEY
|
||||
|
||||
# System Prompt
|
||||
# Defines the bot's personality, capabilities, and behavior
|
||||
# This prompt is customized for Discord-specific constraints (2000 char message limit)
|
||||
systemPrompt: |
|
||||
You are a helpful and friendly Discord bot powered by Dexto. You assist users with a wide range of tasks
|
||||
including answering questions, providing information, and helping with coding, writing, and analysis.
|
||||
|
||||
## 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 explanations
|
||||
- Creative Tasks: Writing, brainstorming, and content generation
|
||||
|
||||
## Response Guidelines
|
||||
- Keep responses concise and Discord-friendly (under 2000 characters where possible)
|
||||
- Use plain text formatting (avoid markdown syntax like ###, ---, etc.)
|
||||
- For code, use simple indentation without backtick formatting
|
||||
- For emphasis, use CAPS or simple punctuation like asterisks *like this*
|
||||
- Break long responses into multiple messages if needed
|
||||
- Be helpful, respectful, and inclusive
|
||||
- If you can't do something, explain why clearly
|
||||
|
||||
## Usage in Discord
|
||||
- Direct Messages: Respond to all messages sent to you
|
||||
- Server Channels: Respond to messages using the !ask prefix (e.g., !ask what is TypeScript?)
|
||||
- Attachments: You can process images attached to messages
|
||||
|
||||
Always aim to be helpful, accurate, and concise in your responses using plain text format.
|
||||
|
||||
# MCP Servers Configuration
|
||||
# These provide the tools and capabilities your bot can use
|
||||
mcpServers:
|
||||
# Add exa server for web search capabilities
|
||||
exa:
|
||||
type: http
|
||||
url: https://mcp.exa.ai/mcp
|
||||
|
||||
# Tool Confirmation Configuration
|
||||
# Discord 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/discord-bot-agent/
|
||||
blob:
|
||||
type: local
|
||||
maxBlobSize: 52428800 # 50MB per attachment
|
||||
maxTotalSize: 1073741824 # 1GB total
|
||||
cleanupAfterDays: 30
|
||||
|
||||
# Memory Configuration - enables the bot to remember context
|
||||
memories:
|
||||
enabled: true
|
||||
priority: 40
|
||||
limit: 10
|
||||
includeTags: true
|
||||
|
||||
# Optional: Greeting message shown when bot starts
|
||||
greeting: "Hi! I'm a Discord bot powered by Dexto. Type `!ask your question` in channels or just message me directly!"
|
||||
319
dexto/examples/discord-bot/bot.ts
Normal file
319
dexto/examples/discord-bot/bot.ts
Normal file
@@ -0,0 +1,319 @@
|
||||
import dotenv from 'dotenv';
|
||||
import { Client, GatewayIntentBits, Partials } from 'discord.js';
|
||||
import https from 'https';
|
||||
import http from 'http'; // ADDED for http support
|
||||
import { DextoAgent, logger } from '@dexto/core';
|
||||
|
||||
// Load environment variables
|
||||
dotenv.config();
|
||||
const token = process.env.DISCORD_BOT_TOKEN;
|
||||
|
||||
// User-based cooldown system for Discord interactions
|
||||
const userCooldowns = new Map<string, number>();
|
||||
const RATE_LIMIT_ENABLED = process.env.DISCORD_RATE_LIMIT_ENABLED?.toLowerCase() !== 'false'; // default-on
|
||||
let COOLDOWN_SECONDS = Number(process.env.DISCORD_RATE_LIMIT_SECONDS ?? 5);
|
||||
|
||||
if (Number.isNaN(COOLDOWN_SECONDS) || COOLDOWN_SECONDS < 0) {
|
||||
console.error(
|
||||
'DISCORD_RATE_LIMIT_SECONDS must be a non-negative number. Defaulting to 5 seconds.'
|
||||
);
|
||||
COOLDOWN_SECONDS = 5; // Default to a safe value
|
||||
}
|
||||
|
||||
// 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,
|
||||
fileName?: string
|
||||
): Promise<{ base64: string; mimeType: string }> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const protocol = fileUrl.startsWith('https:') ? https : http; // Determine protocol
|
||||
const MAX_BYTES = 5 * 1024 * 1024; // 5 MB hard cap
|
||||
let downloadedBytes = 0;
|
||||
|
||||
const req = protocol.get(fileUrl, (res) => {
|
||||
// Store the request object
|
||||
if (res.statusCode && res.statusCode >= 400) {
|
||||
// Clean up response stream
|
||||
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) {
|
||||
// Clean up response stream before destroying request
|
||||
res.resume();
|
||||
req.destroy(new Error('Attachment exceeds 5 MB limit')); // Destroy the request
|
||||
// No explicit reject here, as 'error' on req should handle it or timeout will occur
|
||||
return;
|
||||
}
|
||||
chunks.push(chunk);
|
||||
});
|
||||
res.on('end', () => {
|
||||
if (req.destroyed) return; // If request was destroyed due to size limit, do nothing
|
||||
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 name
|
||||
if (contentType === 'application/octet-stream' && fileName) {
|
||||
contentType = getMimeTypeFromPath(fileName);
|
||||
}
|
||||
|
||||
resolve({ base64: buffer.toString('base64'), mimeType: contentType });
|
||||
});
|
||||
// Handle errors on the response stream itself (e.g., premature close)
|
||||
res.on('error', (err) => {
|
||||
if (!req.destroyed) {
|
||||
// Avoid double-rejection if req.destroy() already called this
|
||||
reject(err);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Handle errors on the request object (e.g., socket hang up, DNS resolution error, or from req.destroy())
|
||||
req.on('error', (err) => {
|
||||
reject(err);
|
||||
});
|
||||
|
||||
// Optional: Add a timeout for the request
|
||||
req.setTimeout(30000, () => {
|
||||
// 30 seconds timeout
|
||||
if (!req.destroyed) {
|
||||
req.destroy(new Error('File download timed out'));
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// Insert initDiscordBot to wire up a Discord client given pre-initialized services
|
||||
export function startDiscordBot(agent: DextoAgent) {
|
||||
if (!token) {
|
||||
throw new Error('DISCORD_BOT_TOKEN is not set');
|
||||
}
|
||||
|
||||
const agentEventBus = agent.agentEventBus;
|
||||
|
||||
// Helper to get or create session for a Discord user
|
||||
// Each Discord user gets their own persistent session
|
||||
function getDiscordSessionId(userId: string): string {
|
||||
return `discord-${userId}`;
|
||||
}
|
||||
|
||||
// Create Discord client
|
||||
const client = new Client({
|
||||
intents: [
|
||||
GatewayIntentBits.Guilds,
|
||||
GatewayIntentBits.GuildMessages,
|
||||
GatewayIntentBits.MessageContent,
|
||||
GatewayIntentBits.DirectMessages,
|
||||
],
|
||||
partials: [Partials.Channel],
|
||||
});
|
||||
|
||||
client.once('ready', () => {
|
||||
console.log(`Discord bot logged in as ${client.user?.tag || 'Unknown'}`);
|
||||
});
|
||||
|
||||
client.on('messageCreate', async (message) => {
|
||||
// Ignore bots
|
||||
if (message.author.bot) return;
|
||||
|
||||
if (RATE_LIMIT_ENABLED && COOLDOWN_SECONDS > 0) {
|
||||
// Only apply cooldown if enabled and seconds > 0
|
||||
const now = Date.now();
|
||||
const cooldownEnd = userCooldowns.get(message.author.id) || 0;
|
||||
|
||||
if (now < cooldownEnd) {
|
||||
const timeLeft = (cooldownEnd - now) / 1000;
|
||||
try {
|
||||
await message.reply(
|
||||
`Please wait ${timeLeft.toFixed(1)} more seconds before using this command again.`
|
||||
);
|
||||
} catch (replyError) {
|
||||
console.error('Error sending cooldown message:', replyError);
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
let userText: string | undefined = message.content;
|
||||
let imageDataInput: { image: string; mimeType: string } | undefined;
|
||||
let fileDataInput: { data: string; mimeType: string; filename?: string } | undefined;
|
||||
|
||||
// Helper to determine if mime type is audio
|
||||
const isAudioMimeType = (mimeType: string): boolean => {
|
||||
return mimeType.startsWith('audio/');
|
||||
};
|
||||
|
||||
// Handle attachments (images and audio)
|
||||
if (message.attachments.size > 0) {
|
||||
const attachment = message.attachments.first();
|
||||
if (attachment && attachment.url) {
|
||||
try {
|
||||
const { base64, mimeType } = await downloadFileAsBase64(
|
||||
attachment.url,
|
||||
attachment.name || 'file'
|
||||
);
|
||||
|
||||
if (isAudioMimeType(mimeType)) {
|
||||
// Handle audio files
|
||||
fileDataInput = {
|
||||
data: base64,
|
||||
mimeType,
|
||||
filename: attachment.name || 'audio.wav',
|
||||
};
|
||||
// Add context if only audio (no text in message)
|
||||
if (!userText) {
|
||||
userText =
|
||||
'(User sent an audio message for transcription and analysis)';
|
||||
}
|
||||
} else if (mimeType.startsWith('image/')) {
|
||||
// Handle image files
|
||||
imageDataInput = { image: base64, mimeType };
|
||||
userText = message.content || '';
|
||||
}
|
||||
} catch (downloadError) {
|
||||
console.error('Failed to download attachment:', downloadError);
|
||||
try {
|
||||
await message.reply(
|
||||
`⚠️ Failed to download attachment: ${downloadError instanceof Error ? downloadError.message : 'Unknown error'}. Please try again or send the message without the attachment.`
|
||||
);
|
||||
} catch (replyError) {
|
||||
console.error('Error sending attachment failure message:', replyError);
|
||||
}
|
||||
// Continue without the attachment - if there's text content, process that
|
||||
if (!userText) {
|
||||
return; // If there's no text and attachment failed, nothing to process
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Only respond to !ask prefix or DMs
|
||||
if (!message.guild || (userText && userText.startsWith('!ask '))) {
|
||||
if (userText && userText.startsWith('!ask ')) {
|
||||
userText = userText.substring(5).trim();
|
||||
}
|
||||
if (!userText) return;
|
||||
|
||||
// Subscribe for toolCall events
|
||||
const toolCallHandler = (payload: {
|
||||
toolName: string;
|
||||
args: unknown;
|
||||
callId?: string;
|
||||
sessionId: string;
|
||||
}) => {
|
||||
message.channel.send(`🔧 Calling tool **${payload.toolName}**`).catch((error) => {
|
||||
console.error(
|
||||
`Failed to send tool call notification for ${payload.toolName} to channel ${message.channel.id}:`,
|
||||
error
|
||||
);
|
||||
});
|
||||
};
|
||||
agentEventBus.on('llm:tool-call', toolCallHandler);
|
||||
|
||||
try {
|
||||
const sessionId = getDiscordSessionId(message.author.id);
|
||||
await message.channel.sendTyping();
|
||||
|
||||
// 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);
|
||||
|
||||
const responseText = response.content;
|
||||
|
||||
// Handle Discord's 2000 character limit
|
||||
const MAX_LENGTH = 1900; // Leave some buffer
|
||||
if (responseText && responseText.length <= MAX_LENGTH) {
|
||||
await message.reply(responseText);
|
||||
} else if (responseText) {
|
||||
// Split into chunks and send multiple messages
|
||||
let remaining = responseText;
|
||||
let isFirst = true;
|
||||
|
||||
while (remaining && remaining.length > 0) {
|
||||
const chunk = remaining.substring(0, MAX_LENGTH);
|
||||
remaining = remaining.substring(MAX_LENGTH);
|
||||
|
||||
if (isFirst) {
|
||||
await message.reply(chunk);
|
||||
isFirst = false;
|
||||
} else {
|
||||
// For subsequent chunks, use message.channel.send to avoid a chain of replies
|
||||
// Adding a small delay helps with ordering and rate limits
|
||||
await new Promise((resolve) => setTimeout(resolve, 250)); // 250ms delay
|
||||
await message.channel.send(chunk);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
await message.reply(
|
||||
'🤖 I received your message but could not generate a response.'
|
||||
);
|
||||
}
|
||||
|
||||
// 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) {
|
||||
console.error('Error handling Discord message', error);
|
||||
try {
|
||||
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
|
||||
await message.reply(`❌ Error: ${errorMessage}`);
|
||||
} catch (replyError) {
|
||||
console.error('Error sending error reply:', replyError);
|
||||
}
|
||||
} finally {
|
||||
agentEventBus.off('llm:tool-call', toolCallHandler);
|
||||
// Set cooldown for the user after processing
|
||||
if (RATE_LIMIT_ENABLED && COOLDOWN_SECONDS > 0) {
|
||||
userCooldowns.set(message.author.id, Date.now() + COOLDOWN_SECONDS * 1000);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
client.login(token);
|
||||
return client;
|
||||
}
|
||||
39
dexto/examples/discord-bot/main.ts
Normal file
39
dexto/examples/discord-bot/main.ts
Normal file
@@ -0,0 +1,39 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
import 'dotenv/config';
|
||||
import { DextoAgent } from '@dexto/core';
|
||||
import { loadAgentConfig, enrichAgentConfig } from '@dexto/agent-management';
|
||||
import { startDiscordBot } from './bot.js';
|
||||
|
||||
async function main() {
|
||||
try {
|
||||
// Load agent configuration from local agent-config.yml
|
||||
console.log('🚀 Initializing Discord 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 Discord bot
|
||||
console.log('📡 Starting Discord bot connection...');
|
||||
startDiscordBot(agent);
|
||||
|
||||
console.log('✅ Discord bot is running! Send messages or use !ask <question> prefix.');
|
||||
console.log(' In DMs, just send your message without the !ask prefix.');
|
||||
|
||||
// Graceful shutdown
|
||||
process.on('SIGINT', async () => {
|
||||
console.log('\n🛑 Shutting down...');
|
||||
await agent.stop();
|
||||
process.exit(0);
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('❌ Failed to start Discord bot:', error);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
main();
|
||||
30
dexto/examples/discord-bot/package.json
Normal file
30
dexto/examples/discord-bot/package.json
Normal file
@@ -0,0 +1,30 @@
|
||||
{
|
||||
"name": "dexto-discord-bot-example",
|
||||
"version": "1.0.0",
|
||||
"description": "Discord 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",
|
||||
"discord",
|
||||
"bot",
|
||||
"ai",
|
||||
"agent"
|
||||
],
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@dexto/core": "^1.1.3",
|
||||
"@dexto/agent-management": "^1.1.3",
|
||||
"discord.js": "^14.19.3",
|
||||
"dotenv": "^16.3.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"tsx": "^4.7.1",
|
||||
"typescript": "^5.5.4"
|
||||
}
|
||||
}
|
||||
2019
dexto/examples/discord-bot/pnpm-lock.yaml
generated
Normal file
2019
dexto/examples/discord-bot/pnpm-lock.yaml
generated
Normal file
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user