1
- import { WebClient } from '@slack/web-api' ;
2
- import { elizaLogger } from '@ai16z/eliza' ;
3
- import { IAgentRuntime , Memory , Content , State } from '@ai16z/eliza' ;
4
1
import {
5
2
stringToUuid ,
6
3
getEmbeddingZeroVector ,
7
4
composeContext ,
8
- generateMessageResponse ,
5
+ generateMessageResponse ,
9
6
generateShouldRespond ,
10
- ModelClass
7
+ ModelClass ,
8
+ Memory ,
9
+ Content ,
10
+ State ,
11
+ elizaLogger ,
12
+ HandlerCallback
11
13
} from '@ai16z/eliza' ;
12
14
import { slackMessageHandlerTemplate , slackShouldRespondTemplate } from './templates' ;
15
+ import { WebClient } from '@slack/web-api' ;
16
+ import { IAgentRuntime } from '@ai16z/eliza' ;
13
17
14
18
export class MessageManager {
15
19
private client : WebClient ;
16
20
private runtime : IAgentRuntime ;
17
21
private botUserId : string ;
22
+ private processedMessages : Map < string , number > = new Map ( ) ;
18
23
19
24
constructor ( client : WebClient , runtime : IAgentRuntime , botUserId : string ) {
20
25
elizaLogger . log ( "📱 Initializing MessageManager..." ) ;
21
26
this . client = client ;
22
27
this . runtime = runtime ;
23
28
this . botUserId = botUserId ;
24
29
elizaLogger . debug ( "MessageManager initialized with botUserId:" , botUserId ) ;
30
+
31
+ // Clear old processed messages every hour
32
+ setInterval ( ( ) => {
33
+ const oneHourAgo = Date . now ( ) - 3600000 ;
34
+ for ( const [ key , timestamp ] of this . processedMessages . entries ( ) ) {
35
+ if ( timestamp < oneHourAgo ) {
36
+ this . processedMessages . delete ( key ) ;
37
+ }
38
+ }
39
+ } , 3600000 ) ;
25
40
}
26
41
27
- public async handleMessage ( event : any ) {
28
- elizaLogger . debug ( "📥 [DETAILED] Incoming message event:" , JSON . stringify ( event , null , 2 ) ) ;
42
+ private cleanMessage ( text : string ) : string {
43
+ elizaLogger . debug ( "🧹 [CLEAN] Cleaning message text:" , text ) ;
44
+ // Remove bot mention
45
+ const cleaned = text . replace ( new RegExp ( `<@${ this . botUserId } >` , 'g' ) , '' ) . trim ( ) ;
46
+ elizaLogger . debug ( "✨ [CLEAN] Cleaned result:" , cleaned ) ;
47
+ return cleaned ;
48
+ }
29
49
30
- // Ignore messages from bots (including ourselves)
31
- if ( event . bot_id || event . user === this . botUserId ) {
32
- elizaLogger . debug ( "⏭️ [SKIP] Message from bot or self:" , {
33
- bot_id : event . bot_id ,
34
- user : event . user ,
35
- bot_user_id : this . botUserId
36
- } ) ;
37
- return ;
50
+ private async _shouldRespond ( message : any , state : State ) : Promise < boolean > {
51
+ // Always respond to direct mentions
52
+ if ( message . type === 'app_mention' || message . text ?. includes ( `<@${ this . botUserId } >` ) ) {
53
+ return true ;
54
+ }
55
+
56
+ // Always respond in direct messages
57
+ if ( message . channel_type === 'im' ) {
58
+ return true ;
59
+ }
60
+
61
+ // Use the shouldRespond template to decide
62
+ const shouldRespondContext = composeContext ( {
63
+ state,
64
+ template : this . runtime . character . templates ?. slackShouldRespondTemplate ||
65
+ this . runtime . character . templates ?. shouldRespondTemplate ||
66
+ slackShouldRespondTemplate ,
67
+ } ) ;
68
+
69
+ const response = await generateShouldRespond ( {
70
+ runtime : this . runtime ,
71
+ context : shouldRespondContext ,
72
+ modelClass : ModelClass . SMALL ,
73
+ } ) ;
74
+
75
+ return response === 'RESPOND' ;
76
+ }
77
+
78
+ private async _generateResponse (
79
+ memory : Memory ,
80
+ state : State ,
81
+ context : string
82
+ ) : Promise < Content > {
83
+ const { userId, roomId } = memory ;
84
+
85
+ const response = await generateMessageResponse ( {
86
+ runtime : this . runtime ,
87
+ context,
88
+ modelClass : ModelClass . SMALL ,
89
+ } ) ;
90
+
91
+ if ( ! response ) {
92
+ elizaLogger . error ( "No response from generateMessageResponse" ) ;
93
+ return {
94
+ text : "I apologize, but I'm having trouble generating a response right now." ,
95
+ source : 'slack'
96
+ } ;
97
+ }
98
+
99
+ await this . runtime . databaseAdapter . log ( {
100
+ body : { memory, context, response } ,
101
+ userId : userId ,
102
+ roomId,
103
+ type : "response" ,
104
+ } ) ;
105
+
106
+ // If response includes a CONTINUE action but there's no direct mention or thread,
107
+ // remove the action to prevent automatic continuation
108
+ if (
109
+ response . action === 'CONTINUE' &&
110
+ ! memory . content . text ?. includes ( `<@${ this . botUserId } >` ) &&
111
+ ! state . recentMessages ?. includes ( memory . id )
112
+ ) {
113
+ elizaLogger . debug ( "🛑 [CONTINUE] Removing CONTINUE action as message is not a direct interaction" ) ;
114
+ delete response . action ;
38
115
}
39
116
117
+ return response ;
118
+ }
119
+
120
+ public async handleMessage ( event : any ) {
121
+ elizaLogger . debug ( "📥 [DETAILED] Incoming message event:" , JSON . stringify ( event , null , 2 ) ) ;
122
+
40
123
try {
41
- // Check if this is a direct mention or a message in a channel where the bot is mentioned
42
- const isMention = event . type === 'app_mention' ||
43
- ( event . text && event . text . includes ( `<@${ this . botUserId } >` ) ) ;
44
-
45
- // Skip if it's not a mention and not in a direct message
46
- if ( ! isMention && event . channel_type !== 'im' ) {
47
- elizaLogger . debug ( "⏭️ [SKIP] Not a mention or direct message" ) ;
124
+ // Generate a unique key for this message that includes all relevant data
125
+ const messageKey = `${ event . channel } -${ event . ts } -${ event . type } -${ event . text } ` ;
126
+ const currentTime = Date . now ( ) ;
127
+
128
+ // Check if we've already processed this message
129
+ if ( this . processedMessages . has ( messageKey ) ) {
130
+ elizaLogger . debug ( "⏭️ [SKIP] Message already processed:" , {
131
+ key : messageKey ,
132
+ originalTimestamp : this . processedMessages . get ( messageKey ) ,
133
+ currentTime
134
+ } ) ;
48
135
return ;
49
136
}
50
137
51
- elizaLogger . debug ( "🎯 [CONTEXT] Message details:" , {
52
- is_mention : isMention ,
53
- channel_type : event . channel_type ,
54
- thread_ts : event . thread_ts ,
55
- text : event . text ,
56
- channel : event . channel ,
57
- subtype : event . subtype ,
58
- event_type : event . type
138
+ // Add to processed messages map with current timestamp
139
+ this . processedMessages . set ( messageKey , currentTime ) ;
140
+ elizaLogger . debug ( "✨ [NEW] Processing new message:" , {
141
+ key : messageKey ,
142
+ timestamp : currentTime
59
143
} ) ;
60
144
61
- // Clean the message text by removing the bot mention
145
+ // Ignore messages from bots (including ourselves)
146
+ if ( event . bot_id || event . user === this . botUserId ) {
147
+ elizaLogger . debug ( "⏭️ [SKIP] Message from bot or self:" , {
148
+ bot_id : event . bot_id ,
149
+ user : event . user ,
150
+ bot_user_id : this . botUserId
151
+ } ) ;
152
+ return ;
153
+ }
154
+
155
+ // Clean the message text
62
156
const cleanedText = this . cleanMessage ( event . text || '' ) ;
63
- elizaLogger . debug ( "🧹 [CLEAN] Cleaned message text:" , cleanedText ) ;
64
157
65
158
// Generate unique IDs for the conversation
66
159
const roomId = stringToUuid ( `${ event . channel } -${ this . runtime . agentId } ` ) ;
@@ -82,8 +175,6 @@ export class MessageManager {
82
175
'slack'
83
176
) ;
84
177
85
- elizaLogger . debug ( "🔌 [CONNECTION] Connection ensured for user" ) ;
86
-
87
178
// Create memory for the message
88
179
const content : Content = {
89
180
text : cleanedText ,
@@ -110,8 +201,8 @@ export class MessageManager {
110
201
await this . runtime . messageManager . createMemory ( memory ) ;
111
202
}
112
203
113
- // Compose state for response generation
114
- const state = await this . runtime . composeState (
204
+ // Initial state composition
205
+ let state = await this . runtime . composeState (
115
206
{ content, userId, agentId : this . runtime . agentId , roomId } ,
116
207
{
117
208
slackClient : this . client ,
@@ -121,36 +212,27 @@ export class MessageManager {
121
212
}
122
213
) ;
123
214
215
+ // Update state with recent messages
216
+ state = await this . runtime . updateRecentMessageState ( state ) ;
217
+
124
218
elizaLogger . debug ( "🔄 [STATE] Composed state:" , {
125
219
agentName : state . agentName ,
126
220
senderName : state . senderName ,
127
221
roomId : state . roomId ,
128
222
recentMessages : state . recentMessages ?. length || 0
129
223
} ) ;
130
224
131
- // Always respond to direct mentions and direct messages
132
- const shouldRespond = isMention || event . channel_type === 'im' ? 'RESPOND' : 'IGNORE' ;
133
-
134
- elizaLogger . debug ( "✅ [DECISION] Should respond:" , {
135
- decision : shouldRespond ,
136
- isMention,
137
- channelType : event . channel_type
138
- } ) ;
225
+ // Check if we should respond
226
+ const shouldRespond = await this . _shouldRespond ( event , state ) ;
139
227
140
- if ( shouldRespond === 'RESPOND' ) {
141
- elizaLogger . debug ( "💭 [GENERATE] Generating response..." ) ;
142
-
228
+ if ( shouldRespond ) {
143
229
// Generate response using message handler template
144
230
const context = composeContext ( {
145
231
state,
146
232
template : this . runtime . character . templates ?. slackMessageHandlerTemplate || slackMessageHandlerTemplate ,
147
233
} ) ;
148
234
149
- const responseContent = await generateMessageResponse ( {
150
- runtime : this . runtime ,
151
- context,
152
- modelClass : ModelClass . SMALL ,
153
- } ) ;
235
+ const responseContent = await this . _generateResponse ( memory , state , context ) ;
154
236
155
237
elizaLogger . debug ( "📝 [RESPONSE] Generated response content:" , {
156
238
hasText : ! ! responseContent ?. text ,
@@ -182,52 +264,63 @@ export class MessageManager {
182
264
text_length : responseContent . text . length
183
265
} ) ;
184
266
185
- await this . sendMessage ( event . channel , responseContent . text , event . thread_ts ) ;
186
- elizaLogger . debug ( "✉️ [SUCCESS] Response sent successfully" ) ;
267
+ const callback : HandlerCallback = async ( content : Content ) => {
268
+ try {
269
+ const response = await this . client . chat . postMessage ( {
270
+ channel : event . channel ,
271
+ text : content . text ,
272
+ thread_ts : event . thread_ts
273
+ } ) ;
274
+
275
+ const responseMemory : Memory = {
276
+ id : stringToUuid ( `${ response . ts } -${ this . runtime . agentId } ` ) ,
277
+ userId : this . runtime . agentId ,
278
+ agentId : this . runtime . agentId ,
279
+ roomId,
280
+ content : {
281
+ ...content ,
282
+ inReplyTo : messageId ,
283
+ } ,
284
+ createdAt : Date . now ( ) ,
285
+ embedding : getEmbeddingZeroVector ( ) ,
286
+ } ;
287
+
288
+ await this . runtime . messageManager . createMemory ( responseMemory ) ;
289
+ return [ responseMemory ] ;
290
+ } catch ( error ) {
291
+ elizaLogger . error ( "Error sending message:" , error ) ;
292
+ return [ ] ;
293
+ }
294
+ } ;
295
+
296
+ const responseMessages = await callback ( responseContent ) ;
297
+
298
+ // Update state with new messages
299
+ state = await this . runtime . updateRecentMessageState ( state ) ;
300
+
301
+ // Process any actions
302
+ await this . runtime . processActions (
303
+ memory ,
304
+ responseMessages ,
305
+ state ,
306
+ callback
307
+ ) ;
187
308
}
188
- } else {
189
- elizaLogger . debug ( "⏭️ [SKIP] Skipping response based on shouldRespond:" , shouldRespond ) ;
190
309
}
191
- } catch ( error ) {
192
- elizaLogger . error ( "❌ [ERROR] Error handling message:" , error ) ;
193
- await this . sendMessage (
194
- event . channel ,
195
- "Sorry, I encountered an error processing your message." ,
196
- event . thread_ts
197
- ) ;
198
- }
199
- }
200
-
201
- private cleanMessage ( text : string ) : string {
202
- elizaLogger . debug ( "🧼 [CLEAN] Cleaning message:" , text ) ;
203
- // Remove mention of the bot
204
- const cleaned = text . replace ( new RegExp ( `<@${ this . botUserId } >` , 'g' ) , '' ) . trim ( ) ;
205
- elizaLogger . debug ( "✨ [CLEAN] Cleaned result:" , cleaned ) ;
206
- return cleaned ;
207
- }
208
310
209
- private async sendMessage ( channel : string , text : string , thread_ts ?: string ) {
210
- elizaLogger . debug ( "📤 [SEND] Sending message:" , {
211
- channel,
212
- text_length : text . length ,
213
- thread_ts : thread_ts || 'none'
214
- } ) ;
311
+ // Evaluate the interaction
312
+ await this . runtime . evaluate ( memory , state , shouldRespond ) ;
215
313
216
- try {
217
- const response = await this . client . chat . postMessage ( {
218
- channel,
219
- text,
220
- thread_ts,
314
+ } catch ( error ) {
315
+ elizaLogger . error ( "❌ [ERROR] Error handling message:" , {
316
+ error : error instanceof Error ? error . message : String ( error ) ,
317
+ stack : error instanceof Error ? error . stack : undefined
221
318
} ) ;
222
- elizaLogger . debug ( "📨 [SEND] Message sent successfully:" , {
223
- ts : response . ts ,
224
- channel : response . channel ,
225
- ok : response . ok
319
+ await this . client . chat . postMessage ( {
320
+ channel : event . channel ,
321
+ text : "Sorry, I encountered an error processing your message." ,
322
+ thread_ts : event . thread_ts
226
323
} ) ;
227
- return response ;
228
- } catch ( error ) {
229
- elizaLogger . error ( "❌ [ERROR] Failed to send message:" , error ) ;
230
- throw error ;
231
324
}
232
325
}
233
326
}
0 commit comments