diff --git a/docs/core_docs/docs/integrations/tools/slack.mdx b/docs/core_docs/docs/integrations/tools/slack.mdx new file mode 100644 index 000000000000..c6e1f12bca61 --- /dev/null +++ b/docs/core_docs/docs/integrations/tools/slack.mdx @@ -0,0 +1,43 @@ +--- +hide_table_of_contents: true +--- + +import CodeBlock from "@theme/CodeBlock"; + +# Slack Tool + +The Slack Tool gives your agent the ability to search, schedule and post messages to Slack Channels. + + +## Setup + +To use the Slack Tool you need to install the following official peer dependency: + +```bash npm2yarn +npm install @slack/web-api +``` + +## Usage, standalone + +import ToolExample from "@examples/tools/slack.ts"; + +import IntegrationInstallTooltip from "@mdx_components/integration_install_tooltip.mdx"; + + + +```bash npm2yarn +npm install @langchain/openai @langchain/core +``` + +{ToolExample} + +## Usage, in an Agent + +import AgentExample from "@examples/agents/slack.ts"; + +{AgentExample} + +## Related + +- Tool [conceptual guide](/docs/concepts/tools) +- Tool [how-to guides](/docs/how_to/#tools) diff --git a/examples/src/agents/slack.ts b/examples/src/agents/slack.ts new file mode 100644 index 000000000000..7e17faa2cb13 --- /dev/null +++ b/examples/src/agents/slack.ts @@ -0,0 +1,68 @@ +import { + SlackGetMessagesTool, + SlackGetChannelsTool, + SlackScheduleMessageTool, + SlackPostMessageTool, +} from "@langchain/community/tools/slack"; +import { AgentExecutor, createOpenAIToolsAgent } from "langchain/agents"; +import { HumanMessage } from "@langchain/core/messages"; +import { ChatOpenAI } from "@langchain/openai"; +import { + ChatPromptTemplate, + MessagesPlaceholder, +} from "@langchain/core/prompts"; + +const chat = new ChatOpenAI({ + model: "gpt-3.5-turbo-1106", + temperature: 0, +}); + +const prompt = ChatPromptTemplate.fromMessages([ + [ + "system", + "You are a helpful assistant. You may not need to use tools for every query - the user may just want to chat!", + ], + new MessagesPlaceholder("messages"), + new MessagesPlaceholder("agent_scratchpad"), +]); + +const tools = [ + new SlackGetMessagesTool(), + new SlackGetChannelsTool(), + new SlackScheduleMessageTool(), + new SlackPostMessageTool(), +]; + +const agent = await createOpenAIToolsAgent({ + llm: chat, + tools, + prompt, +}); + +const agentExecutor = new AgentExecutor({ agent, tools, maxIterations: 20, }); + +let res = await agentExecutor.invoke({ + messages: [ + new HumanMessage("send a greeting message to the general channel"), + ], +}); + +console.log(res.output); + +res = await agentExecutor.invoke({ + messages: [ + new HumanMessage("Schedule a greeting message to the general channel at 11:15 am on December 12, 2024 in New York time."), + ], +}); + +console.log(res.output); + +res = await agentExecutor.invoke({ + messages: [ + new HumanMessage("When did I say 'hi' in the slack channels?"), + ], +}); + +console.log(res.output); + + diff --git a/examples/src/tools/slack.ts b/examples/src/tools/slack.ts new file mode 100644 index 000000000000..064f4b96660a --- /dev/null +++ b/examples/src/tools/slack.ts @@ -0,0 +1,37 @@ +import { + SlackGetChannelsTool, + SlackGetMessagesTool, + SlackScheduleMessageTool, + SlackPostMessageTool, +} from "@langchain/community/tools/slack"; + +// Get messages given a query +const getMessageTool = new SlackGetMessagesTool(); +const messageResults = await getMessageTool.invoke("Hi"); +console.log(messageResults); + +// Get information about Slack channels +const getChannelTool = new SlackGetChannelsTool(); +const channelResults = await getChannelTool.invoke(""); +console.log(channelResults); + +// Schedule a slack message given a message, channel and time +const scheduleMessageTool = new SlackScheduleMessageTool(); +const scheduleResults = await scheduleMessageTool.invoke( + JSON.stringify({ + text: "Test", + channel_id: "C1234567890", + post_at: "2024-12-09T10:30:00+03:00", + }) +); +console.log(scheduleResults); + +// Post a message to a given channel +const postMessageTool = new SlackPostMessageTool(); +const postResult = await postMessageTool.invoke( + JSON.stringify({ + text: "Test", + channel_id: "C1234567890", + }) +); +console.log(postResult); diff --git a/libs/langchain-community/.gitignore b/libs/langchain-community/.gitignore index e6ae5fa54a4f..7f0d213b5295 100644 --- a/libs/langchain-community/.gitignore +++ b/libs/langchain-community/.gitignore @@ -94,6 +94,10 @@ tools/serper.cjs tools/serper.js tools/serper.d.ts tools/serper.d.cts +tools/slack.cjs +tools/slack.js +tools/slack.d.ts +tools/slack.d.cts tools/stackexchange.cjs tools/stackexchange.js tools/stackexchange.d.ts diff --git a/libs/langchain-community/langchain.config.js b/libs/langchain-community/langchain.config.js index 4a402c6941e8..af4a5161a1eb 100644 --- a/libs/langchain-community/langchain.config.js +++ b/libs/langchain-community/langchain.config.js @@ -57,6 +57,7 @@ export const config = { "tools/searxng_search": "tools/searxng_search", "tools/serpapi": "tools/serpapi", "tools/serper": "tools/serper", + "tools/slack": "tools/slack", "tools/stackexchange": "tools/stackexchange", "tools/tavily_search": "tools/tavily_search", "tools/wikipedia_query_run": "tools/wikipedia_query_run", @@ -338,6 +339,7 @@ export const config = { "tools/discord", "tools/gmail", "tools/google_calendar", + "tools/slack", "agents/toolkits/aws_sfn", "agents/toolkits/stagehand", "callbacks/handlers/llmonitor", diff --git a/libs/langchain-community/package.json b/libs/langchain-community/package.json index 7b826ad1e106..a8e292c5de99 100644 --- a/libs/langchain-community/package.json +++ b/libs/langchain-community/package.json @@ -98,6 +98,7 @@ "@qdrant/js-client-rest": "^1.8.2", "@raycast/api": "^1.83.1", "@rockset/client": "^0.9.1", + "@slack/web-api": "^7.7.0", "@smithy/eventstream-codec": "^2.0.5", "@smithy/protocol-http": "^3.0.6", "@smithy/signature-v4": "^2.0.10", @@ -265,6 +266,7 @@ "@qdrant/js-client-rest": "^1.8.2", "@raycast/api": "^1.55.2", "@rockset/client": "^0.9.1", + "@slack/web-api": "^7.7.0", "@smithy/eventstream-codec": "^2.0.5", "@smithy/protocol-http": "^3.0.6", "@smithy/signature-v4": "^2.0.10", @@ -473,6 +475,9 @@ "@rockset/client": { "optional": true }, + "@slack/web-api": { + "optional": true + }, "@smithy/eventstream-codec": { "optional": true }, @@ -928,6 +933,15 @@ "import": "./tools/serper.js", "require": "./tools/serper.cjs" }, + "./tools/slack": { + "types": { + "import": "./tools/slack.d.ts", + "require": "./tools/slack.d.cts", + "default": "./tools/slack.d.ts" + }, + "import": "./tools/slack.js", + "require": "./tools/slack.cjs" + }, "./tools/stackexchange": { "types": { "import": "./tools/stackexchange.d.ts", @@ -3197,6 +3211,10 @@ "tools/serper.js", "tools/serper.d.ts", "tools/serper.d.cts", + "tools/slack.cjs", + "tools/slack.js", + "tools/slack.d.ts", + "tools/slack.d.cts", "tools/stackexchange.cjs", "tools/stackexchange.js", "tools/stackexchange.d.ts", diff --git a/libs/langchain-community/src/load/import_constants.ts b/libs/langchain-community/src/load/import_constants.ts index 722dd82e678b..6864e8b0a084 100644 --- a/libs/langchain-community/src/load/import_constants.ts +++ b/libs/langchain-community/src/load/import_constants.ts @@ -7,6 +7,7 @@ export const optionalImportEntrypoints: string[] = [ "langchain_community/tools/discord", "langchain_community/tools/gmail", "langchain_community/tools/google_calendar", + "langchain_community/tools/slack", "langchain_community/agents/toolkits/aws_sfn", "langchain_community/agents/toolkits/stagehand", "langchain_community/embeddings/bedrock", diff --git a/libs/langchain-community/src/tools/slack.ts b/libs/langchain-community/src/tools/slack.ts new file mode 100644 index 000000000000..a40a3b5519bf --- /dev/null +++ b/libs/langchain-community/src/tools/slack.ts @@ -0,0 +1,269 @@ +import { WebClient } from "@slack/web-api"; +import { getEnvironmentVariable } from "@langchain/core/utils/env"; +import { Tool } from "@langchain/core/tools"; + +/** + * Base tool parameters for the Slack tools + */ +interface SlackToolParams { + token?: string; + client?: WebClient; +} + +/** + * A tool for retrieving messages from a slack channel using a bot. + * It extends the base Tool class and implements the _call method to + * perform the retrieval operation. Requires a slack user token which can be set + * in the environment variables. + * The _call method takes the search query as the input argument. + * It returns the messages including the channel id and name, the text, + * the timestamp, team, user id and username. + */ +export class SlackGetMessagesTool extends Tool { + static lc_name() { + return "SlackGetMessagesTool"; + } + + name = "slack-get-messages"; + + description = `A slack tool. useful for reading messages from a slack channel. + Input should be a search query.`; + + protected token: string; + + protected client: WebClient; + + constructor(fields?: SlackToolParams) { + super(); + + const { token = getEnvironmentVariable("SLACK_TOKEN"), client } = + fields ?? {}; + + if (!token) { + throw new Error( + "Environment variable SLACK_TOKEN missing, but is required for SlackGetMessagesTool." + ); + } + + this.client = client ?? new WebClient(token); + + this.token = token; + } + + /** @ignore */ + async _call(searchTerm: string): Promise { + try { + const results = await this.client.search.messages({ + query: searchTerm, + }); + + const filtered = + results.messages?.matches?.map((match: any) => ({ + channel_id: match.channel.id, + channel_name: match.channel.name, + team: match.team, + text: match.text, + ts: match.ts, + user: match.user, + username: match.username, + })) ?? []; + + return JSON.stringify(filtered); + } catch (err) { + return "Error getting messages."; + } + } +} + +/** + * A tool for retrieving channels from a slack team. + * It extends the base Tool class and implements the _call method to + * perform the retrieval operation. Requires a slack user token which can be set + * in the environment variables. + * It returns channel information including the channel name, id, created time, + * topic, user membership, purpose and number of members. + */ +export class SlackGetChannelsTool extends Tool { + static lc_name() { + return "SlackGetChannelsTool"; + } + + name = "slack-get-channels"; + + description = `A slack tool. useful for retrieving a list of details about channels in a slack team. + This includes channel names and channel ids. There is no input to this tool.`; + + protected token: string; + + protected client: WebClient; + + constructor(fields?: SlackToolParams) { + super(); + + const { token = getEnvironmentVariable("SLACK_TOKEN"), client } = + fields ?? {}; + + if (!token) { + throw new Error( + "Environment variable SLACK_TOKEN missing, but is required for SlackGetChannelsTool." + ); + } + + this.client = client ?? new WebClient(token); + + this.token = token; + } + + /** @ignore */ + async _call(_input: string): Promise { + try { + const results = await this.client.conversations.list(); + + const filtered = + results.channels?.map((result: any) => ({ + channel_id: result.id, + channel_name: result.name, + created: result.created, + topic: result.topic.value, + is_member: result.is_member, + purpose: result.purpose.value, + num_members: result.num_members, + })) ?? []; + + return JSON.stringify(filtered); + } catch (err) { + return "Error getting channel information."; + } + } +} + +/** + * A tool for scheduling messages to be posted on slack channels using a bot. + * It extends the base Tool class and implements the _call method to + * perform the schedule operation. Requires a slack user token which can be set + * in the environment variables. + * The _call method takes the a JSON object in the format + * {text: [text here], channel_id: [channel id here], post_at: [post at here]} as its input. + * It returns the message text, the time to post the message and the channel id. + */ +export class SlackScheduleMessageTool extends Tool { + static lc_name() { + return "SlackScheduleMessageTool"; + } + + name = "slack-schedule-message"; + + description = `A slack tool. useful for scheduling messages to send on a specific date and time. + Input is a JSON object as follows '{text: [text here], channel_id: [channel id here], post_at: [post at here]}' where post_at is the datetime for when the message should be sent in the following format: YYYY-MM-DDTHH:MM:SS±hh:mm, where "T" separates the date + and time components, and the time zone offset is specified as ±hh:mm. + For example: "2023-06-09T10:30:00+03:00" represents June 9th + 2023, at 10:30 AM in a time zone with a positive offset of +03:00 + hours from Coordinated Universal Time (UTC).`; + + protected token: string; + + protected client: WebClient; + + constructor(fields?: SlackToolParams) { + super(); + + const { token = getEnvironmentVariable("SLACK_TOKEN"), client } = + fields ?? {}; + + if (!token) { + throw new Error( + "Environment variable SLACK_TOKEN missing, but is required for SlackScheduleMessageTool." + ); + } + + this.client = client ?? new WebClient(token); + + this.token = token; + } + + /** @ignore */ + async _call(input: string): Promise { + try { + const obj = JSON.parse(input); + const date = new Date(obj.post_at); + const utcTimestamp = date.getTime() / 1000; + + const results = await this.client.chat.scheduleMessage({ + channel: obj.channel_id, + post_at: utcTimestamp, + text: obj.text, + }); + + const filtered = { + channel_id: results.channel, + post_at: results.post_at, + text: results.message?.text, + }; + + return JSON.stringify(filtered); + } catch (err) { + return "Error scheduling message."; + } + } +} + +/** + * A tool for posting messages to a slack channel using a bot. + * It extends the base Tool class and implements the _call method to + * perform the post operation. Requires a slack user token which can be set + * in the environment variables. + * The _call method takes a JSON object in the format {chanel id, text} as its input. + * It returns the message information including the text, timestamp ID and channel id. + */ +export class SlackPostMessageTool extends Tool { + static lc_name() { + return "SlackPostMessageTool"; + } + + name = "slack-post-message"; + + description = `A slack tool. useful for posting a message to a channel + Input is a JSON object as follows '{channel_id, text}'`; + + protected token: string; + + protected client: WebClient; + + constructor(fields?: SlackToolParams) { + super(); + + const { token = getEnvironmentVariable("SLACK_TOKEN"), client } = + fields ?? {}; + + if (!token) { + throw new Error( + "Environment variable SLACK_TOKEN missing, but is required for SlackPostMessageTool." + ); + } + + this.client = client ?? new WebClient(token); + + this.token = token; + } + + /** @ignore */ + async _call(input: string): Promise { + try { + const obj = JSON.parse(input); + const results = await this.client.chat.postMessage({ + text: obj.text, + channel: obj.channel_id, + }); + + const filtered = { + channel_id: results.channel, + ts: results.ts, + text: results.message?.text, + }; + + return JSON.stringify(filtered); + } catch (err) { + return "Error posting message."; + } + } +} diff --git a/libs/langchain-community/src/tools/tests/slack.int.test.ts b/libs/langchain-community/src/tools/tests/slack.int.test.ts new file mode 100644 index 000000000000..5602b87b1140 --- /dev/null +++ b/libs/langchain-community/src/tools/tests/slack.int.test.ts @@ -0,0 +1,50 @@ +import { test } from "@jest/globals"; +import { + SlackGetChannelsTool, + SlackGetMessagesTool, + SlackScheduleMessageTool, + SlackPostMessageTool, +} from "../slack.js"; + +test.skip("SlackGetMessagesTool", async () => { + const tool = new SlackGetMessagesTool(); + + const result = await tool.invoke("Hi"); + // console.log(result); + expect(result).toBeTruthy(); + expect(result).not.toContain("Error getting messages."); +}); + +test.skip("SlackGetChannelsTool", async () => { + const tool = new SlackGetChannelsTool(); + const result = await tool.invoke(""); + // console.log(result); + expect(result).toBeTruthy(); + expect(result).not.toContain("Error getting channel information."); +}); + +test.skip("SlackScheduleMessageTool", async () => { + const tool = new SlackScheduleMessageTool(); + const result = await tool.invoke( + JSON.stringify({ + text: "Test", + channel_id: "C1234567890", + post_at: "2024-12-09T10:30:00+03:00", + }) + ); + // console.log(result); + expect(result).toBeTruthy(); + expect(result).not.toContain("Error scheduling message."); +}); + +test.skip("SlackPostMessageTool", async () => { + const tool = new SlackPostMessageTool(); + const result = await tool.invoke( + JSON.stringify({ + text: "Test", + channel_id: "C1234567890", + }) + ); + // console.log(result); + expect(result).not.toContain("Error posting message."); +}); diff --git a/yarn.lock b/yarn.lock index 5e6b6ed60a57..ece5c492fae2 100644 --- a/yarn.lock +++ b/yarn.lock @@ -11740,6 +11740,7 @@ __metadata: "@qdrant/js-client-rest": ^1.8.2 "@raycast/api": ^1.83.1 "@rockset/client": ^0.9.1 + "@slack/web-api": ^7.7.0 "@smithy/eventstream-codec": ^2.0.5 "@smithy/protocol-http": ^3.0.6 "@smithy/signature-v4": ^2.0.10 @@ -11915,6 +11916,7 @@ __metadata: "@qdrant/js-client-rest": ^1.8.2 "@raycast/api": ^1.55.2 "@rockset/client": ^0.9.1 + "@slack/web-api": ^7.7.0 "@smithy/eventstream-codec": ^2.0.5 "@smithy/protocol-http": ^3.0.6 "@smithy/signature-v4": ^2.0.10 @@ -12080,6 +12082,8 @@ __metadata: optional: true "@rockset/client": optional: true + "@slack/web-api": + optional: true "@smithy/eventstream-codec": optional: true "@smithy/protocol-http": @@ -15071,6 +15075,42 @@ __metadata: languageName: node linkType: hard +"@slack/logger@npm:^4.0.0": + version: 4.0.0 + resolution: "@slack/logger@npm:4.0.0" + dependencies: + "@types/node": ">=18.0.0" + checksum: dc79e9d2032c4bf9ce01d96cc72882f003dd376d036f172d4169662cfc2c9b384a80d5546b06021578dd473e7059f064303f0ba851eeb153387f2081a1e3062e + languageName: node + linkType: hard + +"@slack/types@npm:^2.9.0": + version: 2.14.0 + resolution: "@slack/types@npm:2.14.0" + checksum: fbef74d50d0de8f16125f7178bd2e664a69eeefd827b09c6f78153957278fc4400049685c756076e7dbcabd04c22730ac783bcc5d36fd588c7749d35f02c2afd + languageName: node + linkType: hard + +"@slack/web-api@npm:^7.7.0": + version: 7.7.0 + resolution: "@slack/web-api@npm:7.7.0" + dependencies: + "@slack/logger": ^4.0.0 + "@slack/types": ^2.9.0 + "@types/node": ">=18.0.0" + "@types/retry": 0.12.0 + axios: ^1.7.4 + eventemitter3: ^5.0.1 + form-data: ^4.0.0 + is-electron: 2.2.2 + is-stream: ^2 + p-queue: ^6 + p-retry: ^4 + retry: ^0.13.1 + checksum: f65986446c8debb2dc09fb00425aa47afb69d2c73958cd4791fc38ee8806873696e510ad2237e19e82c56e048017cd2164b50a971c6523ba2d1cb38fd821ac9c + languageName: node + linkType: hard + "@slorber/static-site-generator-webpack-plugin@npm:^4.0.7": version: 4.0.7 resolution: "@slorber/static-site-generator-webpack-plugin@npm:4.0.7" @@ -19536,6 +19576,15 @@ __metadata: languageName: node linkType: hard +"@types/node@npm:>=18.0.0": + version: 22.10.1 + resolution: "@types/node@npm:22.10.1" + dependencies: + undici-types: ~6.20.0 + checksum: 5a9b81500f288a8fb757b61bd939f99f72b6cb59347a5bae52dd1c2c87100ebbaa9da4256ef3cb9add2090e8704cda1d9a1ffc14ccd5db47a6466c8bae10ebcb + languageName: node + linkType: hard + "@types/node@npm:>=8": version: 20.8.4 resolution: "@types/node@npm:20.8.4" @@ -21914,6 +21963,17 @@ __metadata: languageName: node linkType: hard +"axios@npm:^1.7.4": + version: 1.7.8 + resolution: "axios@npm:1.7.8" + dependencies: + follow-redirects: ^1.15.6 + form-data: ^4.0.0 + proxy-from-env: ^1.1.0 + checksum: 3d21652faf8e29fb36c47517d2872bb5e2285127a24f5c53ce23082c4eac7f5a88de84dd49d4a1a83068e5301dcfd9067b41e5fbd00b0d20ab7b0a843559273d + languageName: node + linkType: hard + "axios@npm:^1.7.5": version: 1.7.7 resolution: "axios@npm:1.7.7" @@ -31138,6 +31198,13 @@ __metadata: languageName: node linkType: hard +"is-electron@npm:2.2.2": + version: 2.2.2 + resolution: "is-electron@npm:2.2.2" + checksum: de5aa8bd8d72c96675b8d0f93fab4cc21f62be5440f65bc05c61338ca27bd851a64200f31f1bf9facbaa01b3dbfed7997b2186741d84b93b63e0aff1db6a9494 + languageName: node + linkType: hard + "is-extendable@npm:^0.1.0": version: 0.1.1 resolution: "is-extendable@npm:0.1.1" @@ -31507,7 +31574,7 @@ __metadata: languageName: node linkType: hard -"is-stream@npm:^2.0.0": +"is-stream@npm:^2, is-stream@npm:^2.0.0": version: 2.0.1 resolution: "is-stream@npm:2.0.1" checksum: b8e05ccdf96ac330ea83c12450304d4a591f9958c11fd17bed240af8d5ffe08aedafa4c0f4cfccd4d28dc9d4d129daca1023633d5c11601a6cbc77521f6fae66 @@ -36487,7 +36554,7 @@ __metadata: languageName: node linkType: hard -"p-queue@npm:^6.6.2": +"p-queue@npm:^6, p-queue@npm:^6.6.2": version: 6.6.2 resolution: "p-queue@npm:6.6.2" dependencies: @@ -36507,7 +36574,7 @@ __metadata: languageName: node linkType: hard -"p-retry@npm:4, p-retry@npm:^4.5.0": +"p-retry@npm:4, p-retry@npm:^4, p-retry@npm:^4.5.0": version: 4.6.2 resolution: "p-retry@npm:4.6.2" dependencies: @@ -42886,6 +42953,13 @@ __metadata: languageName: node linkType: hard +"undici-types@npm:~6.20.0": + version: 6.20.0 + resolution: "undici-types@npm:6.20.0" + checksum: b7bc50f012dc6afbcce56c9fd62d7e86b20a62ff21f12b7b5cbf1973b9578d90f22a9c7fe50e638e96905d33893bf2f9f16d98929c4673c2480de05c6c96ea8b + languageName: node + linkType: hard + "undici@npm:5.27.2": version: 5.27.2 resolution: "undici@npm:5.27.2"