Skip to content

Commit 3512475

Browse files
authored
feat: Discord autonomous agent enhancement (elizaOS#2335)
* add Discord autonomous agent * sync fixes * fix redundancy * fix redundancy * fix
1 parent 3a2fa76 commit 3512475

File tree

4 files changed

+328
-1
lines changed

4 files changed

+328
-1
lines changed

packages/client-discord/src/constants.ts

-1
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
export const TEAM_COORDINATION = {
22
KEYWORDS: [
33
"team",
4-
"everyone",
54
"all agents",
65
"team update",
76
"gm team",

packages/client-discord/src/messages.ts

+246
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,8 @@ import type { VoiceManager } from "./voice.ts";
2727
import {
2828
discordShouldRespondTemplate,
2929
discordMessageHandlerTemplate,
30+
discordAutoPostTemplate,
31+
discordAnnouncementHypeTemplate
3032
} from "./templates.ts";
3133
import {
3234
IGNORE_RESPONSE_WORDS,
@@ -48,6 +50,16 @@ interface MessageContext {
4850
timestamp: number;
4951
}
5052

53+
interface AutoPostConfig {
54+
enabled: boolean;
55+
monitorTime: number;
56+
inactivityThreshold: number; // milliseconds
57+
mainChannelId: string;
58+
announcementChannelIds: string[];
59+
lastAutoPost?: number;
60+
minTimeBetweenPosts?: number; // minimum time between auto posts
61+
}
62+
5163
export type InterestChannels = {
5264
[key: string]: {
5365
currentHandler: string | undefined;
@@ -65,16 +77,36 @@ export class MessageManager {
6577
private interestChannels: InterestChannels = {};
6678
private discordClient: any;
6779
private voiceManager: VoiceManager;
80+
//Auto post
81+
private autoPostConfig: AutoPostConfig;
82+
private lastChannelActivity: { [channelId: string]: number } = {};
83+
private autoPostInterval: NodeJS.Timeout;
6884

6985
constructor(discordClient: any, voiceManager: VoiceManager) {
7086
this.client = discordClient.client;
7187
this.voiceManager = voiceManager;
7288
this.discordClient = discordClient;
7389
this.runtime = discordClient.runtime;
7490
this.attachmentManager = new AttachmentManager(this.runtime);
91+
92+
this.autoPostConfig = {
93+
enabled: this.runtime.character.clientConfig?.discord?.autoPost?.enabled || false,
94+
monitorTime: this.runtime.character.clientConfig?.discord?.autoPost?.monitorTime || 300000,
95+
inactivityThreshold: this.runtime.character.clientConfig?.discord?.autoPost?.inactivityThreshold || 3600000, // 1 hour default
96+
mainChannelId: this.runtime.character.clientConfig?.discord?.autoPost?.mainChannelId,
97+
announcementChannelIds: this.runtime.character.clientConfig?.discord?.autoPost?.announcementChannelIds || [],
98+
minTimeBetweenPosts: this.runtime.character.clientConfig?.discord?.autoPost?.minTimeBetweenPosts || 7200000, // 2 hours default
99+
};
100+
101+
if (this.autoPostConfig.enabled) {
102+
this._startAutoPostMonitoring();
103+
}
75104
}
76105

77106
async handleMessage(message: DiscordMessage) {
107+
// Update last activity time for the channel
108+
this.lastChannelActivity[message.channelId] = Date.now();
109+
78110
if (
79111
message.interaction ||
80112
message.author.id ===
@@ -512,6 +544,220 @@ export class MessageManager {
512544
}
513545
}
514546

547+
private _startAutoPostMonitoring(): void {
548+
// Wait for client to be ready
549+
if (!this.client.isReady()) {
550+
elizaLogger.info('[AutoPost Discord] Client not ready, waiting for ready event')
551+
this.client.once('ready', () => {
552+
elizaLogger.info('[AutoPost Discord] Client ready, starting monitoring')
553+
this._initializeAutoPost();
554+
});
555+
} else {
556+
elizaLogger.info('[AutoPost Discord] Client already ready, starting monitoring')
557+
this._initializeAutoPost();
558+
}
559+
}
560+
561+
private _initializeAutoPost(): void {
562+
// Give the client a moment to fully load its cache
563+
setTimeout(() => {
564+
// Monitor with random intervals between 2-6 hours
565+
this.autoPostInterval = setInterval(() => {
566+
this._checkChannelActivity();
567+
}, Math.floor(Math.random() * (4 * 60 * 60 * 1000) + 2 * 60 * 60 * 1000));
568+
569+
// Start monitoring announcement channels
570+
this._monitorAnnouncementChannels();
571+
}, 5000); // 5 second delay to ensure everything is loaded
572+
}
573+
574+
private async _checkChannelActivity(): Promise<void> {
575+
if (!this.autoPostConfig.enabled || !this.autoPostConfig.mainChannelId) return;
576+
577+
const channel = this.client.channels.cache.get(this.autoPostConfig.mainChannelId) as TextChannel;
578+
if (!channel) return;
579+
580+
try {
581+
// Get last message time
582+
const messages = await channel.messages.fetch({ limit: 1 });
583+
const lastMessage = messages.first();
584+
const lastMessageTime = lastMessage ? lastMessage.createdTimestamp : 0;
585+
586+
const now = Date.now();
587+
const timeSinceLastMessage = now - lastMessageTime;
588+
const timeSinceLastAutoPost = now - (this.autoPostConfig.lastAutoPost || 0);
589+
590+
// Add some randomness to the inactivity threshold (±30 minutes)
591+
const randomThreshold = this.autoPostConfig.inactivityThreshold +
592+
(Math.random() * 1800000 - 900000);
593+
594+
// Check if we should post
595+
if ((timeSinceLastMessage > randomThreshold) &&
596+
timeSinceLastAutoPost > (this.autoPostConfig.minTimeBetweenPosts || 0)) {
597+
598+
try {
599+
// Create memory and generate response
600+
const roomId = stringToUuid(channel.id + "-" + this.runtime.agentId);
601+
602+
const memory = {
603+
id: stringToUuid(`autopost-${Date.now()}`),
604+
userId: this.runtime.agentId,
605+
agentId: this.runtime.agentId,
606+
roomId,
607+
content: { text: "AUTO_POST_ENGAGEMENT", source: "discord" },
608+
embedding: getEmbeddingZeroVector(),
609+
createdAt: Date.now()
610+
};
611+
612+
let state = await this.runtime.composeState(memory, {
613+
discordClient: this.client,
614+
discordMessage: null,
615+
agentName: this.runtime.character.name || this.client.user?.displayName
616+
});
617+
618+
// Generate response using template
619+
const context = composeContext({
620+
state,
621+
template: this.runtime.character.templates?.discordAutoPostTemplate || discordAutoPostTemplate
622+
});
623+
624+
const responseContent = await this._generateResponse(memory, state, context);
625+
if (!responseContent?.text) return;
626+
627+
// Send message and update memory
628+
const messages = await sendMessageInChunks(channel, responseContent.text.trim(), null, []);
629+
630+
// Create and store memories
631+
const memories = messages.map(m => ({
632+
id: stringToUuid(m.id + "-" + this.runtime.agentId),
633+
userId: this.runtime.agentId,
634+
agentId: this.runtime.agentId,
635+
content: {
636+
...responseContent,
637+
url: m.url,
638+
},
639+
roomId,
640+
embedding: getEmbeddingZeroVector(),
641+
createdAt: m.createdTimestamp,
642+
}));
643+
644+
for (const m of memories) {
645+
await this.runtime.messageManager.createMemory(m);
646+
}
647+
648+
// Update state and last post time
649+
this.autoPostConfig.lastAutoPost = Date.now();
650+
state = await this.runtime.updateRecentMessageState(state);
651+
await this.runtime.evaluate(memory, state, true);
652+
} catch (error) {
653+
elizaLogger.warn("[AutoPost Discord] Error:", error);
654+
}
655+
} else {
656+
elizaLogger.warn("[AutoPost Discord] Activity within threshold. Not posting.");
657+
}
658+
} catch (error) {
659+
elizaLogger.warn("[AutoPost Discord] Error checking last message:", error);
660+
}
661+
}
662+
663+
private async _monitorAnnouncementChannels(): Promise<void> {
664+
if (!this.autoPostConfig.enabled || !this.autoPostConfig.announcementChannelIds.length) {
665+
elizaLogger.warn('[AutoPost Discord] Auto post config disabled or no announcement channels')
666+
return;
667+
}
668+
669+
for (const announcementChannelId of this.autoPostConfig.announcementChannelIds) {
670+
const channel = this.client.channels.cache.get(announcementChannelId);
671+
672+
if (channel) {
673+
// Check if it's either a text channel or announcement channel
674+
// ChannelType.GuildAnnouncement is 5
675+
// ChannelType.GuildText is 0
676+
if (channel instanceof TextChannel || channel.type === ChannelType.GuildAnnouncement) {
677+
const newsChannel = channel as TextChannel;
678+
try {
679+
newsChannel.createMessageCollector().on('collect', async (message: DiscordMessage) => {
680+
if (message.author.bot || Date.now() - message.createdTimestamp > 300000) return;
681+
682+
const mainChannel = this.client.channels.cache.get(this.autoPostConfig.mainChannelId) as TextChannel;
683+
if (!mainChannel) return;
684+
685+
try {
686+
// Create memory and generate response
687+
const roomId = stringToUuid(mainChannel.id + "-" + this.runtime.agentId);
688+
const memory = {
689+
id: stringToUuid(`announcement-${Date.now()}`),
690+
userId: this.runtime.agentId,
691+
agentId: this.runtime.agentId,
692+
roomId,
693+
content: {
694+
text: message.content,
695+
source: "discord",
696+
metadata: { announcementUrl: message.url }
697+
},
698+
embedding: getEmbeddingZeroVector(),
699+
createdAt: Date.now()
700+
};
701+
702+
let state = await this.runtime.composeState(memory, {
703+
discordClient: this.client,
704+
discordMessage: message,
705+
announcementContent: message?.content,
706+
announcementChannelId: channel.id,
707+
agentName: this.runtime.character.name || this.client.user?.displayName
708+
});
709+
710+
// Generate response using template
711+
const context = composeContext({
712+
state,
713+
template: this.runtime.character.templates?.discordAnnouncementHypeTemplate || discordAnnouncementHypeTemplate
714+
715+
});
716+
717+
const responseContent = await this._generateResponse(memory, state, context);
718+
if (!responseContent?.text) return;
719+
720+
// Send message and update memory
721+
const messages = await sendMessageInChunks(mainChannel, responseContent.text.trim(), null, []);
722+
723+
// Create and store memories
724+
const memories = messages.map(m => ({
725+
id: stringToUuid(m.id + "-" + this.runtime.agentId),
726+
userId: this.runtime.agentId,
727+
agentId: this.runtime.agentId,
728+
content: {
729+
...responseContent,
730+
url: m.url,
731+
},
732+
roomId,
733+
embedding: getEmbeddingZeroVector(),
734+
createdAt: m.createdTimestamp,
735+
}));
736+
737+
for (const m of memories) {
738+
await this.runtime.messageManager.createMemory(m);
739+
}
740+
741+
// Update state
742+
state = await this.runtime.updateRecentMessageState(state);
743+
await this.runtime.evaluate(memory, state, true);
744+
} catch (error) {
745+
elizaLogger.warn("[AutoPost Discord] Announcement Error:", error);
746+
}
747+
});
748+
elizaLogger.info(`[AutoPost Discord] Successfully set up collector for announcement channel: ${newsChannel.name}`);
749+
} catch (error) {
750+
elizaLogger.warn(`[AutoPost Discord] Error setting up announcement channel collector:`, error);
751+
}
752+
} else {
753+
elizaLogger.warn(`[AutoPost Discord] Channel ${announcementChannelId} is not a valid announcement or text channel, type:`, channel.type);
754+
}
755+
} else {
756+
elizaLogger.warn(`[AutoPost Discord] Could not find channel ${announcementChannelId} directly`);
757+
}
758+
}
759+
}
760+
515761
private _isMessageForMe(message: DiscordMessage): boolean {
516762
const isMentioned = message.mentions.users?.has(
517763
this.client.user?.id as string

packages/client-discord/src/templates.ts

+72
Original file line numberDiff line numberDiff line change
@@ -121,3 +121,75 @@ Note that {{agentName}} is capable of reading/seeing/hearing various forms of me
121121
122122
# Instructions: Write the next message for {{agentName}}. Include an action, if appropriate. {{actionNames}}
123123
` + messageCompletionFooter;
124+
125+
export const discordAutoPostTemplate =
126+
`# Action Examples
127+
NONE: Respond but perform no additional action. This is the default if the agent is speaking and not doing anything additional.
128+
129+
# Task: Generate an engaging community message as {{agentName}}.
130+
About {{agentName}}:
131+
{{bio}}
132+
{{lore}}
133+
134+
Examples of {{agentName}}'s dialog and actions:
135+
{{characterMessageExamples}}
136+
137+
{{messageDirections}}
138+
139+
# Recent Chat History:
140+
{{recentMessages}}
141+
142+
# Instructions: Write a natural, engaging message to restart community conversation. Focus on:
143+
- Community engagement
144+
- Educational topics
145+
- General discusions
146+
- Support queries
147+
- Keep message warm and inviting
148+
- Maximum 3 lines
149+
- Use 1-2 emojis maximum
150+
- Avoid financial advice
151+
- Stay within known facts
152+
- No team member mentions
153+
- Be hyped, not repetitive
154+
- Be natural, act like a human, connect with the community
155+
- Don't sound so robotic like
156+
- Randomly grab the most recent 5 messages for some context. Validate the context randomly and use that as a reference point for your next message, but not always, only when relevant.
157+
- If the recent messages are mostly from {{agentName}}, make sure to create conversation starters, given there is no messages from others to reference.
158+
- DO NOT REPEAT THE SAME thing that you just said from your recent chat history, start the message different each time, and be organic, non reptitive.
159+
160+
# Instructions: Write the next message for {{agentName}}. Include the "NONE" action only, as the only valid action for auto-posts is "NONE".
161+
` + messageCompletionFooter;
162+
163+
export const discordAnnouncementHypeTemplate =
164+
`# Action Examples
165+
NONE: Respond but perform no additional action. This is the default if the agent is speaking and not doing anything additional.
166+
167+
# Task: Generate announcement hype message as {{agentName}}.
168+
About {{agentName}}:
169+
{{bio}}
170+
{{lore}}
171+
172+
Examples of {{agentName}}'s dialog and actions:
173+
{{characterMessageExamples}}
174+
175+
{{messageDirections}}
176+
177+
# Announcement Content:
178+
{{announcementContent}}
179+
180+
# Instructions: Write an exciting message to bring attention to the announcement. Requirements:
181+
- Reference the announcement channel using <#{{announcementChannelId}}>
182+
- Reference the announcement content to get information about the announcement to use where appropriate to make the message dynamic vs a static post
183+
- Create genuine excitement
184+
- Encourage community participation
185+
- If there are links like Twitter/X posts, encourage users to like/retweet/comment to spread awarenress, but directly say that, wrap that into the post so its natural.
186+
- Stay within announced facts only
187+
- No additional promises or assumptions
188+
- No team member mentions
189+
- Start the message differently each time. Don't start with the same word like "hey", "hey hey", etc. be dynamic
190+
- Address everyone, not as a direct reply to whoever made the announcement or wrote it, but you can reference them
191+
- Maximum 3-7 lines formatted nicely if needed, based on the context of the announcement
192+
- Use 1-2 emojis maximum
193+
194+
# Instructions: Write the next message for {{agentName}}. Include the "NONE" action only, as no other actions are appropriate for announcement hype.
195+
` + messageCompletionFooter;

packages/core/src/types.ts

+10
Original file line numberDiff line numberDiff line change
@@ -751,6 +751,8 @@ export type Character = {
751751
telegramShouldRespondTemplate?: TemplateType;
752752
telegramAutoPostTemplate?: string;
753753
telegramPinnedMessageTemplate?: string;
754+
discordAutoPostTemplate?: string;
755+
discordAnnouncementHypeTemplate?: string;
754756
discordVoiceHandlerTemplate?: TemplateType;
755757
discordShouldRespondTemplate?: TemplateType;
756758
discordMessageHandlerTemplate?: TemplateType;
@@ -841,6 +843,14 @@ export type Character = {
841843
teamAgentIds?: string[];
842844
teamLeaderId?: string;
843845
teamMemberInterestKeywords?: string[];
846+
autoPost?: {
847+
enabled?: boolean;
848+
monitorTime?: number;
849+
inactivityThreshold?: number;
850+
mainChannelId?: string;
851+
announcementChannelIds?: string[];
852+
minTimeBetweenPosts?: number;
853+
};
844854
};
845855
telegram?: {
846856
shouldIgnoreBotMessages?: boolean;

0 commit comments

Comments
 (0)