Added Slacktoolkit (#14012)

- **Description:** 
This PR introduces the Slack toolkit to LangChain, which allows users to
read and write to Slack using the Slack API. Specifically, we've added
the following tools.
1. get_channel: Provides a summary of all the channels in a workspace.
2. get_message: Gets the message history of a channel.
3. send_message: Sends a message to a channel.
4. schedule_message: Sends a message to a channel at a specific time and
date.

- **Issue:** This pull request addresses [Add Slack Toolkit
#11747](https://github.com/langchain-ai/langchain/issues/11747)
  - **Dependencies:** package`slack_sdk`
Note: For this toolkit to function you will need to add a Slack app to
your workspace. Additional info can be found
[here](https://slack.com/help/articles/202035138-Add-apps-to-your-Slack-workspace).

---------

Co-authored-by: Bagatur <baskaryan@gmail.com>
Co-authored-by: ArianneLavada <ariannelavada@gmail.com>
Co-authored-by: ArianneLavada <84357335+ArianneLavada@users.noreply.github.com>
Co-authored-by: ariannelavada@gmail.com <you@example.com>
This commit is contained in:
gzyJoy 2023-12-03 13:25:38 -05:00 committed by GitHub
parent 99e5ee6a84
commit 32d4bb4590
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 480 additions and 0 deletions

View File

@ -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
}

View File

@ -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",

View File

@ -0,0 +1 @@
"""Slack toolkit."""

View File

@ -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(),
]

View File

@ -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",

View File

@ -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",
]

View File

@ -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."""

View File

@ -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)

View File

@ -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)

View File

@ -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)

View File

@ -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)

View File

@ -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."""

View File

@ -94,6 +94,10 @@ EXPECTED_ALL = [
"SearxSearchResults",
"SearxSearchRun",
"ShellTool",
"SlackGetChannel",
"SlackGetMessage",
"SlackScheduleMessage",
"SlackSendMessage",
"SleepTool",
"StackExchangeTool",
"StdInInquireTool",

View File

@ -96,6 +96,10 @@ _EXPECTED = [
"SearxSearchResults",
"SearxSearchRun",
"ShellTool",
"SlackGetChannel",
"SlackGetMessage",
"SlackScheduleMessage",
"SlackSendMessage",
"SleepTool",
"StdInInquireTool",
"StackExchangeTool",

View File

@ -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__():