@@ -12,7 +12,10 @@ import {
12
12
Memory ,
13
13
ModelClass ,
14
14
State ,
15
+ ServiceType ,
16
+ elizaLogger
15
17
} from "@ai16z/eliza" ;
18
+ import { ISlackService , SLACK_SERVICE_TYPE } from "../types/slack-types" ;
16
19
17
20
export const summarizationTemplate = `# Summarized so far (we are adding to this)
18
21
{{currentSummary}}
@@ -29,25 +32,34 @@ export const dateRangeTemplate = `# Messages we are summarizing (the conversatio
29
32
{{recentMessages}}
30
33
31
34
# Instructions: {{senderName}} is requesting a summary of the conversation. Your goal is to determine their objective, along with the range of dates that their request covers.
32
- The "objective" is a detailed description of what the user wants to summarize based on the conversation. If they just ask for a general summary, you can either base it off the converation if the summary range is very recent, or set the object to be general, like "a detailed summary of the conversation between all users".
33
- The "start" and "end" are the range of dates that the user wants to summarize, relative to the current time. The start and end should be relative to the current time, and measured in seconds, minutes, hours and days. The format is "2 days ago" or "3 hours ago" or "4 minutes ago" or "5 seconds ago", i.e. "<integer> <unit> ago".
34
- If you aren't sure, you can use a default range of "0 minutes ago" to "2 hours ago" or more. Better to err on the side of including too much than too little.
35
+ The "objective" is a detailed description of what the user wants to summarize based on the conversation. If they just ask for a general summary, you can either base it off the conversation if the summary range is very recent, or set the object to be general, like "a detailed summary of the conversation between all users".
35
36
36
- Your response must be formatted as a JSON block with this structure:
37
+ The "start" and "end" are the range of dates that the user wants to summarize, relative to the current time. The format MUST be a number followed by a unit, like:
38
+ - "5 minutes ago"
39
+ - "2 hours ago"
40
+ - "1 day ago"
41
+ - "30 seconds ago"
42
+
43
+ For example:
37
44
\`\`\`json
38
45
{
39
- "objective": "<What the user wants to summarize> ",
40
- "start": "0 minutes ago",
41
- "end": "2 hours ago"
46
+ "objective": "a detailed summary of the conversation between all users ",
47
+ "start": "2 hours ago",
48
+ "end": "0 minutes ago"
42
49
}
43
50
\`\`\`
51
+
52
+ If the user asks for "today", use "24 hours ago" as start and "0 minutes ago" as end.
53
+ If no time range is specified, default to "2 hours ago" for start and "0 minutes ago" for end.
44
54
` ;
45
55
46
56
const getDateRange = async (
47
57
runtime : IAgentRuntime ,
48
58
message : Memory ,
49
59
state : State
50
- ) : Promise < { objective : string ; start : string | number ; end : string | number } | null > => {
60
+ ) : Promise < { objective : string ; start : number ; end : number } | undefined > => {
61
+ state = ( await runtime . composeState ( message ) ) as State ;
62
+
51
63
const context = composeContext ( {
52
64
state,
53
65
template : dateRangeTemplate ,
@@ -67,36 +79,46 @@ const getDateRange = async (
67
79
} | null ;
68
80
69
81
if ( parsedResponse ?. objective && parsedResponse ?. start && parsedResponse ?. end ) {
70
- const startIntegerString = ( parsedResponse . start as string ) . match ( / \d + / ) ?. [ 0 ] ;
71
- const endIntegerString = ( parsedResponse . end as string ) . match ( / \d + / ) ?. [ 0 ] ;
72
-
73
- const multipliers = {
74
- second : 1 * 1000 ,
75
- minute : 60 * 1000 ,
76
- hour : 3600 * 1000 ,
77
- day : 86400 * 1000 ,
82
+ // Parse time strings like "5 minutes ago", "2 hours ago", etc.
83
+ const parseTimeString = ( timeStr : string ) : number | null => {
84
+ const match = timeStr . match ( / ^ ( \d + ) \s + ( s e c o n d | m i n u t e | h o u r | d a y ) s ? \s + a g o $ / i) ;
85
+ if ( ! match ) return null ;
86
+
87
+ const [ _ , amount , unit ] = match ;
88
+ const value = parseInt ( amount ) ;
89
+
90
+ if ( isNaN ( value ) ) return null ;
91
+
92
+ const multipliers : { [ key : string ] : number } = {
93
+ second : 1000 ,
94
+ minute : 60 * 1000 ,
95
+ hour : 60 * 60 * 1000 ,
96
+ day : 24 * 60 * 60 * 1000
97
+ } ;
98
+
99
+ const multiplier = multipliers [ unit . toLowerCase ( ) ] ;
100
+ if ( ! multiplier ) return null ;
101
+
102
+ return value * multiplier ;
78
103
} ;
79
104
80
- const startMultiplier = ( parsedResponse . start as string ) . match (
81
- / s e c o n d | m i n u t e | h o u r | d a y /
82
- ) ?. [ 0 ] ;
83
- const endMultiplier = ( parsedResponse . end as string ) . match (
84
- / s e c o n d | m i n u t e | h o u r | d a y /
85
- ) ?. [ 0 ] ;
105
+ const startTime = parseTimeString ( parsedResponse . start as string ) ;
106
+ const endTime = parseTimeString ( parsedResponse . end as string ) ;
86
107
87
- const startInteger = startIntegerString ? parseInt ( startIntegerString ) : 0 ;
88
- const endInteger = endIntegerString ? parseInt ( endIntegerString ) : 0 ;
108
+ if ( startTime === null || endTime === null ) {
109
+ elizaLogger . error ( "Invalid time format in response" , parsedResponse ) ;
110
+ continue ;
111
+ }
89
112
90
- const startTime = startInteger * multipliers [ startMultiplier as keyof typeof multipliers ] ;
91
- const endTime = endInteger * multipliers [ endMultiplier as keyof typeof multipliers ] ;
92
-
93
- parsedResponse . start = Date . now ( ) - startTime ;
94
- parsedResponse . end = Date . now ( ) - endTime ;
95
-
96
- return parsedResponse ;
113
+ return {
114
+ objective : parsedResponse . objective ,
115
+ start : Date . now ( ) - startTime ,
116
+ end : Date . now ( ) - endTime
117
+ } ;
97
118
}
98
119
}
99
- return null ;
120
+
121
+ return undefined ;
100
122
} ;
101
123
102
124
const summarizeAction : Action = {
@@ -161,10 +183,10 @@ const summarizeAction: Action = {
161
183
message . content . text . toLowerCase ( ) . includes ( keyword . toLowerCase ( ) )
162
184
) ;
163
185
} ,
164
- handler : ( async (
186
+ handler : async (
165
187
runtime : IAgentRuntime ,
166
188
message : Memory ,
167
- state : State | undefined ,
189
+ state : State ,
168
190
_options : any ,
169
191
callback : HandlerCallback
170
192
) : Promise < Content > => {
@@ -177,12 +199,11 @@ const summarizeAction: Action = {
177
199
attachments : [ ] ,
178
200
} ;
179
201
180
- const { roomId } = message ;
181
-
182
202
// 1. Extract date range from the message
183
203
const dateRange = await getDateRange ( runtime , message , currentState ) ;
184
204
if ( ! dateRange ) {
185
- console . error ( "Couldn't get date range from message" ) ;
205
+ elizaLogger . error ( "Couldn't determine date range from message" ) ;
206
+ callbackData . text = "I couldn't determine the time range to summarize. Please try asking for a specific period like 'last hour' or 'today'." ;
186
207
await callback ( callbackData ) ;
187
208
return callbackData ;
188
209
}
@@ -191,30 +212,38 @@ const summarizeAction: Action = {
191
212
192
213
// 2. Get memories from the database
193
214
const memories = await runtime . messageManager . getMemories ( {
194
- roomId,
195
- start : parseInt ( start as string ) ,
196
- end : parseInt ( end as string ) ,
215
+ roomId : message . roomId ,
216
+ start,
217
+ end,
197
218
count : 10000 ,
198
219
unique : false ,
199
220
} ) ;
200
221
222
+ if ( ! memories || memories . length === 0 ) {
223
+ callbackData . text = "I couldn't find any messages in that time range to summarize." ;
224
+ await callback ( callbackData ) ;
225
+ return callbackData ;
226
+ }
227
+
201
228
const actors = await getActorDetails ( {
202
229
runtime : runtime as IAgentRuntime ,
203
- roomId,
230
+ roomId : message . roomId ,
204
231
} ) ;
205
232
206
233
const actorMap = new Map ( actors . map ( ( actor ) => [ actor . id , actor ] ) ) ;
207
234
208
235
const formattedMemories = memories
209
236
. map ( ( memory ) => {
237
+ const actor = actorMap . get ( memory . userId ) ;
238
+ const userName = actor ?. name || actor ?. username || "Unknown User" ;
210
239
const attachments = memory . content . attachments
211
240
?. map ( ( attachment : Media ) => {
212
241
if ( ! attachment ) return '' ;
213
242
return `---\nAttachment: ${ attachment . id } \n${ attachment . description || '' } \n${ attachment . text || '' } \n---` ;
214
243
} )
215
244
. filter ( text => text !== '' )
216
245
. join ( "\n" ) ;
217
- return `${ actorMap . get ( memory . userId ) ?. name ?? "Unknown User" } ( ${ actorMap . get ( memory . userId ) ?. username ?? "" } ) : ${ memory . content . text } \n${ attachments || '' } ` ;
246
+ return `${ userName } : ${ memory . content . text } \n${ attachments || '' } ` ;
218
247
} )
219
248
. join ( "\n" ) ;
220
249
@@ -228,6 +257,7 @@ const summarizeAction: Action = {
228
257
currentState . memoriesWithAttachments = formattedMemories ;
229
258
currentState . objective = objective ;
230
259
260
+ // Only process one chunk at a time and stop after getting a valid summary
231
261
for ( let i = 0 ; i < chunks . length ; i ++ ) {
232
262
const chunk = chunks [ i ] ;
233
263
currentState . currentSummary = currentSummary ;
@@ -248,40 +278,87 @@ const summarizeAction: Action = {
248
278
modelClass : ModelClass . SMALL ,
249
279
} ) ;
250
280
251
- currentSummary = currentSummary + "\n" + summary ;
281
+ if ( summary ) {
282
+ currentSummary = currentSummary + "\n" + summary ;
283
+ break ; // Stop after getting first valid summary
284
+ }
252
285
}
253
286
254
- if ( ! currentSummary ) {
255
- console . error ( "No summary found!" ) ;
287
+ if ( ! currentSummary . trim ( ) ) {
288
+ callbackData . text = "I wasn't able to generate a summary of the conversation." ;
256
289
await callback ( callbackData ) ;
257
290
return callbackData ;
258
291
}
259
292
260
- callbackData . text = currentSummary . trim ( ) ;
293
+ // Format dates consistently
294
+ const formatDate = ( timestamp : number ) => {
295
+ const date = new Date ( timestamp ) ;
296
+ const pad = ( n : number ) => n < 10 ? `0${ n } ` : n ;
297
+ return `${ date . getFullYear ( ) } -${ pad ( date . getMonth ( ) + 1 ) } -${ pad ( date . getDate ( ) ) } ${ pad ( date . getHours ( ) ) } :${ pad ( date . getMinutes ( ) ) } ` ;
298
+ } ;
261
299
262
- if (
263
- callbackData . text &&
264
- ( currentSummary . trim ( ) ?. split ( "\n" ) . length < 4 ||
265
- currentSummary . trim ( ) ?. split ( " " ) . length < 100 )
266
- ) {
267
- callbackData . text = `Here is the summary:
268
- \`\`\`md
269
- ${ currentSummary . trim ( ) }
270
- \`\`\`
271
- ` ;
300
+ try {
301
+ // Get the user's name for the summary header
302
+ const requestingUser = actorMap . get ( message . userId ) ;
303
+ const userName = requestingUser ?. name || requestingUser ?. username || "Unknown User" ;
304
+
305
+ const summaryContent = `Summary of conversation from ${ formatDate ( start ) } to ${ formatDate ( end ) }
306
+
307
+ Here is a detailed summary of the conversation between ${ userName } and ${ runtime . character . name } :\n\n${ currentSummary . trim ( ) } ` ;
308
+
309
+ // If summary is long, upload as a file
310
+ if ( summaryContent . length > 1000 ) {
311
+ const summaryFilename = `summary_${ Date . now ( ) } .txt` ;
312
+ elizaLogger . debug ( "Uploading summary file to Slack..." ) ;
313
+
314
+ try {
315
+ // Save file content
316
+ await runtime . cacheManager . set ( summaryFilename , summaryContent ) ;
317
+
318
+ // Get the Slack service from runtime
319
+ const slackService = runtime . getService ( SLACK_SERVICE_TYPE ) as ISlackService ;
320
+ if ( ! slackService ?. client ) {
321
+ elizaLogger . error ( "Slack service not found or not properly initialized" ) ;
322
+ throw new Error ( 'Slack service not found' ) ;
323
+ }
324
+
325
+ // Upload file using Slack's API
326
+ elizaLogger . debug ( `Uploading file ${ summaryFilename } to channel ${ message . roomId } ` ) ;
327
+ const uploadResult = await slackService . client . files . upload ( {
328
+ channels : message . roomId ,
329
+ filename : summaryFilename ,
330
+ title : 'Conversation Summary' ,
331
+ content : summaryContent ,
332
+ initial_comment : `I've created a summary of the conversation from ${ formatDate ( start ) } to ${ formatDate ( end ) } .`
333
+ } ) ;
334
+
335
+ if ( uploadResult . ok ) {
336
+ elizaLogger . success ( "Successfully uploaded summary file to Slack" ) ;
337
+ callbackData . text = `I've created a summary of the conversation from ${ formatDate ( start ) } to ${ formatDate ( end ) } . You can find it in the thread above.` ;
338
+ } else {
339
+ elizaLogger . error ( "Failed to upload file to Slack:" , uploadResult . error ) ;
340
+ throw new Error ( 'Failed to upload file to Slack' ) ;
341
+ }
342
+ } catch ( error ) {
343
+ elizaLogger . error ( 'Error uploading summary file:' , error ) ;
344
+ // Fallback to sending as a message
345
+ callbackData . text = summaryContent ;
346
+ }
347
+ } else {
348
+ // For shorter summaries, just send as a message
349
+ callbackData . text = summaryContent ;
350
+ }
351
+
272
352
await callback ( callbackData ) ;
273
- } else if ( currentSummary . trim ( ) ) {
274
- const summaryFilename = `content/conversation_summary_${ Date . now ( ) } ` ;
275
- await runtime . cacheManager . set ( summaryFilename , currentSummary ) ;
353
+ return callbackData ;
276
354
277
- callbackData . text = `I've attached the summary of the conversation from \` ${ new Date ( parseInt ( start as string ) ) . toString ( ) } \` to \` ${ new Date ( parseInt ( end as string ) ) . toString ( ) } \` as a text file.` ;
278
- await callback ( callbackData , [ summaryFilename ] ) ;
279
- } else {
355
+ } catch ( error ) {
356
+ elizaLogger . error ( "Error in summary generation:" , error ) ;
357
+ callbackData . text = "I encountered an error while generating the summary. Please try again." ;
280
358
await callback ( callbackData ) ;
359
+ return callbackData ;
281
360
}
282
-
283
- return callbackData ;
284
- } ) as Handler ,
361
+ } ,
285
362
examples : [
286
363
[
287
364
{
0 commit comments