Skip to content

Commit eb86bd5

Browse files
committed
fix(slack): Improve message handling and summarization - Fix duplicate processing, enhance summaries, add proper user names
1 parent 7b8c576 commit eb86bd5

File tree

3 files changed

+374
-271
lines changed

3 files changed

+374
-271
lines changed

packages/client-slack/src/actions/summarize_conversation.ts

+142-65
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,10 @@ import {
1212
Memory,
1313
ModelClass,
1414
State,
15+
ServiceType,
16+
elizaLogger
1517
} from "@ai16z/eliza";
18+
import { ISlackService, SLACK_SERVICE_TYPE } from "../types/slack-types";
1619

1720
export const summarizationTemplate = `# Summarized so far (we are adding to this)
1821
{{currentSummary}}
@@ -29,25 +32,34 @@ export const dateRangeTemplate = `# Messages we are summarizing (the conversatio
2932
{{recentMessages}}
3033
3134
# 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".
3536
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:
3744
\`\`\`json
3845
{
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"
4249
}
4350
\`\`\`
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.
4454
`;
4555

4656
const getDateRange = async (
4757
runtime: IAgentRuntime,
4858
message: Memory,
4959
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+
5163
const context = composeContext({
5264
state,
5365
template: dateRangeTemplate,
@@ -67,36 +79,46 @@ const getDateRange = async (
6779
} | null;
6880

6981
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+(second|minute|hour|day)s?\s+ago$/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;
78103
};
79104

80-
const startMultiplier = (parsedResponse.start as string).match(
81-
/second|minute|hour|day/
82-
)?.[0];
83-
const endMultiplier = (parsedResponse.end as string).match(
84-
/second|minute|hour|day/
85-
)?.[0];
105+
const startTime = parseTimeString(parsedResponse.start as string);
106+
const endTime = parseTimeString(parsedResponse.end as string);
86107

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+
}
89112

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+
};
97118
}
98119
}
99-
return null;
120+
121+
return undefined;
100122
};
101123

102124
const summarizeAction: Action = {
@@ -161,10 +183,10 @@ const summarizeAction: Action = {
161183
message.content.text.toLowerCase().includes(keyword.toLowerCase())
162184
);
163185
},
164-
handler: (async (
186+
handler: async (
165187
runtime: IAgentRuntime,
166188
message: Memory,
167-
state: State | undefined,
189+
state: State,
168190
_options: any,
169191
callback: HandlerCallback
170192
): Promise<Content> => {
@@ -177,12 +199,11 @@ const summarizeAction: Action = {
177199
attachments: [],
178200
};
179201

180-
const { roomId } = message;
181-
182202
// 1. Extract date range from the message
183203
const dateRange = await getDateRange(runtime, message, currentState);
184204
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'.";
186207
await callback(callbackData);
187208
return callbackData;
188209
}
@@ -191,30 +212,38 @@ const summarizeAction: Action = {
191212

192213
// 2. Get memories from the database
193214
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,
197218
count: 10000,
198219
unique: false,
199220
});
200221

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+
201228
const actors = await getActorDetails({
202229
runtime: runtime as IAgentRuntime,
203-
roomId,
230+
roomId: message.roomId,
204231
});
205232

206233
const actorMap = new Map(actors.map((actor) => [actor.id, actor]));
207234

208235
const formattedMemories = memories
209236
.map((memory) => {
237+
const actor = actorMap.get(memory.userId);
238+
const userName = actor?.name || actor?.username || "Unknown User";
210239
const attachments = memory.content.attachments
211240
?.map((attachment: Media) => {
212241
if (!attachment) return '';
213242
return `---\nAttachment: ${attachment.id}\n${attachment.description || ''}\n${attachment.text || ''}\n---`;
214243
})
215244
.filter(text => text !== '')
216245
.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 || ''}`;
218247
})
219248
.join("\n");
220249

@@ -228,6 +257,7 @@ const summarizeAction: Action = {
228257
currentState.memoriesWithAttachments = formattedMemories;
229258
currentState.objective = objective;
230259

260+
// Only process one chunk at a time and stop after getting a valid summary
231261
for (let i = 0; i < chunks.length; i++) {
232262
const chunk = chunks[i];
233263
currentState.currentSummary = currentSummary;
@@ -248,40 +278,87 @@ const summarizeAction: Action = {
248278
modelClass: ModelClass.SMALL,
249279
});
250280

251-
currentSummary = currentSummary + "\n" + summary;
281+
if (summary) {
282+
currentSummary = currentSummary + "\n" + summary;
283+
break; // Stop after getting first valid summary
284+
}
252285
}
253286

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.";
256289
await callback(callbackData);
257290
return callbackData;
258291
}
259292

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+
};
261299

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+
272352
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;
276354

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.";
280358
await callback(callbackData);
359+
return callbackData;
281360
}
282-
283-
return callbackData;
284-
}) as Handler,
361+
},
285362
examples: [
286363
[
287364
{

packages/client-slack/src/index.ts

+8-4
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,9 @@ import { MessageManager } from './messages';
88
import { validateSlackConfig } from './environment';
99
import chat_with_attachments from './actions/chat_with_attachments';
1010
import summarize_conversation from './actions/summarize_conversation';
11-
import transcribe_media from './actions/transcribe_media';
11+
// import transcribe_media from './actions/transcribe_media';
1212
import { channelStateProvider } from './providers/channelState';
13+
import { SlackService } from './services/slack.service';
1314

1415
interface SlackRequest extends Request {
1516
rawBody?: Buffer;
@@ -65,8 +66,6 @@ export class SlackClient extends EventEmitter {
6566
if (event.type === 'message' || event.type === 'app_mention') {
6667
await this.messageManager.handleMessage(event);
6768
}
68-
69-
this.emit(event.type, event);
7069
} catch (error) {
7170
elizaLogger.error("❌ [EVENT] Error handling event:", error);
7271
}
@@ -125,6 +124,11 @@ export class SlackClient extends EventEmitter {
125124

126125
const config = await validateSlackConfig(this.runtime);
127126

127+
// Initialize and register Slack service
128+
const slackService = new SlackService();
129+
await slackService.initialize(this.runtime);
130+
await this.runtime.registerService(slackService);
131+
128132
// Get detailed bot info
129133
const auth = await this.client.auth.test();
130134
if (!auth.ok) throw new Error("Failed to authenticate with Slack");
@@ -165,7 +169,7 @@ export class SlackClient extends EventEmitter {
165169
// Register actions and providers
166170
this.runtime.registerAction(chat_with_attachments);
167171
this.runtime.registerAction(summarize_conversation);
168-
this.runtime.registerAction(transcribe_media);
172+
// this.runtime.registerAction(transcribe_media);
169173
this.runtime.providers.push(channelStateProvider);
170174

171175
// Add request logging middleware

0 commit comments

Comments
 (0)