diff --git a/packages/client-slack/src/actions/chat_with_attachments.ts b/packages/client-slack/src/actions/chat_with_attachments.ts index 27dc1528e94..1d2e2c3f565 100644 --- a/packages/client-slack/src/actions/chat_with_attachments.ts +++ b/packages/client-slack/src/actions/chat_with_attachments.ts @@ -1,4 +1,9 @@ -import { composeContext, generateText, trimTokens, parseJSONObjectFromText } from "@ai16z/eliza"; +import { + composeContext, + generateText, + trimTokens, + parseJSONObjectFromText, +} from "@ai16z/eliza"; import { models } from "@ai16z/eliza"; import { Action, @@ -22,7 +27,7 @@ Summarization objective: {{objective}} # Instructions: Summarize the attachments. Return the summary. Do not acknowledge this request, just summarize and continue the existing summary if there is one. Capture any important details based on the objective. Only respond with the new summary text.`; -export const attachmentIdsTemplate = `# Messages we are summarizing +export const attachmentIdsTemplate = `# Messages we are summarizing {{recentMessages}} # Instructions: {{senderName}} is requesting a summary of specific attachments. Your goal is to determine their objective, along with the list of attachment IDs to summarize. @@ -54,12 +59,12 @@ const getAttachmentIds = async ( context, modelClass: ModelClass.SMALL, }); - + const parsedResponse = parseJSONObjectFromText(response) as { objective: string; attachmentIds: string[]; } | null; - + if (parsedResponse?.objective && parsedResponse?.attachmentIds) { return parsedResponse; } @@ -91,7 +96,7 @@ const summarizeAction: Action = { validate: async ( runtime: IAgentRuntime, message: Memory, - state: State | undefined + _state: State | undefined ): Promise => { if (message.content.source !== "slack") { return false; @@ -122,7 +127,7 @@ const summarizeAction: Action = { "listen", "watch", ]; - + return keywords.some((keyword) => message.content.text.toLowerCase().includes(keyword.toLowerCase()) ); @@ -134,7 +139,8 @@ const summarizeAction: Action = { options: any, callback: HandlerCallback ): Promise => { - const currentState = state ?? await runtime.composeState(message) as State; + const currentState = + state ?? ((await runtime.composeState(message)) as State); const callbackData: Content = { text: "", @@ -143,7 +149,11 @@ const summarizeAction: Action = { attachments: [], }; - const attachmentData = await getAttachmentIds(runtime, message, currentState); + const attachmentData = await getAttachmentIds( + runtime, + message, + currentState + ); if (!attachmentData) { console.error("Couldn't get attachment IDs from message"); await callback(callbackData); @@ -161,23 +171,25 @@ const summarizeAction: Action = { .flatMap((msg) => msg.content.attachments) .filter((attachment) => { if (!attachment) return false; - return attachmentIds - .map((attch) => attch.toLowerCase().slice(0, 5)) - .includes(attachment.id.toLowerCase().slice(0, 5)) || + return ( + attachmentIds + .map((attch) => attch.toLowerCase().slice(0, 5)) + .includes(attachment.id.toLowerCase().slice(0, 5)) || attachmentIds.some((id) => { const attachmentId = id.toLowerCase().slice(0, 5); return attachment.id .toLowerCase() .includes(attachmentId); - }); + }) + ); }); const attachmentsWithText = attachments .map((attachment) => { - if (!attachment) return ''; + if (!attachment) return ""; return `# ${attachment.title}\n${attachment.text}`; }) - .filter(text => text !== '') + .filter((text) => text !== "") .join("\n\n"); let currentSummary = ""; @@ -227,7 +239,7 @@ ${currentSummary.trim()} } else if (currentSummary.trim()) { const summaryFilename = `content/summary_${Date.now()}`; await runtime.cacheManager.set(summaryFilename, currentSummary); - + callbackData.text = `I've attached the summary of the requested attachments as a text file.`; await callback(callbackData, [summaryFilename]); } else { @@ -270,4 +282,4 @@ ${currentSummary.trim()} ] as ActionExample[][], }; -export default summarizeAction; \ No newline at end of file +export default summarizeAction; diff --git a/packages/client-slack/src/actions/summarize_conversation.ts b/packages/client-slack/src/actions/summarize_conversation.ts index 17991c2225e..f99a77b3a39 100644 --- a/packages/client-slack/src/actions/summarize_conversation.ts +++ b/packages/client-slack/src/actions/summarize_conversation.ts @@ -1,4 +1,10 @@ -import { composeContext, generateText, splitChunks, trimTokens, parseJSONObjectFromText } from "@ai16z/eliza"; +import { + composeContext, + generateText, + splitChunks, + trimTokens, + parseJSONObjectFromText, +} from "@ai16z/eliza"; import { models } from "@ai16z/eliza"; import { getActorDetails } from "@ai16z/eliza"; import { @@ -6,14 +12,12 @@ import { ActionExample, Content, HandlerCallback, - Handler, IAgentRuntime, Media, Memory, ModelClass, State, - ServiceType, - elizaLogger + elizaLogger, } from "@ai16z/eliza"; import { ISlackService, SLACK_SERVICE_TYPE } from "../types/slack-types"; @@ -71,34 +75,40 @@ const getDateRange = async ( context, modelClass: ModelClass.SMALL, }); - + const parsedResponse = parseJSONObjectFromText(response) as { objective: string; start: string | number; end: string | number; } | null; - - if (parsedResponse?.objective && parsedResponse?.start && parsedResponse?.end) { + + if ( + parsedResponse?.objective && + parsedResponse?.start && + parsedResponse?.end + ) { // Parse time strings like "5 minutes ago", "2 hours ago", etc. const parseTimeString = (timeStr: string): number | null => { - const match = timeStr.match(/^(\d+)\s+(second|minute|hour|day)s?\s+ago$/i); + const match = timeStr.match( + /^(\d+)\s+(second|minute|hour|day)s?\s+ago$/i + ); if (!match) return null; - + const [_, amount, unit] = match; const value = parseInt(amount); - + if (isNaN(value)) return null; - + const multipliers: { [key: string]: number } = { second: 1000, minute: 60 * 1000, hour: 60 * 60 * 1000, - day: 24 * 60 * 60 * 1000 + day: 24 * 60 * 60 * 1000, }; - + const multiplier = multipliers[unit.toLowerCase()]; if (!multiplier) return null; - + return value * multiplier; }; @@ -106,18 +116,21 @@ const getDateRange = async ( const endTime = parseTimeString(parsedResponse.end as string); if (startTime === null || endTime === null) { - elizaLogger.error("Invalid time format in response", parsedResponse); + elizaLogger.error( + "Invalid time format in response", + parsedResponse + ); continue; } return { objective: parsedResponse.objective, start: Date.now() - startTime, - end: Date.now() - endTime + end: Date.now() - endTime, }; } } - + return undefined; }; @@ -135,7 +148,7 @@ const summarizeAction: Action = { validate: async ( _runtime: IAgentRuntime, message: Memory, - state: State | undefined + _state: State | undefined ): Promise => { if (message.content.source !== "slack") { return false; @@ -178,7 +191,7 @@ const summarizeAction: Action = { "bring me up to speed", "catch me up", ]; - + return keywords.some((keyword) => message.content.text.toLowerCase().includes(keyword.toLowerCase()) ); @@ -190,7 +203,7 @@ const summarizeAction: Action = { _options: any, callback: HandlerCallback ): Promise => { - const currentState = await runtime.composeState(message) as State; + const currentState = (await runtime.composeState(message)) as State; const callbackData: Content = { text: "", @@ -203,7 +216,8 @@ const summarizeAction: Action = { const dateRange = await getDateRange(runtime, message, currentState); if (!dateRange) { elizaLogger.error("Couldn't determine date range from message"); - callbackData.text = "I couldn't determine the time range to summarize. Please try asking for a specific period like 'last hour' or 'today'."; + callbackData.text = + "I couldn't determine the time range to summarize. Please try asking for a specific period like 'last hour' or 'today'."; await callback(callbackData); return callbackData; } @@ -220,7 +234,8 @@ const summarizeAction: Action = { }); if (!memories || memories.length === 0) { - callbackData.text = "I couldn't find any messages in that time range to summarize."; + callbackData.text = + "I couldn't find any messages in that time range to summarize."; await callback(callbackData); return callbackData; } @@ -235,15 +250,16 @@ const summarizeAction: Action = { const formattedMemories = memories .map((memory) => { const actor = actorMap.get(memory.userId); - const userName = actor?.name || actor?.username || "Unknown User"; + const userName = + actor?.name || actor?.username || "Unknown User"; const attachments = memory.content.attachments ?.map((attachment: Media) => { - if (!attachment) return ''; - return `---\nAttachment: ${attachment.id}\n${attachment.description || ''}\n${attachment.text || ''}\n---`; + if (!attachment) return ""; + return `---\nAttachment: ${attachment.id}\n${attachment.description || ""}\n${attachment.text || ""}\n---`; }) - .filter(text => text !== '') + .filter((text) => text !== "") .join("\n"); - return `${userName}: ${memory.content.text}\n${attachments || ''}`; + return `${userName}: ${memory.content.text}\n${attachments || ""}`; }) .join("\n"); @@ -262,7 +278,7 @@ const summarizeAction: Action = { const chunk = chunks[i]; currentState.currentSummary = currentSummary; currentState.currentChunk = chunk; - + const context = composeContext({ state: currentState, template: trimTokens( @@ -285,7 +301,8 @@ const summarizeAction: Action = { } if (!currentSummary.trim()) { - callbackData.text = "I wasn't able to generate a summary of the conversation."; + callbackData.text = + "I wasn't able to generate a summary of the conversation."; await callback(callbackData); return callbackData; } @@ -293,54 +310,73 @@ const summarizeAction: Action = { // Format dates consistently const formatDate = (timestamp: number) => { const date = new Date(timestamp); - const pad = (n: number) => n < 10 ? `0${n}` : n; + const pad = (n: number) => (n < 10 ? `0${n}` : n); return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())} ${pad(date.getHours())}:${pad(date.getMinutes())}`; }; try { // Get the user's name for the summary header const requestingUser = actorMap.get(message.userId); - const userName = requestingUser?.name || requestingUser?.username || "Unknown User"; - + const userName = + requestingUser?.name || + requestingUser?.username || + "Unknown User"; + const summaryContent = `Summary of conversation from ${formatDate(start)} to ${formatDate(end)} Here is a detailed summary of the conversation between ${userName} and ${runtime.character.name}:\n\n${currentSummary.trim()}`; - + // If summary is long, upload as a file if (summaryContent.length > 1000) { const summaryFilename = `summary_${Date.now()}.txt`; elizaLogger.debug("Uploading summary file to Slack..."); - + try { // Save file content - await runtime.cacheManager.set(summaryFilename, summaryContent); - + await runtime.cacheManager.set( + summaryFilename, + summaryContent + ); + // Get the Slack service from runtime - const slackService = runtime.getService(SLACK_SERVICE_TYPE) as ISlackService; + const slackService = runtime.getService( + SLACK_SERVICE_TYPE + ) as ISlackService; if (!slackService?.client) { - elizaLogger.error("Slack service not found or not properly initialized"); - throw new Error('Slack service not found'); + elizaLogger.error( + "Slack service not found or not properly initialized" + ); + throw new Error("Slack service not found"); } // Upload file using Slack's API - elizaLogger.debug(`Uploading file ${summaryFilename} to channel ${message.roomId}`); - const uploadResult = await slackService.client.files.upload({ - channels: message.roomId, - filename: summaryFilename, - title: 'Conversation Summary', - content: summaryContent, - initial_comment: `I've created a summary of the conversation from ${formatDate(start)} to ${formatDate(end)}.` - }); - + elizaLogger.debug( + `Uploading file ${summaryFilename} to channel ${message.roomId}` + ); + const uploadResult = await slackService.client.files.upload( + { + channels: message.roomId, + filename: summaryFilename, + title: "Conversation Summary", + content: summaryContent, + initial_comment: `I've created a summary of the conversation from ${formatDate(start)} to ${formatDate(end)}.`, + } + ); + if (uploadResult.ok) { - elizaLogger.success("Successfully uploaded summary file to Slack"); + elizaLogger.success( + "Successfully uploaded summary file to Slack" + ); callbackData.text = `I've created a summary of the conversation from ${formatDate(start)} to ${formatDate(end)}. You can find it in the thread above.`; } else { - elizaLogger.error("Failed to upload file to Slack:", uploadResult.error); - throw new Error('Failed to upload file to Slack'); + elizaLogger.error( + "Failed to upload file to Slack:", + uploadResult.error + ); + throw new Error("Failed to upload file to Slack"); } } catch (error) { - elizaLogger.error('Error uploading summary file:', error); + elizaLogger.error("Error uploading summary file:", error); // Fallback to sending as a message callbackData.text = summaryContent; } @@ -348,13 +384,13 @@ Here is a detailed summary of the conversation between ${userName} and ${runtime // For shorter summaries, just send as a message callbackData.text = summaryContent; } - + await callback(callbackData); return callbackData; - } catch (error) { elizaLogger.error("Error in summary generation:", error); - callbackData.text = "I encountered an error while generating the summary. Please try again."; + callbackData.text = + "I encountered an error while generating the summary. Please try again."; await callback(callbackData); return callbackData; } @@ -393,4 +429,4 @@ Here is a detailed summary of the conversation between ${userName} and ${runtime ] as ActionExample[][], }; -export default summarizeAction; \ No newline at end of file +export default summarizeAction; diff --git a/packages/client-slack/src/actions/transcribe_media.ts b/packages/client-slack/src/actions/transcribe_media.ts index fa3b8c29369..b1801b855b7 100644 --- a/packages/client-slack/src/actions/transcribe_media.ts +++ b/packages/client-slack/src/actions/transcribe_media.ts @@ -1,4 +1,8 @@ -import { composeContext, generateText, parseJSONObjectFromText } from "@ai16z/eliza"; +import { + composeContext, + generateText, + parseJSONObjectFromText, +} from "@ai16z/eliza"; import { Action, ActionExample, @@ -72,7 +76,7 @@ const transcribeMediaAction: Action = { validate: async ( _runtime: IAgentRuntime, message: Memory, - state: State | undefined + _state: State | undefined ): Promise => { if (message.content.source !== "slack") { return false; @@ -106,7 +110,7 @@ const transcribeMediaAction: Action = { _options: any, callback: HandlerCallback ): Promise => { - const currentState = await runtime.composeState(message) as State; + const currentState = (await runtime.composeState(message)) as State; const callbackData: Content = { text: "", @@ -133,12 +137,12 @@ const transcribeMediaAction: Action = { msg.content.attachments.length > 0 ) .flatMap((msg) => msg.content.attachments) - .find( - (attachment) => { - if (!attachment) return false; - return attachment.id.toLowerCase() === attachmentId.toLowerCase(); - } - ); + .find((attachment) => { + if (!attachment) return false; + return ( + attachment.id.toLowerCase() === attachmentId.toLowerCase() + ); + }); if (!attachment) { console.error(`Couldn't find attachment with ID ${attachmentId}`); @@ -146,7 +150,7 @@ const transcribeMediaAction: Action = { return callbackData; } - const mediaTranscript = attachment.text || ''; + const mediaTranscript = attachment.text || ""; callbackData.text = mediaTranscript.trim(); if ( @@ -210,4 +214,4 @@ ${mediaTranscript.trim()} ] as ActionExample[][], }; -export default transcribeMediaAction; \ No newline at end of file +export default transcribeMediaAction; diff --git a/packages/client-slack/src/examples/standalone-attachment.ts b/packages/client-slack/src/examples/standalone-attachment.ts index ab3751dc6b0..fd71c133002 100644 --- a/packages/client-slack/src/examples/standalone-attachment.ts +++ b/packages/client-slack/src/examples/standalone-attachment.ts @@ -1,27 +1,26 @@ -import { config } from 'dotenv'; -import { WebClient } from '@slack/web-api'; -import { SlackClientProvider } from '../providers/slack-client.provider'; -import { AttachmentManager } from '../attachments'; -import { SlackConfig } from '../types/slack-types'; -import path from 'path'; +import { config } from "dotenv"; +import { SlackClientProvider } from "../providers/slack-client.provider"; +import { AttachmentManager } from "../attachments"; +import { SlackConfig } from "../types/slack-types"; +import path from "path"; // Load environment variables -config({ path: path.resolve(__dirname, '../../../.env') }); +config({ path: path.resolve(__dirname, "../../../.env") }); -console.log('\n=== Starting Slack Attachment Example ===\n'); +console.log("\n=== Starting Slack Attachment Example ===\n"); // Load environment variables const slackConfig: SlackConfig = { - appId: process.env.SLACK_APP_ID || '', - clientId: process.env.SLACK_CLIENT_ID || '', - clientSecret: process.env.SLACK_CLIENT_SECRET || '', - signingSecret: process.env.SLACK_SIGNING_SECRET || '', - verificationToken: process.env.SLACK_VERIFICATION_TOKEN || '', - botToken: process.env.SLACK_BOT_TOKEN || '', - botId: process.env.SLACK_BOT_ID || '', + appId: process.env.SLACK_APP_ID || "", + clientId: process.env.SLACK_CLIENT_ID || "", + clientSecret: process.env.SLACK_CLIENT_SECRET || "", + signingSecret: process.env.SLACK_SIGNING_SECRET || "", + verificationToken: process.env.SLACK_VERIFICATION_TOKEN || "", + botToken: process.env.SLACK_BOT_TOKEN || "", + botId: process.env.SLACK_BOT_ID || "", }; -console.log('Environment variables loaded:'); +console.log("Environment variables loaded:"); Object.entries(slackConfig).forEach(([key, value]) => { if (value) { console.log(`${key}: ${value.slice(0, 4)}...${value.slice(-4)}`); @@ -32,41 +31,47 @@ Object.entries(slackConfig).forEach(([key, value]) => { async function runExample() { try { - console.log('\nInitializing Slack client...'); + console.log("\nInitializing Slack client..."); const provider = new SlackClientProvider(slackConfig); const client = provider.getContext().client; - console.log('\nValidating Slack connection...'); + console.log("\nValidating Slack connection..."); const isValid = await provider.validateConnection(); if (!isValid) { - throw new Error('Failed to validate Slack connection'); + throw new Error("Failed to validate Slack connection"); } - console.log('✓ Successfully connected to Slack'); + console.log("✓ Successfully connected to Slack"); // Test file upload const channelId = process.env.SLACK_CHANNEL_ID; if (!channelId) { - throw new Error('SLACK_CHANNEL_ID is required'); + throw new Error("SLACK_CHANNEL_ID is required"); } - console.log('\nSending test message with attachment...'); - const testMessage = 'Here is a test message with an attachment'; - + console.log("\nSending test message with attachment..."); + const testMessage = "Here is a test message with an attachment"; + // Create a test file - const testFilePath = path.join(__dirname, 'test.txt'); - const fs = require('fs'); - fs.writeFileSync(testFilePath, 'This is a test file content for attachment testing.'); + const testFilePath = path.join(__dirname, "test.txt"); + async function loadFs() { + return await import("fs"); + } + const fs = await loadFs(); + fs.writeFileSync( + testFilePath, + "This is a test file content for attachment testing." + ); // Upload the file const fileUpload = await client.files.upload({ channels: channelId, file: fs.createReadStream(testFilePath), - filename: 'test.txt', - title: 'Test Attachment', + filename: "test.txt", + title: "Test Attachment", initial_comment: testMessage, }); - console.log('✓ File uploaded successfully'); + console.log("✓ File uploaded successfully"); // Initialize AttachmentManager const runtime = { @@ -78,30 +83,30 @@ async function runExample() { // Process the uploaded file if (fileUpload.file) { - console.log('\nProcessing attachment...'); - const processedAttachment = await attachmentManager.processAttachment({ - id: fileUpload.file.id, - url_private: fileUpload.file.url_private || '', - name: fileUpload.file.name || '', - size: fileUpload.file.size || 0, - mimetype: fileUpload.file.mimetype || 'text/plain', - title: fileUpload.file.title || '', - }); - - console.log('✓ Attachment processed:', processedAttachment); + console.log("\nProcessing attachment..."); + const processedAttachment = + await attachmentManager.processAttachment({ + id: fileUpload.file.id, + url_private: fileUpload.file.url_private || "", + name: fileUpload.file.name || "", + size: fileUpload.file.size || 0, + mimetype: fileUpload.file.mimetype || "text/plain", + title: fileUpload.file.title || "", + }); + + console.log("✓ Attachment processed:", processedAttachment); } // Cleanup fs.unlinkSync(testFilePath); - console.log('\n✓ Test completed successfully'); - + console.log("\n✓ Test completed successfully"); } catch (error) { - console.error('Error:', error); + console.error("Error:", error); process.exit(1); } } runExample().then(() => { - console.log('\n=== Example completed ===\n'); + console.log("\n=== Example completed ===\n"); process.exit(0); -}); \ No newline at end of file +});