diff --git a/docs/docs/integrations/toolkits/slack.ipynb b/docs/docs/integrations/toolkits/slack.ipynb new file mode 100644 index 00000000000..ece8d8a59d9 --- /dev/null +++ b/docs/docs/integrations/toolkits/slack.ipynb @@ -0,0 +1,147 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Slack\n", + "\n", + "This notebook walks through connecting LangChain to your `Slack` account.\n", + "\n", + "To use this toolkit, you will need to get a token explained in the [Slack API docs](https://api.slack.com/tutorials/tracks/getting-a-token). Once you've received a SLACK_USER_TOKEN, you can input it as an environmental variable below." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "!pip install --upgrade slack_sdk > /dev/null\n", + "!pip install beautifulsoup4 > /dev/null # This is optional but is useful for parsing HTML messages" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Assign Environmental Variables\n", + "\n", + "The toolkit will read the SLACK_USER_TOKEN environmental variable to authenticate the user so you need to set them here. You will also need to set your OPENAI_API_KEY to use the agent later." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Set environmental variables here" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Create the Toolkit and Get Tools\n", + "\n", + "To start, you need to create the toolkit, so you can access its tools later." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "from langchain.agents.agent_toolkits import SlackToolkit\n", + "\n", + "toolkit = SlackToolkit()\n", + "tools = toolkit.get_tools()\n", + "tools" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Use within an Agent" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "from langchain.agents import AgentType, initialize_agent\n", + "from langchain.llms import OpenAI" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "llm = OpenAI(temperature=0)\n", + "agent = initialize_agent(\n", + " tools=toolkit.get_tools(),\n", + " llm=llm,\n", + " verbose=False,\n", + " agent=AgentType.STRUCTURED_CHAT_ZERO_SHOT_REACT_DESCRIPTION,\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "agent.run(\"Send a greeting to my coworkers in the #general channel.\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "agent.run(\"How many channels are in the workspace? Please list out their names.\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "agent.run(\n", + " \"Tell me the number of messages sent in the #introductions channel from the past month.\"\n", + ")" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.11.6" + } + }, + "nbformat": 4, + "nbformat_minor": 4 +} diff --git a/libs/langchain/langchain/agents/agent_toolkits/__init__.py b/libs/langchain/langchain/agents/agent_toolkits/__init__.py index 324b1e2942b..2266b2413bb 100644 --- a/libs/langchain/langchain/agents/agent_toolkits/__init__.py +++ b/libs/langchain/langchain/agents/agent_toolkits/__init__.py @@ -42,6 +42,7 @@ from langchain.agents.agent_toolkits.playwright.toolkit import PlayWrightBrowser from langchain.agents.agent_toolkits.powerbi.base import create_pbi_agent from langchain.agents.agent_toolkits.powerbi.chat_base import create_pbi_chat_agent from langchain.agents.agent_toolkits.powerbi.toolkit import PowerBIToolkit +from langchain.agents.agent_toolkits.slack.toolkit import SlackToolkit from langchain.agents.agent_toolkits.spark_sql.base import create_spark_sql_agent from langchain.agents.agent_toolkits.spark_sql.toolkit import SparkSQLToolkit from langchain.agents.agent_toolkits.sql.base import create_sql_agent @@ -96,6 +97,7 @@ __all__ = [ "OpenAPIToolkit", "PlayWrightBrowserToolkit", "PowerBIToolkit", + "SlackToolkit", "SQLDatabaseToolkit", "SparkSQLToolkit", "VectorStoreInfo", diff --git a/libs/langchain/langchain/agents/agent_toolkits/slack/__init__.py b/libs/langchain/langchain/agents/agent_toolkits/slack/__init__.py new file mode 100644 index 00000000000..1ec5ae704ce --- /dev/null +++ b/libs/langchain/langchain/agents/agent_toolkits/slack/__init__.py @@ -0,0 +1 @@ +"""Slack toolkit.""" diff --git a/libs/langchain/langchain/agents/agent_toolkits/slack/toolkit.py b/libs/langchain/langchain/agents/agent_toolkits/slack/toolkit.py new file mode 100644 index 00000000000..29fae3f01b3 --- /dev/null +++ b/libs/langchain/langchain/agents/agent_toolkits/slack/toolkit.py @@ -0,0 +1,35 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING, List + +from langchain.agents.agent_toolkits.base import BaseToolkit +from langchain.pydantic_v1 import Field +from langchain.tools import BaseTool +from langchain.tools.slack.get_channel import SlackGetChannel +from langchain.tools.slack.get_message import SlackGetMessage +from langchain.tools.slack.schedule_message import SlackScheduleMessage +from langchain.tools.slack.send_message import SlackSendMessage +from langchain.tools.slack.utils import login + +if TYPE_CHECKING: + from slack_sdk import WebClient + + +class SlackToolkit(BaseToolkit): + """Toolkit for interacting with Slack.""" + + client: WebClient = Field(default_factory=login) + + class Config: + """Pydantic config.""" + + arbitrary_types_allowed = True + + def get_tools(self) -> List[BaseTool]: + """Get the tools in the toolkit.""" + return [ + SlackGetChannel(), + SlackGetMessage(), + SlackScheduleMessage(), + SlackSendMessage(), + ] diff --git a/libs/langchain/langchain/tools/__init__.py b/libs/langchain/langchain/tools/__init__.py index db0e271c6c2..19c91b10230 100644 --- a/libs/langchain/langchain/tools/__init__.py +++ b/libs/langchain/langchain/tools/__init__.py @@ -558,6 +558,30 @@ def _import_shell_tool() -> Any: return ShellTool +def _import_slack_get_channel() -> Any: + from langchain.tools.slack.get_channel import SlackGetChannel + + return SlackGetChannel + + +def _import_slack_get_message() -> Any: + from langchain.tools.slack.get_message import SlackGetMessage + + return SlackGetMessage + + +def _import_slack_schedule_message() -> Any: + from langchain.tools.slack.schedule_message import SlackScheduleMessage + + return SlackScheduleMessage + + +def _import_slack_send_message() -> Any: + from langchain.tools.slack.send_message import SlackSendMessage + + return SlackSendMessage + + def _import_sleep_tool() -> Any: from langchain.tools.sleep.tool import SleepTool @@ -871,6 +895,14 @@ def __getattr__(name: str) -> Any: return _import_searx_search_tool_SearxSearchRun() elif name == "ShellTool": return _import_shell_tool() + elif name == "SlackGetChannel": + return _import_slack_get_channel + elif name == "SlackGetMessage": + return _import_slack_get_message + elif name == "SlackScheduleMessage": + return _import_slack_schedule_message + elif name == "SlackSendMessage": + return _import_slack_send_message elif name == "SleepTool": return _import_sleep_tool() elif name == "BaseSparkSQLTool": @@ -1016,6 +1048,10 @@ __all__ = [ "SearxSearchResults", "SearxSearchRun", "ShellTool", + "SlackGetChannel", + "SlackGetMessage", + "SlackScheduleMessage", + "SlackSendMessage", "SleepTool", "StdInInquireTool", "StackExchangeTool", diff --git a/libs/langchain/langchain/tools/slack/__init__.py b/libs/langchain/langchain/tools/slack/__init__.py new file mode 100644 index 00000000000..66b4c77cbe8 --- /dev/null +++ b/libs/langchain/langchain/tools/slack/__init__.py @@ -0,0 +1,15 @@ +"""Slack tools.""" + +from langchain.tools.slack.get_channel import SlackGetChannel +from langchain.tools.slack.get_message import SlackGetMessage +from langchain.tools.slack.schedule_message import SlackScheduleMessage +from langchain.tools.slack.send_message import SlackSendMessage +from langchain.tools.slack.utils import login + +__all__ = [ + "SlackGetChannel", + "SlackGetMessage", + "SlackScheduleMessage", + "SlackSendMessage", + "login", +] diff --git a/libs/langchain/langchain/tools/slack/base.py b/libs/langchain/langchain/tools/slack/base.py new file mode 100644 index 00000000000..06e994debc5 --- /dev/null +++ b/libs/langchain/langchain/tools/slack/base.py @@ -0,0 +1,18 @@ +"""Base class for Slack tools.""" +from __future__ import annotations + +from typing import TYPE_CHECKING + +from langchain.pydantic_v1 import Field +from langchain.tools.base import BaseTool +from langchain.tools.slack.utils import login + +if TYPE_CHECKING: + from slack_sdk import WebClient + + +class SlackBaseTool(BaseTool): + """Base class for Slack tools.""" + + client: WebClient = Field(default_factory=login) + """The WebClient object.""" diff --git a/libs/langchain/langchain/tools/slack/get_channel.py b/libs/langchain/langchain/tools/slack/get_channel.py new file mode 100644 index 00000000000..e7445979591 --- /dev/null +++ b/libs/langchain/langchain/tools/slack/get_channel.py @@ -0,0 +1,33 @@ +import json +import logging +from typing import Optional + +from langchain.callbacks.manager import CallbackManagerForToolRun +from langchain.tools.slack.base import SlackBaseTool + + +class SlackGetChannel(SlackBaseTool): + name: str = "get_channelid_name_dict" + description: str = "Use this tool to get channelid-name dict." + + def _run( + self, + run_manager: Optional[CallbackManagerForToolRun] = None, + ) -> str: + try: + logging.getLogger(__name__) + + result = self.client.conversations_list() + channels = result["channels"] + filtered_result = [ + {key: channel[key] for key in ("id", "name", "created", "num_members")} + for channel in channels + if "id" in channel + and "name" in channel + and "created" in channel + and "num_members" in channel + ] + return json.dumps(filtered_result) + + except Exception as e: + return "Error creating conversation: {}".format(e) diff --git a/libs/langchain/langchain/tools/slack/get_message.py b/libs/langchain/langchain/tools/slack/get_message.py new file mode 100644 index 00000000000..5f882302607 --- /dev/null +++ b/libs/langchain/langchain/tools/slack/get_message.py @@ -0,0 +1,41 @@ +import json +import logging +from typing import Optional, Type + +from langchain.callbacks.manager import CallbackManagerForToolRun +from langchain.pydantic_v1 import BaseModel, Field +from langchain.tools.slack.base import SlackBaseTool + + +class SlackGetMessageSchema(BaseModel): + """Input schema for SlackGetMessages.""" + + channel_id: str = Field( + ..., + description="The channel id, private group, or IM channel to send message to.", + ) + + +class SlackGetMessage(SlackBaseTool): + name: str = "get_messages" + description: str = "Use this tool to get messages from a channel." + + args_schema: Type[SlackGetMessageSchema] = SlackGetMessageSchema + + def _run( + self, + channel_id: str, + run_manager: Optional[CallbackManagerForToolRun] = None, + ) -> str: + logging.getLogger(__name__) + try: + result = self.client.conversations_history(channel=channel_id) + messages = result["messages"] + filtered_messages = [ + {key: message[key] for key in ("user", "text", "ts")} + for message in messages + if "user" in message and "text" in message and "ts" in message + ] + return json.dumps(filtered_messages) + except Exception as e: + return "Error creating conversation: {}".format(e) diff --git a/libs/langchain/langchain/tools/slack/schedule_message.py b/libs/langchain/langchain/tools/slack/schedule_message.py new file mode 100644 index 00000000000..2916d5e5c7a --- /dev/null +++ b/libs/langchain/langchain/tools/slack/schedule_message.py @@ -0,0 +1,59 @@ +import logging +from datetime import datetime as dt +from typing import Optional, Type + +from langchain.callbacks.manager import CallbackManagerForToolRun +from langchain.pydantic_v1 import BaseModel, Field +from langchain.tools.slack.base import SlackBaseTool +from langchain.tools.slack.utils import UTC_FORMAT + +logger = logging.getLogger(__name__) + + +class ScheduleMessageSchema(BaseModel): + """Input for ScheduleMessageTool.""" + + message: str = Field( + ..., + description="The message to be sent.", + ) + channel: str = Field( + ..., + description="The channel, private group, or IM channel to send message to.", + ) + timestamp: str = Field( + ..., + description="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 3 " + " hours from Coordinated Universal Time (UTC).", + ) + + +class SlackScheduleMessage(SlackBaseTool): + """Tool for scheduling a message in Slack.""" + + name: str = "schedule_message" + description: str = ( + "Use this tool to schedule a message to be sent on a specific date and time." + ) + args_schema: Type[ScheduleMessageSchema] = ScheduleMessageSchema + + def _run( + self, + message: str, + channel: str, + timestamp: str, + run_manager: Optional[CallbackManagerForToolRun] = None, + ) -> str: + try: + unix_timestamp = dt.timestamp(dt.strptime(timestamp, UTC_FORMAT)) + result = self.client.chat_scheduleMessage( + channel=channel, text=message, post_at=unix_timestamp + ) + output = "Message scheduled: " + str(result) + return output + except Exception as e: + return "Error scheduling message: {}".format(e) diff --git a/libs/langchain/langchain/tools/slack/send_message.py b/libs/langchain/langchain/tools/slack/send_message.py new file mode 100644 index 00000000000..bb6d89964a6 --- /dev/null +++ b/libs/langchain/langchain/tools/slack/send_message.py @@ -0,0 +1,41 @@ +from typing import Optional, Type + +from langchain.callbacks.manager import CallbackManagerForToolRun +from langchain.pydantic_v1 import BaseModel, Field +from langchain.tools.slack.base import SlackBaseTool + + +class SendMessageSchema(BaseModel): + """Input for SendMessageTool.""" + + message: str = Field( + ..., + description="The message to be sent.", + ) + channel: str = Field( + ..., + description="The channel, private group, or IM channel to send message to.", + ) + + +class SlackSendMessage(SlackBaseTool): + """Tool for sending a message in Slack.""" + + name: str = "send_message" + description: str = ( + "Use this tool to send a message with the provided message fields." + ) + args_schema: Type[SendMessageSchema] = SendMessageSchema + + def _run( + self, + message: str, + channel: str, + run_manager: Optional[CallbackManagerForToolRun] = None, + ) -> str: + try: + result = self.client.chat_postMessage(channel=channel, text=message) + output = "Message sent: " + str(result) + return output + except Exception as e: + return "Error creating conversation: {}".format(e) diff --git a/libs/langchain/langchain/tools/slack/utils.py b/libs/langchain/langchain/tools/slack/utils.py new file mode 100644 index 00000000000..1d614f6e9b3 --- /dev/null +++ b/libs/langchain/langchain/tools/slack/utils.py @@ -0,0 +1,42 @@ +"""Slack tool utils.""" +from __future__ import annotations + +import logging +import os +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from slack_sdk import WebClient + +logger = logging.getLogger(__name__) + + +def login() -> WebClient: + """Authenticate using the Slack API.""" + try: + from slack_sdk import WebClient + except ImportError as e: + raise ImportError( + "Cannot import slack_sdk. Please install the package with \ + `pip install slack_sdk`." + ) from e + + if "SLACK_BOT_TOKEN" in os.environ: + token = os.environ["SLACK_BOT_TOKEN"] + client = WebClient(token=token) + logger.info("slack login success") + return client + elif "SLACK_USER_TOKEN" in os.environ: + token = os.environ["SLACK_USER_TOKEN"] + client = WebClient(token=token) + logger.info("slack login success") + return client + else: + logger.error( + "Error: The SLACK_BOT_TOKEN or SLACK_USER_TOKEN \ + environment variable have not been set." + ) + + +UTC_FORMAT = "%Y-%m-%dT%H:%M:%S%z" +"""UTC format for datetime objects.""" diff --git a/libs/langchain/tests/unit_tests/tools/test_imports.py b/libs/langchain/tests/unit_tests/tools/test_imports.py index 0f9869ce103..58bd210e9bb 100644 --- a/libs/langchain/tests/unit_tests/tools/test_imports.py +++ b/libs/langchain/tests/unit_tests/tools/test_imports.py @@ -94,6 +94,10 @@ EXPECTED_ALL = [ "SearxSearchResults", "SearxSearchRun", "ShellTool", + "SlackGetChannel", + "SlackGetMessage", + "SlackScheduleMessage", + "SlackSendMessage", "SleepTool", "StackExchangeTool", "StdInInquireTool", diff --git a/libs/langchain/tests/unit_tests/tools/test_public_api.py b/libs/langchain/tests/unit_tests/tools/test_public_api.py index 4bdc848033a..4db38fd13e1 100644 --- a/libs/langchain/tests/unit_tests/tools/test_public_api.py +++ b/libs/langchain/tests/unit_tests/tools/test_public_api.py @@ -96,6 +96,10 @@ _EXPECTED = [ "SearxSearchResults", "SearxSearchRun", "ShellTool", + "SlackGetChannel", + "SlackGetMessage", + "SlackScheduleMessage", + "SlackSendMessage", "SleepTool", "StdInInquireTool", "StackExchangeTool", diff --git a/libs/langchain/tests/unit_tests/tools/test_signatures.py b/libs/langchain/tests/unit_tests/tools/test_signatures.py index 4c1da32e5ea..2a2b0215e6e 100644 --- a/libs/langchain/tests/unit_tests/tools/test_signatures.py +++ b/libs/langchain/tests/unit_tests/tools/test_signatures.py @@ -12,6 +12,7 @@ from langchain.tools.base import BaseTool from langchain.tools.gmail.base import GmailBaseTool from langchain.tools.office365.base import O365BaseTool from langchain.tools.playwright.base import BaseBrowserTool +from langchain.tools.slack.base import SlackBaseTool def get_non_abstract_subclasses(cls: Type[BaseTool]) -> List[Type[BaseTool]]: @@ -20,6 +21,7 @@ def get_non_abstract_subclasses(cls: Type[BaseTool]) -> List[Type[BaseTool]]: BaseBrowserTool, GmailBaseTool, O365BaseTool, + SlackBaseTool, } # Abstract but not recognized subclasses = [] for subclass in cls.__subclasses__():