From 7001521a0ccbc19bf08b9bf859fce940b2b2abe6 Mon Sep 17 00:00:00 2001 From: Ting Chien Meng Date: Thu, 13 Feb 2025 12:04:26 +0800 Subject: [PATCH 01/24] environment test --- .../__tests__/environment.test.ts | 54 +++++++++++++++++++ packages/plugin-discord/src/environment.ts | 4 +- 2 files changed, 56 insertions(+), 2 deletions(-) create mode 100644 packages/plugin-discord/__tests__/environment.test.ts diff --git a/packages/plugin-discord/__tests__/environment.test.ts b/packages/plugin-discord/__tests__/environment.test.ts new file mode 100644 index 00000000000..a1e5de502fe --- /dev/null +++ b/packages/plugin-discord/__tests__/environment.test.ts @@ -0,0 +1,54 @@ +import { describe, it, expect } from 'vitest'; +import { validateDiscordConfig } from '../src/environment'; +import type { IAgentRuntime } from '@elizaos/core'; + +// Mock runtime environment +const mockRuntime: IAgentRuntime = { + env: { + DISCORD_APPLICATION_ID: '123456789012345678', + DISCORD_API_TOKEN: 'mocked-discord-token', + }, + getEnv: function (key: string) { + return this.env[key] || null; + }, + getSetting: function (key: string) { + return this.env[key] || null; + } +} as unknown as IAgentRuntime; + +describe('Discord Environment Configuration', () => { + it('should validate correct configuration', async () => { + const config = await validateDiscordConfig(mockRuntime); + expect(config).toBeDefined(); + expect(config.DISCORD_APPLICATION_ID).toBe('123456789012345678'); + expect(config.DISCORD_API_TOKEN).toBe('mocked-discord-token'); + }); + + it('should throw an error when DISCORD_APPLICATION_ID is missing', async () => { + const invalidRuntime = { + ...mockRuntime, + env: { + ...mockRuntime.env, + DISCORD_APPLICATION_ID: undefined, + }, + } as IAgentRuntime; + + await expect(validateDiscordConfig(invalidRuntime)).rejects.toThrowError( + 'Discord configuration validation failed:\nDISCORD_APPLICATION_ID: Discord application ID is required' + ); + }); + + it('should throw an error when DISCORD_API_TOKEN is missing', async () => { + const invalidRuntime = { + ...mockRuntime, + env: { + ...mockRuntime.env, + DISCORD_API_TOKEN: undefined, + }, + } as IAgentRuntime; + + await expect(validateDiscordConfig(invalidRuntime)).rejects.toThrowError( + 'Discord configuration validation failed:\nDISCORD_API_TOKEN: Discord API token is required' + ); + }); +}); diff --git a/packages/plugin-discord/src/environment.ts b/packages/plugin-discord/src/environment.ts index ad3489a8fc2..ff1f9f29f0a 100644 --- a/packages/plugin-discord/src/environment.ts +++ b/packages/plugin-discord/src/environment.ts @@ -16,9 +16,9 @@ export async function validateDiscordConfig( try { const config = { DISCORD_APPLICATION_ID: - runtime.getSetting("DISCORD_APPLICATION_ID"), + runtime.getSetting("DISCORD_APPLICATION_ID") || "", DISCORD_API_TOKEN: - runtime.getSetting("DISCORD_API_TOKEN"), + runtime.getSetting("DISCORD_API_TOKEN") || "", }; return discordEnvSchema.parse(config); From d793b208fd15e511ae73c40dd9bedd2b9afe9449 Mon Sep 17 00:00:00 2001 From: Ting Chien Meng Date: Thu, 13 Feb 2025 12:28:00 +0800 Subject: [PATCH 02/24] validate env --- packages/plugin-discord/src/index.ts | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/packages/plugin-discord/src/index.ts b/packages/plugin-discord/src/index.ts index 57b870bd9b0..1255f30d7d4 100644 --- a/packages/plugin-discord/src/index.ts +++ b/packages/plugin-discord/src/index.ts @@ -30,6 +30,7 @@ import voiceStateProvider from "./providers/voiceState.ts"; import reply from "./actions/reply.ts"; import type { IDiscordClient } from "./types.ts"; import { VoiceManager } from "./voice.ts"; +import { validateDiscordConfig, DiscordConfig } from "./environment.ts"; export class DiscordClient extends EventEmitter implements IDiscordClient { apiToken: string; @@ -39,10 +40,10 @@ export class DiscordClient extends EventEmitter implements IDiscordClient { private messageManager: MessageManager; private voiceManager: VoiceManager; - constructor(runtime: IAgentRuntime) { + constructor(runtime: IAgentRuntime, discordConfig: DiscordConfig) { super(); - this.apiToken = runtime.getSetting("DISCORD_API_TOKEN") as string; + this.apiToken = discordConfig.DISCORD_API_TOKEN; this.client = new Client({ intents: [ GatewayIntentBits.Guilds, @@ -389,7 +390,11 @@ export class DiscordClient extends EventEmitter implements IDiscordClient { const DiscordClientInterface: ElizaClient = { name: 'discord', - start: async (runtime: IAgentRuntime) => new DiscordClient(runtime), + start: async (runtime: IAgentRuntime) => { + const discordConfig: DiscordConfig = + await validateDiscordConfig(runtime); + return new DiscordClient(runtime, discordConfig); + }, }; const testSuite: TestSuite = { @@ -398,7 +403,9 @@ const testSuite: TestSuite = { { name: "test creating discord client", fn: async (runtime: IAgentRuntime) => { - const discordClient = new DiscordClient(runtime); + const discordConfig: DiscordConfig = + await validateDiscordConfig(runtime); + const discordClient = new DiscordClient(runtime, discordConfig); console.log("Created a discord client"); } } From fa01d9d0e29f7a5142399719aada084e0dc02f2e Mon Sep 17 00:00:00 2001 From: Ting Chien Meng Date: Thu, 13 Feb 2025 12:28:24 +0800 Subject: [PATCH 03/24] pretty --- packages/plugin-discord/src/index.ts | 765 +++++++++++++-------------- 1 file changed, 376 insertions(+), 389 deletions(-) diff --git a/packages/plugin-discord/src/index.ts b/packages/plugin-discord/src/index.ts index 1255f30d7d4..50e4ed1b732 100644 --- a/packages/plugin-discord/src/index.ts +++ b/packages/plugin-discord/src/index.ts @@ -1,21 +1,21 @@ import { - logger, - stringToUuid, - type TestSuite, - type Character, - type Client as ElizaClient, - type IAgentRuntime, - type Plugin + logger, + stringToUuid, + type TestSuite, + type Character, + type Client as ElizaClient, + type IAgentRuntime, + type Plugin, } from "@elizaos/core"; import { - Client, - Events, - GatewayIntentBits, - Partials, - PermissionsBitField, - type Guild, - type MessageReaction, - type User, + Client, + Events, + GatewayIntentBits, + Partials, + PermissionsBitField, + type Guild, + type MessageReaction, + type User, } from "discord.js"; import { EventEmitter } from "events"; import chat_with_attachments from "./actions/chat_with_attachments.ts"; @@ -33,404 +33,391 @@ import { VoiceManager } from "./voice.ts"; import { validateDiscordConfig, DiscordConfig } from "./environment.ts"; export class DiscordClient extends EventEmitter implements IDiscordClient { - apiToken: string; - client: Client; - runtime: IAgentRuntime; - character: Character; - private messageManager: MessageManager; - private voiceManager: VoiceManager; - - constructor(runtime: IAgentRuntime, discordConfig: DiscordConfig) { - super(); - - this.apiToken = discordConfig.DISCORD_API_TOKEN; - this.client = new Client({ - intents: [ - GatewayIntentBits.Guilds, - GatewayIntentBits.DirectMessages, - GatewayIntentBits.GuildVoiceStates, - GatewayIntentBits.MessageContent, - GatewayIntentBits.GuildMessages, - GatewayIntentBits.DirectMessageTyping, - GatewayIntentBits.GuildMessageTyping, - GatewayIntentBits.GuildMessageReactions, - ], - partials: [ - Partials.Channel, - Partials.Message, - Partials.User, - Partials.Reaction, - ], - }); - - this.runtime = runtime; - this.voiceManager = new VoiceManager(this); - this.messageManager = new MessageManager(this, this.voiceManager); - - this.client.once(Events.ClientReady, this.onClientReady.bind(this)); - this.client.login(this.apiToken); - - this.setupEventListeners(); - } - - private setupEventListeners() { - // When joining to a new server - this.client.on("guildCreate", this.handleGuildCreate.bind(this)); - - this.client.on( - Events.MessageReactionAdd, - this.handleReactionAdd.bind(this) - ); - this.client.on( - Events.MessageReactionRemove, - this.handleReactionRemove.bind(this) - ); - - // Handle voice events with the voice manager - this.client.on( - "voiceStateUpdate", - this.voiceManager.handleVoiceStateUpdate.bind(this.voiceManager) - ); - this.client.on( - "userStream", - this.voiceManager.handleUserStream.bind(this.voiceManager) - ); - - // Handle a new message with the message manager - this.client.on( - Events.MessageCreate, - this.messageManager.handleMessage.bind(this.messageManager) - ); - - // Handle a new interaction - this.client.on( - Events.InteractionCreate, - this.handleInteractionCreate.bind(this) - ); + apiToken: string; + client: Client; + runtime: IAgentRuntime; + character: Character; + private messageManager: MessageManager; + private voiceManager: VoiceManager; + + constructor(runtime: IAgentRuntime, discordConfig: DiscordConfig) { + super(); + + this.apiToken = discordConfig.DISCORD_API_TOKEN; + this.client = new Client({ + intents: [ + GatewayIntentBits.Guilds, + GatewayIntentBits.DirectMessages, + GatewayIntentBits.GuildVoiceStates, + GatewayIntentBits.MessageContent, + GatewayIntentBits.GuildMessages, + GatewayIntentBits.DirectMessageTyping, + GatewayIntentBits.GuildMessageTyping, + GatewayIntentBits.GuildMessageReactions, + ], + partials: [ + Partials.Channel, + Partials.Message, + Partials.User, + Partials.Reaction, + ], + }); + + this.runtime = runtime; + this.voiceManager = new VoiceManager(this); + this.messageManager = new MessageManager(this, this.voiceManager); + + this.client.once(Events.ClientReady, this.onClientReady.bind(this)); + this.client.login(this.apiToken); + + this.setupEventListeners(); + } + + private setupEventListeners() { + // When joining to a new server + this.client.on("guildCreate", this.handleGuildCreate.bind(this)); + + this.client.on( + Events.MessageReactionAdd, + this.handleReactionAdd.bind(this) + ); + this.client.on( + Events.MessageReactionRemove, + this.handleReactionRemove.bind(this) + ); + + // Handle voice events with the voice manager + this.client.on( + "voiceStateUpdate", + this.voiceManager.handleVoiceStateUpdate.bind(this.voiceManager) + ); + this.client.on( + "userStream", + this.voiceManager.handleUserStream.bind(this.voiceManager) + ); + + // Handle a new message with the message manager + this.client.on( + Events.MessageCreate, + this.messageManager.handleMessage.bind(this.messageManager) + ); + + // Handle a new interaction + this.client.on( + Events.InteractionCreate, + this.handleInteractionCreate.bind(this) + ); + } + + async stop() { + try { + // disconnect websocket + // this unbinds all the listeners + await this.client.destroy(); + } catch (e) { + logger.error("client-discord instance stop err", e); } - - async stop() { - try { - // disconnect websocket - // this unbinds all the listeners - await this.client.destroy(); - } catch (e) { - logger.error("client-discord instance stop err", e); - } + } + + private async onClientReady(readyClient: { user: { tag: any; id: any } }) { + logger.success(`Logged in as ${readyClient.user?.tag}`); + + // Register slash commands + const commands = [ + { + name: "joinchannel", + description: "Join a voice channel", + options: [ + { + name: "channel", + type: 7, // CHANNEL type + description: "The voice channel to join", + required: true, + channel_types: [2], // GuildVoice type + }, + ], + }, + { + name: "leavechannel", + description: "Leave the current voice channel", + }, + ]; + + try { + await this.client.application?.commands.set(commands); + logger.success("Slash commands registered"); + } catch (error) { + console.error("Error registering slash commands:", error); } - private async onClientReady(readyClient: { user: { tag: any; id: any } }) { - logger.success(`Logged in as ${readyClient.user?.tag}`); - - // Register slash commands - const commands = [ - { - name: "joinchannel", - description: "Join a voice channel", - options: [ - { - name: "channel", - type: 7, // CHANNEL type - description: "The voice channel to join", - required: true, - channel_types: [2], // GuildVoice type - }, - ], - }, - { - name: "leavechannel", - description: "Leave the current voice channel", - }, - ]; - + // Required permissions for the bot + const requiredPermissions = [ + // Text Permissions + PermissionsBitField.Flags.ViewChannel, + PermissionsBitField.Flags.SendMessages, + PermissionsBitField.Flags.SendMessagesInThreads, + PermissionsBitField.Flags.CreatePrivateThreads, + PermissionsBitField.Flags.CreatePublicThreads, + PermissionsBitField.Flags.EmbedLinks, + PermissionsBitField.Flags.AttachFiles, + PermissionsBitField.Flags.AddReactions, + PermissionsBitField.Flags.UseExternalEmojis, + PermissionsBitField.Flags.UseExternalStickers, + PermissionsBitField.Flags.MentionEveryone, + PermissionsBitField.Flags.ManageMessages, + PermissionsBitField.Flags.ReadMessageHistory, + // Voice Permissions + PermissionsBitField.Flags.Connect, + PermissionsBitField.Flags.Speak, + PermissionsBitField.Flags.UseVAD, + PermissionsBitField.Flags.PrioritySpeaker, + ].reduce((a, b) => a | b, 0n); + + logger.success("Use this URL to add the bot to your server:"); + logger.success( + `https://discord.com/api/oauth2/authorize?client_id=${readyClient.user?.id}&permissions=${requiredPermissions}&scope=bot%20applications.commands` + ); + await this.onReady(); + } + + async handleReactionAdd(reaction: MessageReaction, user: User) { + try { + logger.log("Reaction added"); + + // Early returns + if (!reaction || !user) { + logger.warn("Invalid reaction or user"); + return; + } + + // Get emoji info + let emoji = reaction.emoji.name; + if (!emoji && reaction.emoji.id) { + emoji = `<:${reaction.emoji.name}:${reaction.emoji.id}>`; + } + + // Fetch full message if partial + if (reaction.partial) { try { - await this.client.application?.commands.set(commands); - logger.success("Slash commands registered"); + await reaction.fetch(); } catch (error) { - console.error("Error registering slash commands:", error); + logger.error("Failed to fetch partial reaction:", error); + return; } - - // Required permissions for the bot - const requiredPermissions = [ - // Text Permissions - PermissionsBitField.Flags.ViewChannel, - PermissionsBitField.Flags.SendMessages, - PermissionsBitField.Flags.SendMessagesInThreads, - PermissionsBitField.Flags.CreatePrivateThreads, - PermissionsBitField.Flags.CreatePublicThreads, - PermissionsBitField.Flags.EmbedLinks, - PermissionsBitField.Flags.AttachFiles, - PermissionsBitField.Flags.AddReactions, - PermissionsBitField.Flags.UseExternalEmojis, - PermissionsBitField.Flags.UseExternalStickers, - PermissionsBitField.Flags.MentionEveryone, - PermissionsBitField.Flags.ManageMessages, - PermissionsBitField.Flags.ReadMessageHistory, - // Voice Permissions - PermissionsBitField.Flags.Connect, - PermissionsBitField.Flags.Speak, - PermissionsBitField.Flags.UseVAD, - PermissionsBitField.Flags.PrioritySpeaker, - ].reduce((a, b) => a | b, 0n); - - logger.success("Use this URL to add the bot to your server:"); - logger.success( - `https://discord.com/api/oauth2/authorize?client_id=${readyClient.user?.id}&permissions=${requiredPermissions}&scope=bot%20applications.commands` - ); - await this.onReady(); - } - - async handleReactionAdd(reaction: MessageReaction, user: User) { - try { - logger.log("Reaction added"); - - // Early returns - if (!reaction || !user) { - logger.warn("Invalid reaction or user"); - return; - } - - // Get emoji info - let emoji = reaction.emoji.name; - if (!emoji && reaction.emoji.id) { - emoji = `<:${reaction.emoji.name}:${reaction.emoji.id}>`; - } - - // Fetch full message if partial - if (reaction.partial) { - try { - await reaction.fetch(); - } catch (error) { - logger.error( - "Failed to fetch partial reaction:", - error - ); - return; - } - } - - // Generate IDs with timestamp to ensure uniqueness - const timestamp = Date.now(); - const roomId = stringToUuid( - `${reaction.message.channel.id}-${this.runtime.agentId}` - ); - const userIdUUID = stringToUuid( - `${user.id}-${this.runtime.agentId}` - ); - const reactionUUID = stringToUuid( - `${reaction.message.id}-${user.id}-${emoji}-${timestamp}-${this.runtime.agentId}` - ); - - // Validate IDs - if (!userIdUUID || !roomId) { - logger.error("Invalid user ID or room ID", { - userIdUUID, - roomId, - }); - return; - } - - // Process message content - const messageContent = reaction.message.content || ""; - const truncatedContent = - messageContent.length > 100 - ? `${messageContent.substring(0, 100)}...` - : messageContent; - const reactionMessage = `*<${emoji}>: "${truncatedContent}"*`; - - // Get user info - const userName = reaction.message.author?.username || "unknown"; - const name = reaction.message.author?.displayName || userName; - - // Ensure connection - await this.runtime.ensureConnection( - userIdUUID, - roomId, - userName, - name, - "discord" - ); - - // Create memory with retry logic - const memory = { - id: reactionUUID, - userId: userIdUUID, - agentId: this.runtime.agentId, - content: { - text: reactionMessage, - source: "discord", - inReplyTo: stringToUuid( - `${reaction.message.id}-${this.runtime.agentId}` - ), - }, - roomId, - createdAt: timestamp, - }; - - try { - await this.runtime.messageManager.createMemory(memory); - logger.debug("Reaction memory created", { - reactionId: reactionUUID, - emoji, - userId: user.id, - }); - } catch (error) { - if (error.code === "23505") { - // Duplicate key error - logger.warn("Duplicate reaction memory, skipping", { - reactionId: reactionUUID, - }); - return; - } - throw error; // Re-throw other errors - } - } catch (error) { - logger.error("Error handling reaction:", error); + } + + // Generate IDs with timestamp to ensure uniqueness + const timestamp = Date.now(); + const roomId = stringToUuid( + `${reaction.message.channel.id}-${this.runtime.agentId}` + ); + const userIdUUID = stringToUuid(`${user.id}-${this.runtime.agentId}`); + const reactionUUID = stringToUuid( + `${reaction.message.id}-${user.id}-${emoji}-${timestamp}-${this.runtime.agentId}` + ); + + // Validate IDs + if (!userIdUUID || !roomId) { + logger.error("Invalid user ID or room ID", { + userIdUUID, + roomId, + }); + return; + } + + // Process message content + const messageContent = reaction.message.content || ""; + const truncatedContent = + messageContent.length > 100 + ? `${messageContent.substring(0, 100)}...` + : messageContent; + const reactionMessage = `*<${emoji}>: "${truncatedContent}"*`; + + // Get user info + const userName = reaction.message.author?.username || "unknown"; + const name = reaction.message.author?.displayName || userName; + + // Ensure connection + await this.runtime.ensureConnection( + userIdUUID, + roomId, + userName, + name, + "discord" + ); + + // Create memory with retry logic + const memory = { + id: reactionUUID, + userId: userIdUUID, + agentId: this.runtime.agentId, + content: { + text: reactionMessage, + source: "discord", + inReplyTo: stringToUuid( + `${reaction.message.id}-${this.runtime.agentId}` + ), + }, + roomId, + createdAt: timestamp, + }; + + try { + await this.runtime.messageManager.createMemory(memory); + logger.debug("Reaction memory created", { + reactionId: reactionUUID, + emoji, + userId: user.id, + }); + } catch (error) { + if (error.code === "23505") { + // Duplicate key error + logger.warn("Duplicate reaction memory, skipping", { + reactionId: reactionUUID, + }); + return; } + throw error; // Re-throw other errors + } + } catch (error) { + logger.error("Error handling reaction:", error); } + } - async handleReactionRemove(reaction: MessageReaction, user: User) { - logger.log("Reaction removed"); - // if (user.bot) return; - - let emoji = reaction.emoji.name; - if (!emoji && reaction.emoji.id) { - emoji = `<:${reaction.emoji.name}:${reaction.emoji.id}>`; - } - - // Fetch the full message if it's a partial - if (reaction.partial) { - try { - await reaction.fetch(); - } catch (error) { - console.error( - "Something went wrong when fetching the message:", - error - ); - return; - } - } - - const messageContent = reaction.message.content; - const truncatedContent = - messageContent.length > 50 - ? messageContent.substring(0, 50) + "..." - : messageContent; - - const reactionMessage = `*Removed <${emoji} emoji> from: "${truncatedContent}"*`; - - const roomId = stringToUuid( - reaction.message.channel.id + "-" + this.runtime.agentId - ); - const userIdUUID = stringToUuid(user.id); - - // Generate a unique UUID for the reaction removal - const reactionUUID = stringToUuid( - `${reaction.message.id}-${user.id}-${emoji}-removed-${this.runtime.agentId}` - ); - - const userName = reaction.message.author.username; - const name = reaction.message.author.displayName; + async handleReactionRemove(reaction: MessageReaction, user: User) { + logger.log("Reaction removed"); + // if (user.bot) return; - await this.runtime.ensureConnection( - userIdUUID, - roomId, - userName, - name, - "discord" - ); - - try { - // Save the reaction removal as a message - await this.runtime.messageManager.createMemory({ - id: reactionUUID, // This is the ID of the reaction removal message - userId: userIdUUID, - agentId: this.runtime.agentId, - content: { - text: reactionMessage, - source: "discord", - inReplyTo: stringToUuid( - reaction.message.id + "-" + this.runtime.agentId - ), // This is the ID of the original message - }, - roomId, - createdAt: Date.now(), - }); - } catch (error) { - console.error("Error creating reaction removal message:", error); - } + let emoji = reaction.emoji.name; + if (!emoji && reaction.emoji.id) { + emoji = `<:${reaction.emoji.name}:${reaction.emoji.id}>`; } - private handleGuildCreate(guild: Guild) { - console.log(`Joined guild ${guild.name}`); - this.voiceManager.scanGuild(guild); + // Fetch the full message if it's a partial + if (reaction.partial) { + try { + await reaction.fetch(); + } catch (error) { + console.error("Something went wrong when fetching the message:", error); + return; + } } - private async handleInteractionCreate(interaction: any) { - if (!interaction.isCommand()) return; - - switch (interaction.commandName) { - case "joinchannel": - await this.voiceManager.handleJoinChannelCommand(interaction); - break; - case "leavechannel": - await this.voiceManager.handleLeaveChannelCommand(interaction); - break; - } + const messageContent = reaction.message.content; + const truncatedContent = + messageContent.length > 50 + ? messageContent.substring(0, 50) + "..." + : messageContent; + + const reactionMessage = `*Removed <${emoji} emoji> from: "${truncatedContent}"*`; + + const roomId = stringToUuid( + reaction.message.channel.id + "-" + this.runtime.agentId + ); + const userIdUUID = stringToUuid(user.id); + + // Generate a unique UUID for the reaction removal + const reactionUUID = stringToUuid( + `${reaction.message.id}-${user.id}-${emoji}-removed-${this.runtime.agentId}` + ); + + const userName = reaction.message.author.username; + const name = reaction.message.author.displayName; + + await this.runtime.ensureConnection( + userIdUUID, + roomId, + userName, + name, + "discord" + ); + + try { + // Save the reaction removal as a message + await this.runtime.messageManager.createMemory({ + id: reactionUUID, // This is the ID of the reaction removal message + userId: userIdUUID, + agentId: this.runtime.agentId, + content: { + text: reactionMessage, + source: "discord", + inReplyTo: stringToUuid( + reaction.message.id + "-" + this.runtime.agentId + ), // This is the ID of the original message + }, + roomId, + createdAt: Date.now(), + }); + } catch (error) { + console.error("Error creating reaction removal message:", error); } + } + + private handleGuildCreate(guild: Guild) { + console.log(`Joined guild ${guild.name}`); + this.voiceManager.scanGuild(guild); + } + + private async handleInteractionCreate(interaction: any) { + if (!interaction.isCommand()) return; + + switch (interaction.commandName) { + case "joinchannel": + await this.voiceManager.handleJoinChannelCommand(interaction); + break; + case "leavechannel": + await this.voiceManager.handleLeaveChannelCommand(interaction); + break; + } + } - private async onReady() { - const guilds = await this.client.guilds.fetch(); - for (const [, guild] of guilds) { - const fullGuild = await guild.fetch(); - this.voiceManager.scanGuild(fullGuild); - } + private async onReady() { + const guilds = await this.client.guilds.fetch(); + for (const [, guild] of guilds) { + const fullGuild = await guild.fetch(); + this.voiceManager.scanGuild(fullGuild); } + } } const DiscordClientInterface: ElizaClient = { - name: 'discord', - start: async (runtime: IAgentRuntime) => { - const discordConfig: DiscordConfig = - await validateDiscordConfig(runtime); - return new DiscordClient(runtime, discordConfig); - }, + name: "discord", + start: async (runtime: IAgentRuntime) => { + const discordConfig: DiscordConfig = await validateDiscordConfig(runtime); + return new DiscordClient(runtime, discordConfig); + }, }; const testSuite: TestSuite = { - name: "discord", - tests: [ - { - name: "test creating discord client", - fn: async (runtime: IAgentRuntime) => { - const discordConfig: DiscordConfig = - await validateDiscordConfig(runtime); - const discordClient = new DiscordClient(runtime, discordConfig); - console.log("Created a discord client"); - } - } - ] + name: "discord", + tests: [ + { + name: "test creating discord client", + fn: async (runtime: IAgentRuntime) => { + const discordConfig: DiscordConfig = await validateDiscordConfig( + runtime + ); + const discordClient = new DiscordClient(runtime, discordConfig); + console.log("Created a discord client"); + }, + }, + ], }; const discordPlugin: Plugin = { - name: "discord", - description: "Discord client plugin", - clients: [DiscordClientInterface], - actions: [ - reply, - chat_with_attachments, - download_media, - joinvoice, - leavevoice, - summarize, - transcribe_media, - ], - providers: [ - channelStateProvider, - voiceStateProvider, - ], - tests: [ - testSuite, - ] + name: "discord", + description: "Discord client plugin", + clients: [DiscordClientInterface], + actions: [ + reply, + chat_with_attachments, + download_media, + joinvoice, + leavevoice, + summarize, + transcribe_media, + ], + providers: [channelStateProvider, voiceStateProvider], + tests: [testSuite], }; -export default discordPlugin; \ No newline at end of file +export default discordPlugin; From fab17f9f5cffe9b423ce5a928383e0f78314446b Mon Sep 17 00:00:00 2001 From: Ting Chien Meng Date: Thu, 13 Feb 2025 12:58:14 +0800 Subject: [PATCH 04/24] remove DISCORD_APPLICATION_ID since we're not using it --- packages/plugin-discord/src/environment.ts | 5 ----- 1 file changed, 5 deletions(-) diff --git a/packages/plugin-discord/src/environment.ts b/packages/plugin-discord/src/environment.ts index ff1f9f29f0a..82634066ede 100644 --- a/packages/plugin-discord/src/environment.ts +++ b/packages/plugin-discord/src/environment.ts @@ -2,9 +2,6 @@ import type { IAgentRuntime } from "@elizaos/core"; import { z } from "zod"; export const discordEnvSchema = z.object({ - DISCORD_APPLICATION_ID: z - .string() - .min(1, "Discord application ID is required"), DISCORD_API_TOKEN: z.string().min(1, "Discord API token is required"), }); @@ -15,8 +12,6 @@ export async function validateDiscordConfig( ): Promise { try { const config = { - DISCORD_APPLICATION_ID: - runtime.getSetting("DISCORD_APPLICATION_ID") || "", DISCORD_API_TOKEN: runtime.getSetting("DISCORD_API_TOKEN") || "", }; From 816e0419fa5182d6dc70173b17ebc3ad857acff6 Mon Sep 17 00:00:00 2001 From: Ting Chien Meng Date: Thu, 13 Feb 2025 13:01:19 +0800 Subject: [PATCH 05/24] remove DISCORD_APPLICATION_ID since we're not using it --- .../plugin-discord/__tests__/environment.test.ts | 16 ---------------- 1 file changed, 16 deletions(-) diff --git a/packages/plugin-discord/__tests__/environment.test.ts b/packages/plugin-discord/__tests__/environment.test.ts index a1e5de502fe..78add535707 100644 --- a/packages/plugin-discord/__tests__/environment.test.ts +++ b/packages/plugin-discord/__tests__/environment.test.ts @@ -5,7 +5,6 @@ import type { IAgentRuntime } from '@elizaos/core'; // Mock runtime environment const mockRuntime: IAgentRuntime = { env: { - DISCORD_APPLICATION_ID: '123456789012345678', DISCORD_API_TOKEN: 'mocked-discord-token', }, getEnv: function (key: string) { @@ -20,24 +19,9 @@ describe('Discord Environment Configuration', () => { it('should validate correct configuration', async () => { const config = await validateDiscordConfig(mockRuntime); expect(config).toBeDefined(); - expect(config.DISCORD_APPLICATION_ID).toBe('123456789012345678'); expect(config.DISCORD_API_TOKEN).toBe('mocked-discord-token'); }); - it('should throw an error when DISCORD_APPLICATION_ID is missing', async () => { - const invalidRuntime = { - ...mockRuntime, - env: { - ...mockRuntime.env, - DISCORD_APPLICATION_ID: undefined, - }, - } as IAgentRuntime; - - await expect(validateDiscordConfig(invalidRuntime)).rejects.toThrowError( - 'Discord configuration validation failed:\nDISCORD_APPLICATION_ID: Discord application ID is required' - ); - }); - it('should throw an error when DISCORD_API_TOKEN is missing', async () => { const invalidRuntime = { ...mockRuntime, From eeebc59b34465435b24dad59c19ec1983220a3ae Mon Sep 17 00:00:00 2001 From: Ting Chien Meng Date: Thu, 13 Feb 2025 13:11:13 +0800 Subject: [PATCH 06/24] add mockConfig --- packages/plugin-discord/__tests__/discord-client.test.ts | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/packages/plugin-discord/__tests__/discord-client.test.ts b/packages/plugin-discord/__tests__/discord-client.test.ts index ee1cb0b8030..ad88a122aa0 100644 --- a/packages/plugin-discord/__tests__/discord-client.test.ts +++ b/packages/plugin-discord/__tests__/discord-client.test.ts @@ -1,6 +1,7 @@ import { Events } from 'discord.js'; import { beforeEach, describe, expect, it, vi } from 'vitest'; import { DiscordClient } from '../src'; +import { DiscordConfig } from '../src/environment'; // Mock @elizaos/core vi.mock('@elizaos/core', () => ({ @@ -56,6 +57,7 @@ vi.mock('discord.js', () => { }); describe('DiscordClient', () => { + let mockConfig: DiscordConfig; let mockRuntime: any; let discordClient: DiscordClient; @@ -81,13 +83,16 @@ describe('DiscordClient', () => { } }; - discordClient = new DiscordClient(mockRuntime); + mockConfig = { + DISCORD_API_TOKEN: "mock-token", + } + + discordClient = new DiscordClient(mockRuntime, mockConfig); }); it('should initialize with correct configuration', () => { expect(discordClient.apiToken).toBe('mock-token'); expect(discordClient.client).toBeDefined(); - expect(mockRuntime.getSetting).toHaveBeenCalledWith('DISCORD_API_TOKEN'); }); it('should login to Discord on initialization', () => { From e118a0c6b2e964f7250545053747e9b03501a5cd Mon Sep 17 00:00:00 2001 From: Ting Chien Meng Date: Thu, 13 Feb 2025 13:15:05 +0800 Subject: [PATCH 07/24] clean unused code and fix ts error --- packages/plugin-discord/src/messages.ts | 8 -------- packages/plugin-discord/src/voice.ts | 2 +- 2 files changed, 1 insertion(+), 9 deletions(-) diff --git a/packages/plugin-discord/src/messages.ts b/packages/plugin-discord/src/messages.ts index cc95ad25359..d22e2f0daad 100644 --- a/packages/plugin-discord/src/messages.ts +++ b/packages/plugin-discord/src/messages.ts @@ -19,8 +19,6 @@ import { MESSAGE_LENGTH_THRESHOLDS } from "./constants.ts"; import { - discordAnnouncementHypeTemplate, - discordAutoPostTemplate, discordMessageHandlerTemplate, discordShouldRespondTemplate } from "./templates.ts"; @@ -52,9 +50,6 @@ export class MessageManager { private interestChannels: InterestChannels = {}; private discordClient: any; private voiceManager: VoiceManager; - //Auto post - private lastChannelActivity: { [channelId: string]: number } = {}; - private autoPostInterval: NodeJS.Timeout; constructor(discordClient: any, voiceManager: VoiceManager) { this.client = discordClient.client; @@ -70,9 +65,6 @@ export class MessageManager { return; } - // Update last activity time for the channel - this.lastChannelActivity[message.channelId] = Date.now(); - if ( message.interaction || message.author.id === diff --git a/packages/plugin-discord/src/voice.ts b/packages/plugin-discord/src/voice.ts index 070ec363d34..2fbc9240033 100644 --- a/packages/plugin-discord/src/voice.ts +++ b/packages/plugin-discord/src/voice.ts @@ -509,7 +509,7 @@ export class VoiceManager extends EventEmitter { } finally { this.processingVoice = false; } - }, DEBOUNCE_TRANSCRIPTION_THRESHOLD); + }, DEBOUNCE_TRANSCRIPTION_THRESHOLD) as unknown as NodeJS.Timeout; } async handleUserStream( From 8ec59aeee8663690d92898e7ae6704c1635d7b1a Mon Sep 17 00:00:00 2001 From: Ting Chien Meng Date: Thu, 13 Feb 2025 17:00:43 +0800 Subject: [PATCH 08/24] clean code --- packages/plugin-discord/__tests__/discord-client.test.ts | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/packages/plugin-discord/__tests__/discord-client.test.ts b/packages/plugin-discord/__tests__/discord-client.test.ts index ad88a122aa0..73fc2f94edd 100644 --- a/packages/plugin-discord/__tests__/discord-client.test.ts +++ b/packages/plugin-discord/__tests__/discord-client.test.ts @@ -63,10 +63,7 @@ describe('DiscordClient', () => { beforeEach(() => { mockRuntime = { - getSetting: vi.fn((key: string) => { - if (key === 'DISCORD_API_TOKEN') return 'mock-token'; - return undefined; - }), + getSetting: vi.fn(), getState: vi.fn(), setState: vi.fn(), getMemory: vi.fn(), From 08e0cc8f72b20379884be486eb2e10ad28610699 Mon Sep 17 00:00:00 2001 From: Ting Chien Meng Date: Thu, 13 Feb 2025 17:01:16 +0800 Subject: [PATCH 09/24] message test --- .../plugin-discord/__tests__/message.test.ts | 159 ++++++++++++++++++ packages/plugin-discord/src/messages.ts | 7 +- 2 files changed, 161 insertions(+), 5 deletions(-) create mode 100644 packages/plugin-discord/__tests__/message.test.ts diff --git a/packages/plugin-discord/__tests__/message.test.ts b/packages/plugin-discord/__tests__/message.test.ts new file mode 100644 index 00000000000..73514435b18 --- /dev/null +++ b/packages/plugin-discord/__tests__/message.test.ts @@ -0,0 +1,159 @@ +import { describe, it, expect, vi, beforeEach } from 'vitest'; +import { MessageManager } from "../src/messages.ts"; +import { ChannelType, Client, type Message as DiscordMessage, } from 'discord.js'; +import { composeContext, type IAgentRuntime } from '@elizaos/core'; +import { AttachmentManager } from '../src/attachments'; +import type { VoiceManager } from '../src/voice'; + + +vi.mock('@elizaos/core', () => ({ + logger: { + info: vi.fn(), + error: vi.fn(), + debug: vi.fn(), + }, + stringToUuid: (str: string) => str, + messageCompletionFooter: '# INSTRUCTIONS: Choose the best response for the agent.', + shouldRespondFooter: '# INSTRUCTIONS: Choose if the agent should respond.', + generateMessageResponse: vi.fn(), + generateShouldRespond: vi.fn().mockResolvedValue("IGNORE"), // Prevent API calls by always returning "IGNORE" + composeContext: vi.fn(), + ModelClass: { + TEXT_SMALL: 'TEXT_SMALL' + } +})); + +describe('Discord MessageManager', () => { + let mockRuntime: IAgentRuntime; + let mockClient: Client; + let mockDiscordClient: { client: Client; runtime: IAgentRuntime }; + let mockVoiceManager: VoiceManager; + let mockMessage: any; + let messageManager: MessageManager; + + beforeEach(() => { + mockRuntime = { + character: { + name: 'TestBot', + templates: {}, + clientConfig: { + discord: { + allowedChannelIds: ['mock-channal-id'], + shouldIgnoreBotMessages: true, + shouldIgnoreDirectMessages: true + } + } + }, + evaluate: vi.fn(), + composeState: vi.fn(), + ensureConnection: vi.fn(), + ensureUserExists: vi.fn(), + messageManager: { + createMemory: vi.fn(), + addEmbeddingToMemory: vi.fn() + }, + databaseAdapter: { + getParticipantUserState: vi.fn().mockResolvedValue('ACTIVE'), + log: vi.fn() + }, + processActions: vi.fn() + } as unknown as IAgentRuntime; + + mockClient = new Client({ intents: [] }); + mockClient.user = { + id: 'mock-bot-id', + username: 'MockBot', + tag: 'MockBot#0001', + displayName: 'MockBotDisplay', + } as any; + + mockDiscordClient = { + client: mockClient, + runtime: mockRuntime + }; + + mockVoiceManager = { + playAudioStream: vi.fn() + } as unknown as VoiceManager; + + messageManager = new MessageManager(mockDiscordClient, mockVoiceManager); + + const guild = { + members: { + cache: { + get: vi.fn().mockReturnValue({ + nickname: 'MockBotNickname', + permissions: { + has: vi.fn().mockReturnValue(true), // Bot has permissions + } + }) + } + } + } + mockMessage = { + content: 'Hello, MockBot!', + author: { + id: 'mock-user-id', + username: 'MockUser', + bot: false + }, + guild, + channel: { + id: 'mock-channal-id', + type: ChannelType.GuildText, + send: vi.fn(), + guild, + client: { + user: mockClient.user + }, + permissionsFor: vi.fn().mockReturnValue({ + has: vi.fn().mockReturnValue(true) + }) + }, + id: 'mock-message-id', + createdTimestamp: Date.now(), + mentions: { + has: vi.fn().mockReturnValue(false) + }, + reference: null, + attachments: [] + }; + }); + + it('should initialize MessageManager', () => { + expect(messageManager).toBeDefined(); + }); + + it('should process user messages', async () => { + // Prevent further message processing after response check + const proto = Object.getPrototypeOf(messageManager); + vi.spyOn(proto, '_shouldRespond').mockReturnValueOnce(false); + + await messageManager.handleMessage(mockMessage); + expect(mockRuntime.ensureConnection).toHaveBeenCalled(); + expect(mockRuntime.messageManager.createMemory).toHaveBeenCalled(); + }); + + it('should ignore bot messages', async () => { + mockMessage.author.bot = true; + await messageManager.handleMessage(mockMessage); + expect(mockRuntime.ensureConnection).not.toHaveBeenCalled(); + }); + + it('should ignore messages from restricted channels', async () => { + mockMessage.channel.id = 'undefined-channel-id'; + await messageManager.handleMessage(mockMessage); + expect(mockRuntime.ensureConnection).not.toHaveBeenCalled(); + }); + + it.each([ + ["Hey MockBot, are you there?", "username"], + ["MockBot#0001, respond please.", "tag"], + ["MockBotNickname, can you help?", "nickname"] + ])('should respond if the bot %s is included in the message', async (content) => { + mockMessage.content = content; + + const result = await messageManager["_shouldRespond"](mockMessage, {}); + expect(result).toBe(true); + }); +}); diff --git a/packages/plugin-discord/src/messages.ts b/packages/plugin-discord/src/messages.ts index d22e2f0daad..c87304949da 100644 --- a/packages/plugin-discord/src/messages.ts +++ b/packages/plugin-discord/src/messages.ts @@ -48,20 +48,18 @@ export class MessageManager { private runtime: IAgentRuntime; private attachmentManager: AttachmentManager; private interestChannels: InterestChannels = {}; - private discordClient: any; private voiceManager: VoiceManager; constructor(discordClient: any, voiceManager: VoiceManager) { this.client = discordClient.client; this.voiceManager = voiceManager; - this.discordClient = discordClient; this.runtime = discordClient.runtime; this.attachmentManager = new AttachmentManager(this.runtime); } - async handleMessage(message: DiscordMessage) { + async handleMessage(message: DiscordMessage) { if (this.runtime.character.clientConfig?.discord?.allowedChannelIds && - !this.runtime.character.clientConfig.discord.allowedChannelIds.includes(message.channelId)) { + !this.runtime.character.clientConfig.discord.allowedChannelIds.some((id: string) => id == message.channel.id)) { return; } @@ -98,7 +96,6 @@ export class MessageManager { try { const { processedContent, attachments } = await this.processMessageMedia(message); - const audioAttachments = message.attachments.filter((attachment) => attachment.contentType?.startsWith("audio/") ); From 981c1547f87fc963e50f1e443ca131ee5c7104ef Mon Sep 17 00:00:00 2001 From: Ting Chien Meng Date: Thu, 13 Feb 2025 17:02:36 +0800 Subject: [PATCH 10/24] clean code --- packages/plugin-discord/__tests__/message.test.ts | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/packages/plugin-discord/__tests__/message.test.ts b/packages/plugin-discord/__tests__/message.test.ts index 73514435b18..98f3c13a764 100644 --- a/packages/plugin-discord/__tests__/message.test.ts +++ b/packages/plugin-discord/__tests__/message.test.ts @@ -1,8 +1,7 @@ import { describe, it, expect, vi, beforeEach } from 'vitest'; import { MessageManager } from "../src/messages.ts"; -import { ChannelType, Client, type Message as DiscordMessage, } from 'discord.js'; -import { composeContext, type IAgentRuntime } from '@elizaos/core'; -import { AttachmentManager } from '../src/attachments'; +import { ChannelType, Client } from 'discord.js'; +import { type IAgentRuntime } from '@elizaos/core'; import type { VoiceManager } from '../src/voice'; From 72a9987e3e67bb6f2f0de2a854fdbf32675cfa92 Mon Sep 17 00:00:00 2001 From: Ting Chien Meng Date: Thu, 13 Feb 2025 18:22:14 +0800 Subject: [PATCH 11/24] handel attachment --- .../plugin-discord/__tests__/message.test.ts | 52 +++++++++++++++++-- 1 file changed, 47 insertions(+), 5 deletions(-) diff --git a/packages/plugin-discord/__tests__/message.test.ts b/packages/plugin-discord/__tests__/message.test.ts index 98f3c13a764..06d04859265 100644 --- a/packages/plugin-discord/__tests__/message.test.ts +++ b/packages/plugin-discord/__tests__/message.test.ts @@ -1,10 +1,11 @@ import { describe, it, expect, vi, beforeEach } from 'vitest'; import { MessageManager } from "../src/messages.ts"; -import { ChannelType, Client } from 'discord.js'; -import { type IAgentRuntime } from '@elizaos/core'; +import { ChannelType, Client, Collection, AttachmentBuilder } from 'discord.js'; +import { type IAgentRuntime, ServiceType } from '@elizaos/core'; import type { VoiceManager } from '../src/voice'; + vi.mock('@elizaos/core', () => ({ logger: { info: vi.fn(), @@ -19,6 +20,10 @@ vi.mock('@elizaos/core', () => ({ composeContext: vi.fn(), ModelClass: { TEXT_SMALL: 'TEXT_SMALL' + }, + ServiceType: { // ✅ Add this missing mock + VIDEO: 'VIDEO', + BROWSER: 'BROWSER' } })); @@ -31,6 +36,7 @@ describe('Discord MessageManager', () => { let messageManager: MessageManager; beforeEach(() => { + vi.clearAllMocks(); mockRuntime = { character: { name: 'TestBot', @@ -125,8 +131,7 @@ describe('Discord MessageManager', () => { it('should process user messages', async () => { // Prevent further message processing after response check - const proto = Object.getPrototypeOf(messageManager); - vi.spyOn(proto, '_shouldRespond').mockReturnValueOnce(false); + vi.spyOn(Object.getPrototypeOf(messageManager), '_shouldRespond').mockReturnValueOnce(false); await messageManager.handleMessage(mockMessage); expect(mockRuntime.ensureConnection).toHaveBeenCalled(); @@ -148,11 +153,48 @@ describe('Discord MessageManager', () => { it.each([ ["Hey MockBot, are you there?", "username"], ["MockBot#0001, respond please.", "tag"], - ["MockBotNickname, can you help?", "nickname"] + ["MockBotNickname, can you help?", "nickname"], + ["MoCkBoT, can you help?", "mixed case mention"] ])('should respond if the bot %s is included in the message', async (content) => { mockMessage.content = content; const result = await messageManager["_shouldRespond"](mockMessage, {}); expect(result).toBe(true); }); + + it('should process audio attachments', async () => { + vi.spyOn(Object.getPrototypeOf(messageManager), '_shouldRespond').mockReturnValueOnce(false); + vi.spyOn(messageManager, 'processMessageMedia').mockReturnValueOnce( + Promise.resolve({ processedContent: '', attachments: [] }) + ); + + const myVariable = new Collection([ + [ + 'mock-attachment-id', + { + attachment: 'https://www.example.mp3', + name: 'mock-attachment.mp3', + contentType: 'audio/mpeg', + } + ] + ]); + + mockMessage.attachments = myVariable; + + const processAttachmentsMock = vi.fn().mockResolvedValue([]); + + const mockAttachmentManager = { + processAttachments: processAttachmentsMock + } as unknown as typeof messageManager["attachmentManager"]; // ✅ Correct typing + + // Override the private property with a mock + Object.defineProperty(messageManager, 'attachmentManager', { + value: mockAttachmentManager, + writable: true + }); + + await messageManager.handleMessage(mockMessage); + + expect(processAttachmentsMock).toHaveBeenCalled(); + }); }); From 94b9a1f7c05291f13419d0c9d66d26be53c011bb Mon Sep 17 00:00:00 2001 From: Ting Chien Meng Date: Thu, 13 Feb 2025 18:24:31 +0800 Subject: [PATCH 12/24] correct des --- packages/plugin-discord/__tests__/message.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/plugin-discord/__tests__/message.test.ts b/packages/plugin-discord/__tests__/message.test.ts index 06d04859265..01143aea0f2 100644 --- a/packages/plugin-discord/__tests__/message.test.ts +++ b/packages/plugin-discord/__tests__/message.test.ts @@ -155,7 +155,7 @@ describe('Discord MessageManager', () => { ["MockBot#0001, respond please.", "tag"], ["MockBotNickname, can you help?", "nickname"], ["MoCkBoT, can you help?", "mixed case mention"] - ])('should respond if the bot %s is included in the message', async (content) => { + ])('should respond if the bot name is included in the message', async (content) => { mockMessage.content = content; const result = await messageManager["_shouldRespond"](mockMessage, {}); From de4ef79dde699f8cfba517d60467c581198b3436 Mon Sep 17 00:00:00 2001 From: Ting Chien Meng Date: Thu, 13 Feb 2025 22:09:35 +0800 Subject: [PATCH 13/24] add test suite --- packages/plugin-discord/src/index.ts | 19 +--- packages/plugin-discord/src/test-suite.ts | 118 ++++++++++++++++++++++ 2 files changed, 120 insertions(+), 17 deletions(-) create mode 100644 packages/plugin-discord/src/test-suite.ts diff --git a/packages/plugin-discord/src/index.ts b/packages/plugin-discord/src/index.ts index 50e4ed1b732..80535836ca4 100644 --- a/packages/plugin-discord/src/index.ts +++ b/packages/plugin-discord/src/index.ts @@ -31,6 +31,7 @@ import reply from "./actions/reply.ts"; import type { IDiscordClient } from "./types.ts"; import { VoiceManager } from "./voice.ts"; import { validateDiscordConfig, DiscordConfig } from "./environment.ts"; +import { DiscordTestSuite } from "./test-suite.ts"; export class DiscordClient extends EventEmitter implements IDiscordClient { apiToken: string; @@ -388,22 +389,6 @@ const DiscordClientInterface: ElizaClient = { }, }; -const testSuite: TestSuite = { - name: "discord", - tests: [ - { - name: "test creating discord client", - fn: async (runtime: IAgentRuntime) => { - const discordConfig: DiscordConfig = await validateDiscordConfig( - runtime - ); - const discordClient = new DiscordClient(runtime, discordConfig); - console.log("Created a discord client"); - }, - }, - ], -}; - const discordPlugin: Plugin = { name: "discord", description: "Discord client plugin", @@ -418,6 +403,6 @@ const discordPlugin: Plugin = { transcribe_media, ], providers: [channelStateProvider, voiceStateProvider], - tests: [testSuite], + tests: [new DiscordTestSuite()], }; export default discordPlugin; diff --git a/packages/plugin-discord/src/test-suite.ts b/packages/plugin-discord/src/test-suite.ts new file mode 100644 index 00000000000..a37c7e7e0e6 --- /dev/null +++ b/packages/plugin-discord/src/test-suite.ts @@ -0,0 +1,118 @@ +import { + logger, + stringToUuid, + type TestSuite, + type Character, + type Client as ElizaClient, + type IAgentRuntime, + type Plugin, +} from "@elizaos/core"; + +import { DiscordClient } from "./index.ts"; +import { DiscordConfig, validateDiscordConfig } from "./environment"; +import { sendMessageInChunks } from "./utils.ts"; +import { ChannelType, Events, TextChannel } from "discord.js"; + +export class DiscordTestSuite implements TestSuite { + name = "discord"; + private discordClient: DiscordClient | null = null; + tests: { name: string; fn: (runtime: IAgentRuntime) => Promise }[]; + + constructor() { + this.tests = [ + { + name: "test creating discord client", + fn: this.testCreatingDiscordClient.bind(this), + }, + { + name: "test sending message", + fn: this.testSendingTextMessage.bind(this), + }, + ]; + } + + async testCreatingDiscordClient(runtime: IAgentRuntime) { + try { + if (!this.discordClient) { + const discordConfig: DiscordConfig = await validateDiscordConfig( + runtime + ); + this.discordClient = new DiscordClient(runtime, discordConfig); + await new Promise((resolve, reject) => { + this.discordClient.client.once(Events.ClientReady, resolve); + this.discordClient.client.once(Events.Error, reject); + }); + } else { + logger.info("Reusing existing DiscordClient instance."); + } + logger.success("DiscordClient successfully initialized."); + } catch (error) { + throw new Error("Error in test creating Discord client:", error); + } + } + + async testSendingTextMessage(runtime: IAgentRuntime) { + try { + let channel: TextChannel | null = null; + let channelId = process.env.DISCORD_VOICE_CHANNEL_ID || null; + + if (!channelId) { + const guilds = await this.discordClient.client.guilds.fetch(); + for (const [, guild] of guilds) { + const fullGuild = await guild.fetch(); + const textChannels = fullGuild.channels.cache + .filter((c) => c.type === ChannelType.GuildText) + .values(); + channel = textChannels.next().value as TextChannel; + if (channel) break; // Stop if we found a valid channel + } + + if (!channel) { + logger.warn("No suitable text channel found to send the message."); + return; + } + } else { + const fetchedChannel = await this.discordClient.client.channels.fetch( + channelId + ); + if (fetchedChannel && fetchedChannel.isTextBased()) { + channel = fetchedChannel as TextChannel; + } else { + logger.warn( + `Provided channel ID (${channelId}) is invalid or not a text channel.` + ); + return; + } + } + + if (!channel) { + logger.warn( + "Failed to determine a valid channel for sending the message." + ); + return; + } + + await this.sendMessageToChannel(channel, "Testing sending message"); + } catch (error) { + logger.error("Error in sending text message:", error); + } + } + + async sendMessageToChannel(channel: TextChannel, messageContent: string) { + try { + if (!channel || !channel.isTextBased()) { + console.error("Channel is not a text-based channel or does not exist."); + return; + } + + await sendMessageInChunks( + channel as TextChannel, + messageContent, + null, + null + ); + } catch (error) { + console.error("Error sending message:", error); + } + } +} From cc975df7e66045fd9c2083706fed7846b75fcda2 Mon Sep 17 00:00:00 2001 From: Ting Chien Meng Date: Thu, 13 Feb 2025 22:17:13 +0800 Subject: [PATCH 14/24] add join channel --- packages/plugin-discord/src/test-suite.ts | 62 +++++++++++++++++++---- 1 file changed, 52 insertions(+), 10 deletions(-) diff --git a/packages/plugin-discord/src/test-suite.ts b/packages/plugin-discord/src/test-suite.ts index a37c7e7e0e6..aeed591a253 100644 --- a/packages/plugin-discord/src/test-suite.ts +++ b/packages/plugin-discord/src/test-suite.ts @@ -1,17 +1,9 @@ -import { - logger, - stringToUuid, - type TestSuite, - type Character, - type Client as ElizaClient, - type IAgentRuntime, - type Plugin, -} from "@elizaos/core"; - +import { logger, type TestSuite, type IAgentRuntime } from "@elizaos/core"; import { DiscordClient } from "./index.ts"; import { DiscordConfig, validateDiscordConfig } from "./environment"; import { sendMessageInChunks } from "./utils.ts"; import { ChannelType, Events, TextChannel } from "discord.js"; +import { joinVoiceChannel } from "@discordjs/voice"; export class DiscordTestSuite implements TestSuite { name = "discord"; @@ -24,6 +16,10 @@ export class DiscordTestSuite implements TestSuite { name: "test creating discord client", fn: this.testCreatingDiscordClient.bind(this), }, + { + name: "test joining voice channel", + fn: this.testJoiningVoiceChannel.bind(this), + }, { name: "test sending message", fn: this.testSendingTextMessage.bind(this), @@ -51,6 +47,52 @@ export class DiscordTestSuite implements TestSuite { } } + async testJoiningVoiceChannel(runtime: IAgentRuntime) { + try { + let voiceChannel = null; + let channelId = process.env.DISCORD_VOICE_CHANNEL_ID || null; + + if (!channelId) { + const guilds = await this.discordClient.client.guilds.fetch(); + for (const [, guild] of guilds) { + const fullGuild = await guild.fetch(); + const voiceChannels = fullGuild.channels.cache + .filter((c) => c.type === ChannelType.GuildVoice) + .values(); + voiceChannel = voiceChannels.next().value; + if (voiceChannel) break; + } + + if (!voiceChannel) { + logger.warn("No suitable voice channel found to join."); + return; + } + } else { + voiceChannel = await this.discordClient.client.channels.fetch( + channelId + ); + } + + if (!voiceChannel || voiceChannel.type !== ChannelType.GuildVoice) { + logger.error("Invalid voice channel."); + return; + } + + joinVoiceChannel({ + channelId: voiceChannel.id, + guildId: voiceChannel.guild.id, + adapterCreator: voiceChannel.guild.voiceAdapterCreator as any, + selfDeaf: false, + selfMute: false, + group: this.discordClient.client.user.id, + }); + + logger.success(`Joined voice channel: ${voiceChannel.id}`); + } catch (error) { + console.error("Error joining voice channel:", error); + } + } + async testSendingTextMessage(runtime: IAgentRuntime) { try { let channel: TextChannel | null = null; From 77d9aef10ec839e7b789dabe05ffa580e291a7b9 Mon Sep 17 00:00:00 2001 From: Ting Chien Meng Date: Thu, 13 Feb 2025 22:52:57 +0800 Subject: [PATCH 15/24] add text to speech test --- packages/plugin-discord/src/index.ts | 4 +- packages/plugin-discord/src/test-suite.ts | 96 ++++++++++++++++++++--- packages/plugin-discord/src/voice.ts | 2 +- 3 files changed, 89 insertions(+), 13 deletions(-) diff --git a/packages/plugin-discord/src/index.ts b/packages/plugin-discord/src/index.ts index 80535836ca4..6c1ffc33f03 100644 --- a/packages/plugin-discord/src/index.ts +++ b/packages/plugin-discord/src/index.ts @@ -38,8 +38,8 @@ export class DiscordClient extends EventEmitter implements IDiscordClient { client: Client; runtime: IAgentRuntime; character: Character; - private messageManager: MessageManager; - private voiceManager: VoiceManager; + messageManager: MessageManager; + voiceManager: VoiceManager; constructor(runtime: IAgentRuntime, discordConfig: DiscordConfig) { super(); diff --git a/packages/plugin-discord/src/test-suite.ts b/packages/plugin-discord/src/test-suite.ts index aeed591a253..82da1eeb2e8 100644 --- a/packages/plugin-discord/src/test-suite.ts +++ b/packages/plugin-discord/src/test-suite.ts @@ -1,9 +1,19 @@ -import { logger, type TestSuite, type IAgentRuntime } from "@elizaos/core"; +import { + logger, + type TestSuite, + type IAgentRuntime, + ModelClass, +} from "@elizaos/core"; import { DiscordClient } from "./index.ts"; import { DiscordConfig, validateDiscordConfig } from "./environment"; import { sendMessageInChunks } from "./utils.ts"; import { ChannelType, Events, TextChannel } from "discord.js"; -import { joinVoiceChannel } from "@discordjs/voice"; +import { + createAudioPlayer, + NoSubscriberBehavior, + createAudioResource, + AudioPlayerStatus, +} from "@discordjs/voice"; export class DiscordTestSuite implements TestSuite { name = "discord"; @@ -20,6 +30,10 @@ export class DiscordTestSuite implements TestSuite { name: "test joining voice channel", fn: this.testJoiningVoiceChannel.bind(this), }, + { + name: "test text-to-speech playback", + fn: this.testTextToSpeechPlayback.bind(this), + }, { name: "test sending message", fn: this.testSendingTextMessage.bind(this), @@ -78,14 +92,7 @@ export class DiscordTestSuite implements TestSuite { return; } - joinVoiceChannel({ - channelId: voiceChannel.id, - guildId: voiceChannel.guild.id, - adapterCreator: voiceChannel.guild.voiceAdapterCreator as any, - selfDeaf: false, - selfMute: false, - group: this.discordClient.client.user.id, - }); + await this.discordClient.voiceManager.joinChannel(voiceChannel); logger.success(`Joined voice channel: ${voiceChannel.id}`); } catch (error) { @@ -93,6 +100,75 @@ export class DiscordTestSuite implements TestSuite { } } + async testTextToSpeechPlayback(runtime: IAgentRuntime) { + try { + let guildId = this.discordClient.client.guilds.cache.find( + (guild) => guild.members.me?.voice.channelId + )?.id; + + if (!guildId) { + logger.warn( + "Bot is not connected to a voice channel. Attempting to join one..." + ); + + await this.testJoiningVoiceChannel(runtime); + + guildId = this.discordClient.client.guilds.cache.find( + (guild) => guild.members.me?.voice.channelId + )?.id; + + if (!guildId) { + logger.error("Failed to join a voice channel. TTS playback aborted."); + return; + } + } + + const connection = + this.discordClient.voiceManager.getVoiceConnection(guildId); + if (!connection) { + logger.warn("No active voice connection found for the bot."); + return; + } + + const responseStream = await runtime.useModel( + ModelClass.TEXT_TO_SPEECH, + `Hi! I'm ${runtime.character.name}! How are you doing today?` + ); + + if (!responseStream) { + logger.error("TTS response stream is null or undefined."); + return; + } + + const audioPlayer = createAudioPlayer({ + behaviors: { + noSubscriber: NoSubscriberBehavior.Pause, + }, + }); + + const audioResource = createAudioResource(responseStream); + + audioPlayer.play(audioResource); + connection.subscribe(audioPlayer); + + logger.success("TTS playback started successfully."); + + await new Promise((resolve, reject) => { + audioPlayer.once(AudioPlayerStatus.Idle, () => { + logger.info("TTS playback finished."); + resolve(); + }); + + audioPlayer.once("error", (error) => { + logger.error("TTS playback error:", error); + reject(error); + }); + }); + } catch (error) { + console.error("Error in TTS playback test:", error); + } + } + async testSendingTextMessage(runtime: IAgentRuntime) { try { let channel: TextChannel | null = null; diff --git a/packages/plugin-discord/src/voice.ts b/packages/plugin-discord/src/voice.ts index 2fbc9240033..1886d8c6142 100644 --- a/packages/plugin-discord/src/voice.ts +++ b/packages/plugin-discord/src/voice.ts @@ -327,7 +327,7 @@ export class VoiceManager extends EventEmitter { } } - private getVoiceConnection(guildId: string) { + getVoiceConnection(guildId: string) { const connections = getVoiceConnections(this.client.user.id); if (!connections) { return; From 4bad07f1a1e2735087b8797d8b63b89a9a1ec91f Mon Sep 17 00:00:00 2001 From: Ting Chien Meng Date: Thu, 13 Feb 2025 22:54:46 +0800 Subject: [PATCH 16/24] clean code --- .../plugin-discord/__tests__/message.test.ts | 392 +++++++++--------- 1 file changed, 200 insertions(+), 192 deletions(-) diff --git a/packages/plugin-discord/__tests__/message.test.ts b/packages/plugin-discord/__tests__/message.test.ts index 01143aea0f2..79b05103516 100644 --- a/packages/plugin-discord/__tests__/message.test.ts +++ b/packages/plugin-discord/__tests__/message.test.ts @@ -1,200 +1,208 @@ -import { describe, it, expect, vi, beforeEach } from 'vitest'; +import { describe, it, expect, vi, beforeEach } from "vitest"; import { MessageManager } from "../src/messages.ts"; -import { ChannelType, Client, Collection, AttachmentBuilder } from 'discord.js'; -import { type IAgentRuntime, ServiceType } from '@elizaos/core'; -import type { VoiceManager } from '../src/voice'; - - - -vi.mock('@elizaos/core', () => ({ - logger: { - info: vi.fn(), - error: vi.fn(), - debug: vi.fn(), - }, - stringToUuid: (str: string) => str, - messageCompletionFooter: '# INSTRUCTIONS: Choose the best response for the agent.', - shouldRespondFooter: '# INSTRUCTIONS: Choose if the agent should respond.', - generateMessageResponse: vi.fn(), - generateShouldRespond: vi.fn().mockResolvedValue("IGNORE"), // Prevent API calls by always returning "IGNORE" - composeContext: vi.fn(), - ModelClass: { - TEXT_SMALL: 'TEXT_SMALL' - }, - ServiceType: { // ✅ Add this missing mock - VIDEO: 'VIDEO', - BROWSER: 'BROWSER' - } +import { ChannelType, Client, Collection, AttachmentBuilder } from "discord.js"; +import { type IAgentRuntime, ServiceType } from "@elizaos/core"; +import type { VoiceManager } from "../src/voice"; + +vi.mock("@elizaos/core", () => ({ + logger: { + info: vi.fn(), + error: vi.fn(), + debug: vi.fn(), + }, + stringToUuid: (str: string) => str, + messageCompletionFooter: + "# INSTRUCTIONS: Choose the best response for the agent.", + shouldRespondFooter: "# INSTRUCTIONS: Choose if the agent should respond.", + generateMessageResponse: vi.fn(), + generateShouldRespond: vi.fn().mockResolvedValue("IGNORE"), // Prevent API calls by always returning "IGNORE" + composeContext: vi.fn(), + ModelClass: { + TEXT_SMALL: "TEXT_SMALL", + }, + ServiceType: { + VIDEO: "VIDEO", + BROWSER: "BROWSER", + }, })); -describe('Discord MessageManager', () => { - let mockRuntime: IAgentRuntime; - let mockClient: Client; - let mockDiscordClient: { client: Client; runtime: IAgentRuntime }; - let mockVoiceManager: VoiceManager; - let mockMessage: any; - let messageManager: MessageManager; - - beforeEach(() => { - vi.clearAllMocks(); - mockRuntime = { - character: { - name: 'TestBot', - templates: {}, - clientConfig: { - discord: { - allowedChannelIds: ['mock-channal-id'], - shouldIgnoreBotMessages: true, - shouldIgnoreDirectMessages: true - } - } - }, - evaluate: vi.fn(), - composeState: vi.fn(), - ensureConnection: vi.fn(), - ensureUserExists: vi.fn(), - messageManager: { - createMemory: vi.fn(), - addEmbeddingToMemory: vi.fn() - }, - databaseAdapter: { - getParticipantUserState: vi.fn().mockResolvedValue('ACTIVE'), - log: vi.fn() - }, - processActions: vi.fn() - } as unknown as IAgentRuntime; - - mockClient = new Client({ intents: [] }); - mockClient.user = { - id: 'mock-bot-id', - username: 'MockBot', - tag: 'MockBot#0001', - displayName: 'MockBotDisplay', - } as any; - - mockDiscordClient = { - client: mockClient, - runtime: mockRuntime - }; - - mockVoiceManager = { - playAudioStream: vi.fn() - } as unknown as VoiceManager; - - messageManager = new MessageManager(mockDiscordClient, mockVoiceManager); - - const guild = { - members: { - cache: { - get: vi.fn().mockReturnValue({ - nickname: 'MockBotNickname', - permissions: { - has: vi.fn().mockReturnValue(true), // Bot has permissions - } - }) - } - } - } - mockMessage = { - content: 'Hello, MockBot!', - author: { - id: 'mock-user-id', - username: 'MockUser', - bot: false - }, - guild, - channel: { - id: 'mock-channal-id', - type: ChannelType.GuildText, - send: vi.fn(), - guild, - client: { - user: mockClient.user - }, - permissionsFor: vi.fn().mockReturnValue({ - has: vi.fn().mockReturnValue(true) - }) +describe("Discord MessageManager", () => { + let mockRuntime: IAgentRuntime; + let mockClient: Client; + let mockDiscordClient: { client: Client; runtime: IAgentRuntime }; + let mockVoiceManager: VoiceManager; + let mockMessage: any; + let messageManager: MessageManager; + + beforeEach(() => { + vi.clearAllMocks(); + mockRuntime = { + character: { + name: "TestBot", + templates: {}, + clientConfig: { + discord: { + allowedChannelIds: ["mock-channal-id"], + shouldIgnoreBotMessages: true, + shouldIgnoreDirectMessages: true, + }, + }, + }, + evaluate: vi.fn(), + composeState: vi.fn(), + ensureConnection: vi.fn(), + ensureUserExists: vi.fn(), + messageManager: { + createMemory: vi.fn(), + addEmbeddingToMemory: vi.fn(), + }, + databaseAdapter: { + getParticipantUserState: vi.fn().mockResolvedValue("ACTIVE"), + log: vi.fn(), + }, + processActions: vi.fn(), + } as unknown as IAgentRuntime; + + mockClient = new Client({ intents: [] }); + mockClient.user = { + id: "mock-bot-id", + username: "MockBot", + tag: "MockBot#0001", + displayName: "MockBotDisplay", + } as any; + + mockDiscordClient = { + client: mockClient, + runtime: mockRuntime, + }; + + mockVoiceManager = { + playAudioStream: vi.fn(), + } as unknown as VoiceManager; + + messageManager = new MessageManager(mockDiscordClient, mockVoiceManager); + + const guild = { + members: { + cache: { + get: vi.fn().mockReturnValue({ + nickname: "MockBotNickname", + permissions: { + has: vi.fn().mockReturnValue(true), // Bot has permissions }, - id: 'mock-message-id', - createdTimestamp: Date.now(), - mentions: { - has: vi.fn().mockReturnValue(false) - }, - reference: null, - attachments: [] - }; - }); - - it('should initialize MessageManager', () => { - expect(messageManager).toBeDefined(); - }); - - it('should process user messages', async () => { - // Prevent further message processing after response check - vi.spyOn(Object.getPrototypeOf(messageManager), '_shouldRespond').mockReturnValueOnce(false); - - await messageManager.handleMessage(mockMessage); - expect(mockRuntime.ensureConnection).toHaveBeenCalled(); - expect(mockRuntime.messageManager.createMemory).toHaveBeenCalled(); - }); - - it('should ignore bot messages', async () => { - mockMessage.author.bot = true; - await messageManager.handleMessage(mockMessage); - expect(mockRuntime.ensureConnection).not.toHaveBeenCalled(); - }); - - it('should ignore messages from restricted channels', async () => { - mockMessage.channel.id = 'undefined-channel-id'; - await messageManager.handleMessage(mockMessage); - expect(mockRuntime.ensureConnection).not.toHaveBeenCalled(); + }), + }, + }, + }; + mockMessage = { + content: "Hello, MockBot!", + author: { + id: "mock-user-id", + username: "MockUser", + bot: false, + }, + guild, + channel: { + id: "mock-channal-id", + type: ChannelType.GuildText, + send: vi.fn(), + guild, + client: { + user: mockClient.user, + }, + permissionsFor: vi.fn().mockReturnValue({ + has: vi.fn().mockReturnValue(true), + }), + }, + id: "mock-message-id", + createdTimestamp: Date.now(), + mentions: { + has: vi.fn().mockReturnValue(false), + }, + reference: null, + attachments: [], + }; + }); + + it("should initialize MessageManager", () => { + expect(messageManager).toBeDefined(); + }); + + it("should process user messages", async () => { + // Prevent further message processing after response check + vi.spyOn( + Object.getPrototypeOf(messageManager), + "_shouldRespond" + ).mockReturnValueOnce(false); + + await messageManager.handleMessage(mockMessage); + expect(mockRuntime.ensureConnection).toHaveBeenCalled(); + expect(mockRuntime.messageManager.createMemory).toHaveBeenCalled(); + }); + + it("should ignore bot messages", async () => { + mockMessage.author.bot = true; + await messageManager.handleMessage(mockMessage); + expect(mockRuntime.ensureConnection).not.toHaveBeenCalled(); + }); + + it("should ignore messages from restricted channels", async () => { + mockMessage.channel.id = "undefined-channel-id"; + await messageManager.handleMessage(mockMessage); + expect(mockRuntime.ensureConnection).not.toHaveBeenCalled(); + }); + + it.each([ + ["Hey MockBot, are you there?", "username"], + ["MockBot#0001, respond please.", "tag"], + ["MockBotNickname, can you help?", "nickname"], + ["MoCkBoT, can you help?", "mixed case mention"], + ])( + "should respond if the bot name is included in the message", + async (content) => { + mockMessage.content = content; + + const result = await messageManager["_shouldRespond"](mockMessage, {}); + expect(result).toBe(true); + } + ); + + it("should process audio attachments", async () => { + vi.spyOn( + Object.getPrototypeOf(messageManager), + "_shouldRespond" + ).mockReturnValueOnce(false); + vi.spyOn(messageManager, "processMessageMedia").mockReturnValueOnce( + Promise.resolve({ processedContent: "", attachments: [] }) + ); + + const myVariable = new Collection([ + [ + "mock-attachment-id", + { + attachment: "https://www.example.mp3", + name: "mock-attachment.mp3", + contentType: "audio/mpeg", + }, + ], + ]); + + mockMessage.attachments = myVariable; + + const processAttachmentsMock = vi.fn().mockResolvedValue([]); + + const mockAttachmentManager = { + processAttachments: processAttachmentsMock, + } as unknown as (typeof messageManager)["attachmentManager"]; // ✅ Correct typing + + // Override the private property with a mock + Object.defineProperty(messageManager, "attachmentManager", { + value: mockAttachmentManager, + writable: true, }); - it.each([ - ["Hey MockBot, are you there?", "username"], - ["MockBot#0001, respond please.", "tag"], - ["MockBotNickname, can you help?", "nickname"], - ["MoCkBoT, can you help?", "mixed case mention"] - ])('should respond if the bot name is included in the message', async (content) => { - mockMessage.content = content; - - const result = await messageManager["_shouldRespond"](mockMessage, {}); - expect(result).toBe(true); - }); + await messageManager.handleMessage(mockMessage); - it('should process audio attachments', async () => { - vi.spyOn(Object.getPrototypeOf(messageManager), '_shouldRespond').mockReturnValueOnce(false); - vi.spyOn(messageManager, 'processMessageMedia').mockReturnValueOnce( - Promise.resolve({ processedContent: '', attachments: [] }) - ); - - const myVariable = new Collection([ - [ - 'mock-attachment-id', - { - attachment: 'https://www.example.mp3', - name: 'mock-attachment.mp3', - contentType: 'audio/mpeg', - } - ] - ]); - - mockMessage.attachments = myVariable; - - const processAttachmentsMock = vi.fn().mockResolvedValue([]); - - const mockAttachmentManager = { - processAttachments: processAttachmentsMock - } as unknown as typeof messageManager["attachmentManager"]; // ✅ Correct typing - - // Override the private property with a mock - Object.defineProperty(messageManager, 'attachmentManager', { - value: mockAttachmentManager, - writable: true - }); - - await messageManager.handleMessage(mockMessage); - - expect(processAttachmentsMock).toHaveBeenCalled(); - }); + expect(processAttachmentsMock).toHaveBeenCalled(); + }); }); From af71043e0186147824edd2d965d6268f0b91f696 Mon Sep 17 00:00:00 2001 From: Ting Chien Meng Date: Thu, 13 Feb 2025 22:58:40 +0800 Subject: [PATCH 17/24] clean code --- packages/plugin-discord/__tests__/message.test.ts | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/packages/plugin-discord/__tests__/message.test.ts b/packages/plugin-discord/__tests__/message.test.ts index 79b05103516..f05b665d8ff 100644 --- a/packages/plugin-discord/__tests__/message.test.ts +++ b/packages/plugin-discord/__tests__/message.test.ts @@ -162,7 +162,10 @@ describe("Discord MessageManager", () => { async (content) => { mockMessage.content = content; - const result = await messageManager["_shouldRespond"](mockMessage, {}); + const result = await messageManager["_shouldRespond"]( + mockMessage, + {} as any + ); expect(result).toBe(true); } ); @@ -193,7 +196,7 @@ describe("Discord MessageManager", () => { const mockAttachmentManager = { processAttachments: processAttachmentsMock, - } as unknown as (typeof messageManager)["attachmentManager"]; // ✅ Correct typing + } as unknown as (typeof messageManager)["attachmentManager"]; // Override the private property with a mock Object.defineProperty(messageManager, "attachmentManager", { From 9c16b075131ba41b7c685a9950ad76e0fef86540 Mon Sep 17 00:00:00 2001 From: Ting Chien Meng Date: Thu, 13 Feb 2025 23:12:37 +0800 Subject: [PATCH 18/24] small fix --- packages/plugin-discord/__tests__/environment.test.ts | 3 +-- packages/plugin-discord/__tests__/message.test.ts | 4 ++-- packages/plugin-discord/src/environment.ts | 2 +- packages/plugin-discord/src/test-suite.ts | 2 +- 4 files changed, 5 insertions(+), 6 deletions(-) diff --git a/packages/plugin-discord/__tests__/environment.test.ts b/packages/plugin-discord/__tests__/environment.test.ts index 78add535707..a250a4833ed 100644 --- a/packages/plugin-discord/__tests__/environment.test.ts +++ b/packages/plugin-discord/__tests__/environment.test.ts @@ -26,13 +26,12 @@ describe('Discord Environment Configuration', () => { const invalidRuntime = { ...mockRuntime, env: { - ...mockRuntime.env, DISCORD_API_TOKEN: undefined, }, } as IAgentRuntime; await expect(validateDiscordConfig(invalidRuntime)).rejects.toThrowError( - 'Discord configuration validation failed:\nDISCORD_API_TOKEN: Discord API token is required' + 'Discord configuration validation failed:\nDISCORD_API_TOKEN: Expected string, received null' ); }); }); diff --git a/packages/plugin-discord/__tests__/message.test.ts b/packages/plugin-discord/__tests__/message.test.ts index f05b665d8ff..b2b498b70de 100644 --- a/packages/plugin-discord/__tests__/message.test.ts +++ b/packages/plugin-discord/__tests__/message.test.ts @@ -1,7 +1,7 @@ import { describe, it, expect, vi, beforeEach } from "vitest"; import { MessageManager } from "../src/messages.ts"; -import { ChannelType, Client, Collection, AttachmentBuilder } from "discord.js"; -import { type IAgentRuntime, ServiceType } from "@elizaos/core"; +import { ChannelType, Client, Collection } from "discord.js"; +import { type IAgentRuntime } from "@elizaos/core"; import type { VoiceManager } from "../src/voice"; vi.mock("@elizaos/core", () => ({ diff --git a/packages/plugin-discord/src/environment.ts b/packages/plugin-discord/src/environment.ts index 82634066ede..a99dbb1e172 100644 --- a/packages/plugin-discord/src/environment.ts +++ b/packages/plugin-discord/src/environment.ts @@ -13,7 +13,7 @@ export async function validateDiscordConfig( try { const config = { DISCORD_API_TOKEN: - runtime.getSetting("DISCORD_API_TOKEN") || "", + runtime.getSetting("DISCORD_API_TOKEN"), }; return discordEnvSchema.parse(config); diff --git a/packages/plugin-discord/src/test-suite.ts b/packages/plugin-discord/src/test-suite.ts index 82da1eeb2e8..9cfdc32c97c 100644 --- a/packages/plugin-discord/src/test-suite.ts +++ b/packages/plugin-discord/src/test-suite.ts @@ -57,7 +57,7 @@ export class DiscordTestSuite implements TestSuite { } logger.success("DiscordClient successfully initialized."); } catch (error) { - throw new Error("Error in test creating Discord client:", error); + throw new Error(`Error in test creating Discord client: ${error}`); } } From ab018de823358b765965befcc0d6749505cd2385 Mon Sep 17 00:00:00 2001 From: Ting Chien Meng Date: Thu, 13 Feb 2025 23:59:35 +0800 Subject: [PATCH 19/24] reuse client --- packages/plugin-discord/src/test-suite.ts | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/packages/plugin-discord/src/test-suite.ts b/packages/plugin-discord/src/test-suite.ts index 9cfdc32c97c..89de198c49e 100644 --- a/packages/plugin-discord/src/test-suite.ts +++ b/packages/plugin-discord/src/test-suite.ts @@ -43,6 +43,13 @@ export class DiscordTestSuite implements TestSuite { async testCreatingDiscordClient(runtime: IAgentRuntime) { try { + const existingPlugin = runtime.getClient("discord"); + + if (existingPlugin) { + // Reuse the existing DiscordClient if available + this.discordClient = existingPlugin as any; + logger.info("Reusing existing DiscordClient instance."); + } else { if (!this.discordClient) { const discordConfig: DiscordConfig = await validateDiscordConfig( runtime @@ -56,6 +63,7 @@ export class DiscordTestSuite implements TestSuite { logger.info("Reusing existing DiscordClient instance."); } logger.success("DiscordClient successfully initialized."); + } } catch (error) { throw new Error(`Error in test creating Discord client: ${error}`); } From 3fad682983e8278ea74529e34262b96f3407fafa Mon Sep 17 00:00:00 2001 From: Ting Chien Meng Date: Thu, 13 Feb 2025 23:59:52 +0800 Subject: [PATCH 20/24] pretty --- packages/plugin-discord/src/test-suite.ts | 28 +++++++++++------------ 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/packages/plugin-discord/src/test-suite.ts b/packages/plugin-discord/src/test-suite.ts index 89de198c49e..a2551fdffb8 100644 --- a/packages/plugin-discord/src/test-suite.ts +++ b/packages/plugin-discord/src/test-suite.ts @@ -45,25 +45,25 @@ export class DiscordTestSuite implements TestSuite { try { const existingPlugin = runtime.getClient("discord"); - if (existingPlugin) { + if (existingPlugin) { // Reuse the existing DiscordClient if available this.discordClient = existingPlugin as any; logger.info("Reusing existing DiscordClient instance."); - } else { - if (!this.discordClient) { - const discordConfig: DiscordConfig = await validateDiscordConfig( - runtime - ); - this.discordClient = new DiscordClient(runtime, discordConfig); - await new Promise((resolve, reject) => { - this.discordClient.client.once(Events.ClientReady, resolve); - this.discordClient.client.once(Events.Error, reject); - }); } else { - logger.info("Reusing existing DiscordClient instance."); + if (!this.discordClient) { + const discordConfig: DiscordConfig = await validateDiscordConfig( + runtime + ); + this.discordClient = new DiscordClient(runtime, discordConfig); + await new Promise((resolve, reject) => { + this.discordClient.client.once(Events.ClientReady, resolve); + this.discordClient.client.once(Events.Error, reject); + }); + } else { + logger.info("Reusing existing DiscordClient instance."); + } + logger.success("DiscordClient successfully initialized."); } - logger.success("DiscordClient successfully initialized."); - } } catch (error) { throw new Error(`Error in test creating Discord client: ${error}`); } From 9c9c0071ab3bb023f5c8056ac815f0f74780b668 Mon Sep 17 00:00:00 2001 From: Ting Chien Meng Date: Fri, 14 Feb 2025 00:00:33 +0800 Subject: [PATCH 21/24] correct type --- packages/plugin-discord/src/test-suite.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/plugin-discord/src/test-suite.ts b/packages/plugin-discord/src/test-suite.ts index a2551fdffb8..18a399cbfe5 100644 --- a/packages/plugin-discord/src/test-suite.ts +++ b/packages/plugin-discord/src/test-suite.ts @@ -47,7 +47,7 @@ export class DiscordTestSuite implements TestSuite { if (existingPlugin) { // Reuse the existing DiscordClient if available - this.discordClient = existingPlugin as any; + this.discordClient = existingPlugin as DiscordClient; logger.info("Reusing existing DiscordClient instance."); } else { if (!this.discordClient) { From 801572ad90b1075690ebb8e4d1188056ee8ca909 Mon Sep 17 00:00:00 2001 From: Ting Chien Meng Date: Fri, 14 Feb 2025 00:30:45 +0800 Subject: [PATCH 22/24] handle no text to speech model --- packages/plugin-discord/src/test-suite.ts | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/packages/plugin-discord/src/test-suite.ts b/packages/plugin-discord/src/test-suite.ts index 18a399cbfe5..26e79de5963 100644 --- a/packages/plugin-discord/src/test-suite.ts +++ b/packages/plugin-discord/src/test-suite.ts @@ -138,10 +138,17 @@ export class DiscordTestSuite implements TestSuite { return; } - const responseStream = await runtime.useModel( - ModelClass.TEXT_TO_SPEECH, - `Hi! I'm ${runtime.character.name}! How are you doing today?` - ); + let responseStream = null; + try { + responseStream = await runtime.useModel( + ModelClass.TEXT_TO_SPEECH, + `Hi! I'm ${runtime.character.name}! How are you doing today?` + ); + } catch(error) { + logger.warn("No text to speech service found"); + return; + } + if (!responseStream) { logger.error("TTS response stream is null or undefined."); From 53d85aaed0c40e3fa2d104089354a8ce86f59cbe Mon Sep 17 00:00:00 2001 From: Ting Chien Meng Date: Fri, 14 Feb 2025 00:52:05 +0800 Subject: [PATCH 23/24] test handle message in message manager --- packages/plugin-discord/src/test-suite.ts | 88 ++++++++++++++++------- 1 file changed, 64 insertions(+), 24 deletions(-) diff --git a/packages/plugin-discord/src/test-suite.ts b/packages/plugin-discord/src/test-suite.ts index 26e79de5963..fe4e404fe64 100644 --- a/packages/plugin-discord/src/test-suite.ts +++ b/packages/plugin-discord/src/test-suite.ts @@ -38,6 +38,10 @@ export class DiscordTestSuite implements TestSuite { name: "test sending message", fn: this.testSendingTextMessage.bind(this), }, + { + name: "handle message in message manager", + fn: this.testHandlingMessage.bind(this), + }, ]; } @@ -104,7 +108,7 @@ export class DiscordTestSuite implements TestSuite { logger.success(`Joined voice channel: ${voiceChannel.id}`); } catch (error) { - console.error("Error joining voice channel:", error); + logger.error("Error joining voice channel:", error); } } @@ -180,15 +184,55 @@ export class DiscordTestSuite implements TestSuite { }); }); } catch (error) { - console.error("Error in TTS playback test:", error); + logger.error("Error in TTS playback test:", error); } } async testSendingTextMessage(runtime: IAgentRuntime) { try { - let channel: TextChannel | null = null; - let channelId = process.env.DISCORD_VOICE_CHANNEL_ID || null; + const channel = await this.getTextChannel(); + if (!channel) return; + + await this.sendMessageToChannel(channel, "Testing sending message"); + } catch (error) { + logger.error("Error in sending text message:", error); + } + } + + async testHandlingMessage(runtime: IAgentRuntime) { + try { + const channel = await this.getTextChannel(); + if (!channel) return; + const fakeMessage = { + content: `Hello, ${runtime.character.name}! How are you?`, + author: { + id: "mock-user-id", + username: "MockUser", + bot: false, + }, + channel, + id: "mock-message-id", + createdTimestamp: Date.now(), + mentions: { + has: () => false, + }, + reference: null, + attachments: [], + }; + await this.discordClient.messageManager.handleMessage(fakeMessage as any); + + } catch (error) { + logger.error("Error in sending text message:", error); + } + } + + + async getTextChannel(): Promise { + try { + let channel: TextChannel | null = null; + const channelId = process.env.DISCORD_TEXT_CHANNEL_ID || null; + if (!channelId) { const guilds = await this.discordClient.client.guilds.fetch(); for (const [, guild] of guilds) { @@ -199,42 +243,38 @@ export class DiscordTestSuite implements TestSuite { channel = textChannels.next().value as TextChannel; if (channel) break; // Stop if we found a valid channel } - + if (!channel) { - logger.warn("No suitable text channel found to send the message."); - return; + logger.warn("No suitable text channel found."); + return null; } } else { - const fetchedChannel = await this.discordClient.client.channels.fetch( - channelId - ); + const fetchedChannel = await this.discordClient.client.channels.fetch(channelId); if (fetchedChannel && fetchedChannel.isTextBased()) { channel = fetchedChannel as TextChannel; } else { - logger.warn( - `Provided channel ID (${channelId}) is invalid or not a text channel.` - ); - return; + logger.warn(`Provided channel ID (${channelId}) is invalid or not a text channel.`); + return null; } } - + if (!channel) { - logger.warn( - "Failed to determine a valid channel for sending the message." - ); - return; + logger.warn("Failed to determine a valid text channel."); + return null; } - - await this.sendMessageToChannel(channel, "Testing sending message"); + + return channel; } catch (error) { - logger.error("Error in sending text message:", error); + logger.error("Error fetching text channel:", error); + return null; } } + async sendMessageToChannel(channel: TextChannel, messageContent: string) { try { if (!channel || !channel.isTextBased()) { - console.error("Channel is not a text-based channel or does not exist."); + logger.error("Channel is not a text-based channel or does not exist."); return; } @@ -245,7 +285,7 @@ export class DiscordTestSuite implements TestSuite { null ); } catch (error) { - console.error("Error sending message:", error); + logger.error("Error sending message:", error); } } } From 6d7f53e847b9b22d2b10425780b6d26e0b624ca9 Mon Sep 17 00:00:00 2001 From: Sayo Date: Thu, 13 Feb 2025 23:02:40 +0530 Subject: [PATCH 24/24] feat: add anthropic local embedding + misc (#3474) * added test for openai, anthropic embeding, added fastembed for anthropic * add todo flags added todo flag * fix semantics --- packages/plugin-anthropic/package.json | 1 + packages/plugin-anthropic/src/index.ts | 39 ++++++++++++++++++++++++-- packages/plugin-openai/src/index.ts | 12 ++++++++ 3 files changed, 50 insertions(+), 2 deletions(-) diff --git a/packages/plugin-anthropic/package.json b/packages/plugin-anthropic/package.json index 7e97ce362e1..9cd965e40f5 100644 --- a/packages/plugin-anthropic/package.json +++ b/packages/plugin-anthropic/package.json @@ -19,6 +19,7 @@ ], "dependencies": { "@ai-sdk/anthropic": "^1.1.6", + "fastembed": "^1.0.0", "@elizaos/core": "workspace:*", "tsup": "8.3.5", "zod": "3.21.4" diff --git a/packages/plugin-anthropic/src/index.ts b/packages/plugin-anthropic/src/index.ts index 62ca07e73ed..6f7e5576020 100644 --- a/packages/plugin-anthropic/src/index.ts +++ b/packages/plugin-anthropic/src/index.ts @@ -6,6 +6,7 @@ import { } from "@elizaos/core"; import { generateText } from "ai"; import { z } from "zod"; +import { EmbeddingModel, FlagEmbedding } from "fastembed"; // Define a configuration schema for the Anthropics plugin. const configSchema = z.object({ @@ -82,12 +83,46 @@ export const anthropicPlugin: Plugin = { stopSequences, }); return text; + }, + [ModelClass.TEXT_EMBEDDING]: async (runtime, text: string | null) => { + + // TODO: Make this fallback only!!! + // TODO: pass on cacheDir to FlagEmbedding.init + if (!text) return new Array(1536).fill(0); + + const model = await FlagEmbedding.init({ model: EmbeddingModel.BGESmallENV15 }); + const embedding = await model.queryEmbed(text); + + const finalEmbedding = Array.isArray(embedding) + ? ArrayBuffer.isView(embedding[0]) + ? Array.from(embedding[0] as never) + : embedding + : Array.from(embedding); + + if (!Array.isArray(finalEmbedding) || finalEmbedding[0] === undefined) { + throw new Error("Invalid embedding format"); + } + + return finalEmbedding.map(Number); } }, tests: [ { name: "anthropic_plugin_tests", tests: [ + { + name: 'anthropic_test_text_embedding', + fn: async (runtime) => { + try { + console.log("testing embedding"); + const embedding = await runtime.useModel(ModelClass.TEXT_EMBEDDING, "Hello, world!"); + console.log("embedding done", embedding); + } catch (error) { + console.error("Error in test_text_embedding:", error); + throw error; + } + } + }, { name: 'anthropic_test_text_small', fn: async (runtime) => { @@ -117,9 +152,9 @@ export const anthropicPlugin: Plugin = { if (text.length === 0) { throw new Error("Failed to generate text"); } - console.log("generated with test_text_small:", text); + console.log("generated with test_text_large:", text); } catch (error) { - console.error("Error in test_text_small:", error); + console.error("Error in test_text_large:", error); throw error; } } diff --git a/packages/plugin-openai/src/index.ts b/packages/plugin-openai/src/index.ts index bfc795010c2..87888799bde 100644 --- a/packages/plugin-openai/src/index.ts +++ b/packages/plugin-openai/src/index.ts @@ -310,6 +310,18 @@ export const openaiPlugin: Plugin = { } } }, + { + name: 'openai_test_text_embedding', + fn: async (runtime) => { + try { + const embedding = await runtime.useModel(ModelClass.TEXT_EMBEDDING, "Hello, world!"); + console.log("embedding", embedding); + } catch (error) { + console.error("Error in test_text_embedding:", error); + throw error; + } + } + }, { name: 'openai_test_text_large', fn: async (runtime) => {