From e99c2cee5bb2b7bf87d36c9c007cd903468a43b6 Mon Sep 17 00:00:00 2001 From: EugeneP Date: Tue, 22 Oct 2024 20:54:12 +0200 Subject: [PATCH 01/18] TLK-1725 - Slack tool initial commit --- poetry.lock | 37 +++-- pyproject.toml | 1 + src/backend/config/settings.py | 23 +++ src/backend/config/tools.py | 20 +++ src/backend/tools/__init__.py | 3 + src/backend/tools/slack/__init__.py | 11 ++ src/backend/tools/slack/auth.py | 151 ++++++++++++++++++ src/backend/tools/slack/client.py | 10 ++ src/backend/tools/slack/constants.py | 2 + src/backend/tools/slack/tool.py | 52 ++++++ src/backend/tools/slack/utils.py | 78 +++++++++ .../src/app/(main)/settings/Settings.tsx | 60 +++++++ .../assistants_web/src/assets/icons/index.ts | 1 + .../assistants_web/src/components/UI/Icon.tsx | 7 + .../assistants_web/src/constants/tools.ts | 3 + 15 files changed, 443 insertions(+), 16 deletions(-) create mode 100644 src/backend/tools/slack/__init__.py create mode 100644 src/backend/tools/slack/auth.py create mode 100644 src/backend/tools/slack/client.py create mode 100644 src/backend/tools/slack/constants.py create mode 100644 src/backend/tools/slack/tool.py create mode 100644 src/backend/tools/slack/utils.py diff --git a/poetry.lock b/poetry.lock index 362a985c29..a71e5de694 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,4 +1,4 @@ -# This file is automatically @generated by Poetry 1.8.3 and should not be changed by hand. +# This file is automatically @generated by Poetry 1.8.2 and should not be changed by hand. [[package]] name = "aiohappyeyeballs" @@ -3870,43 +3870,31 @@ python-versions = ">=3.9" files = [ {file = "pandas-2.2.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:1948ddde24197a0f7add2bdc4ca83bf2b1ef84a1bc8ccffd95eda17fd836ecb5"}, {file = "pandas-2.2.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:381175499d3802cde0eabbaf6324cce0c4f5d52ca6f8c377c29ad442f50f6348"}, - {file = "pandas-2.2.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d9c45366def9a3dd85a6454c0e7908f2b3b8e9c138f5dc38fed7ce720d8453ed"}, {file = "pandas-2.2.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:86976a1c5b25ae3f8ccae3a5306e443569ee3c3faf444dfd0f41cda24667ad57"}, - {file = "pandas-2.2.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:b8661b0238a69d7aafe156b7fa86c44b881387509653fdf857bebc5e4008ad42"}, {file = "pandas-2.2.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:37e0aced3e8f539eccf2e099f65cdb9c8aa85109b0be6e93e2baff94264bdc6f"}, {file = "pandas-2.2.3-cp310-cp310-win_amd64.whl", hash = "sha256:56534ce0746a58afaf7942ba4863e0ef81c9c50d3f0ae93e9497d6a41a057645"}, {file = "pandas-2.2.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:66108071e1b935240e74525006034333f98bcdb87ea116de573a6a0dccb6c039"}, {file = "pandas-2.2.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:7c2875855b0ff77b2a64a0365e24455d9990730d6431b9e0ee18ad8acee13dbd"}, - {file = "pandas-2.2.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:cd8d0c3be0515c12fed0bdbae072551c8b54b7192c7b1fda0ba56059a0179698"}, {file = "pandas-2.2.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c124333816c3a9b03fbeef3a9f230ba9a737e9e5bb4060aa2107a86cc0a497fc"}, - {file = "pandas-2.2.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:63cc132e40a2e084cf01adf0775b15ac515ba905d7dcca47e9a251819c575ef3"}, {file = "pandas-2.2.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:29401dbfa9ad77319367d36940cd8a0b3a11aba16063e39632d98b0e931ddf32"}, {file = "pandas-2.2.3-cp311-cp311-win_amd64.whl", hash = "sha256:3fc6873a41186404dad67245896a6e440baacc92f5b716ccd1bc9ed2995ab2c5"}, {file = "pandas-2.2.3-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:b1d432e8d08679a40e2a6d8b2f9770a5c21793a6f9f47fdd52c5ce1948a5a8a9"}, {file = "pandas-2.2.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:a5a1595fe639f5988ba6a8e5bc9649af3baf26df3998a0abe56c02609392e0a4"}, - {file = "pandas-2.2.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:5de54125a92bb4d1c051c0659e6fcb75256bf799a732a87184e5ea503965bce3"}, {file = "pandas-2.2.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fffb8ae78d8af97f849404f21411c95062db1496aeb3e56f146f0355c9989319"}, - {file = "pandas-2.2.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:6dfcb5ee8d4d50c06a51c2fffa6cff6272098ad6540aed1a76d15fb9318194d8"}, {file = "pandas-2.2.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:062309c1b9ea12a50e8ce661145c6aab431b1e99530d3cd60640e255778bd43a"}, {file = "pandas-2.2.3-cp312-cp312-win_amd64.whl", hash = "sha256:59ef3764d0fe818125a5097d2ae867ca3fa64df032331b7e0917cf5d7bf66b13"}, {file = "pandas-2.2.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f00d1345d84d8c86a63e476bb4955e46458b304b9575dcf71102b5c705320015"}, {file = "pandas-2.2.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:3508d914817e153ad359d7e069d752cdd736a247c322d932eb89e6bc84217f28"}, - {file = "pandas-2.2.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:22a9d949bfc9a502d320aa04e5d02feab689d61da4e7764b62c30b991c42c5f0"}, {file = "pandas-2.2.3-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f3a255b2c19987fbbe62a9dfd6cff7ff2aa9ccab3fc75218fd4b7530f01efa24"}, - {file = "pandas-2.2.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:800250ecdadb6d9c78eae4990da62743b857b470883fa27f652db8bdde7f6659"}, {file = "pandas-2.2.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6374c452ff3ec675a8f46fd9ab25c4ad0ba590b71cf0656f8b6daa5202bca3fb"}, {file = "pandas-2.2.3-cp313-cp313-win_amd64.whl", hash = "sha256:61c5ad4043f791b61dd4752191d9f07f0ae412515d59ba8f005832a532f8736d"}, {file = "pandas-2.2.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:3b71f27954685ee685317063bf13c7709a7ba74fc996b84fc6821c59b0f06468"}, {file = "pandas-2.2.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:38cf8125c40dae9d5acc10fa66af8ea6fdf760b2714ee482ca691fc66e6fcb18"}, - {file = "pandas-2.2.3-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:ba96630bc17c875161df3818780af30e43be9b166ce51c9a18c1feae342906c2"}, {file = "pandas-2.2.3-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1db71525a1538b30142094edb9adc10be3f3e176748cd7acc2240c2f2e5aa3a4"}, - {file = "pandas-2.2.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:15c0e1e02e93116177d29ff83e8b1619c93ddc9c49083f237d4312337a61165d"}, {file = "pandas-2.2.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:ad5b65698ab28ed8d7f18790a0dc58005c7629f227be9ecc1072aa74c0c1d43a"}, {file = "pandas-2.2.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:bc6b93f9b966093cb0fd62ff1a7e4c09e6d546ad7c1de191767baffc57628f39"}, {file = "pandas-2.2.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:5dbca4c1acd72e8eeef4753eeca07de9b1db4f398669d5994086f788a5d7cc30"}, - {file = "pandas-2.2.3-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:8cd6d7cc958a3910f934ea8dbdf17b2364827bb4dafc38ce6eef6bb3d65ff09c"}, {file = "pandas-2.2.3-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:99df71520d25fade9db7c1076ac94eb994f4d2673ef2aa2e86ee039b6746d20c"}, - {file = "pandas-2.2.3-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:31d0ced62d4ea3e231a9f228366919a5ea0b07440d9d4dac345376fd8e1477ea"}, {file = "pandas-2.2.3-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:7eee9e7cea6adf3e3d24e304ac6b8300646e2a5d1cd3a3c2abed9101b0846761"}, {file = "pandas-2.2.3-cp39-cp39-win_amd64.whl", hash = "sha256:4850ba03528b6dd51d6c5d273c46f183f39a9baf3f0143e566b89450965b105e"}, {file = "pandas-2.2.3.tar.gz", hash = "sha256:4f18ba62b61d7e192368b84517265a99b4d7ee8912f8708660fb4a366cc82667"}, @@ -4174,8 +4162,6 @@ files = [ {file = "psycopg2-2.9.9-cp310-cp310-win_amd64.whl", hash = "sha256:426f9f29bde126913a20a96ff8ce7d73fd8a216cfb323b1f04da402d452853c3"}, {file = "psycopg2-2.9.9-cp311-cp311-win32.whl", hash = "sha256:ade01303ccf7ae12c356a5e10911c9e1c51136003a9a1d92f7aa9d010fb98372"}, {file = "psycopg2-2.9.9-cp311-cp311-win_amd64.whl", hash = "sha256:121081ea2e76729acfb0673ff33755e8703d45e926e416cb59bae3a86c6a4981"}, - {file = "psycopg2-2.9.9-cp312-cp312-win32.whl", hash = "sha256:d735786acc7dd25815e89cc4ad529a43af779db2e25aa7c626de864127e5a024"}, - {file = "psycopg2-2.9.9-cp312-cp312-win_amd64.whl", hash = "sha256:a7653d00b732afb6fc597e29c50ad28087dcb4fbfb28e86092277a559ae4e693"}, {file = "psycopg2-2.9.9-cp37-cp37m-win32.whl", hash = "sha256:5e0d98cade4f0e0304d7d6f25bbfbc5bd186e07b38eac65379309c4ca3193efa"}, {file = "psycopg2-2.9.9-cp37-cp37m-win_amd64.whl", hash = "sha256:7e2dacf8b009a1c1e843b5213a87f7c544b2b042476ed7755be813eaf4e8347a"}, {file = "psycopg2-2.9.9-cp38-cp38-win32.whl", hash = "sha256:ff432630e510709564c01dafdbe996cb552e0b9f3f065eb89bdce5bd31fabf4c"}, @@ -5423,6 +5409,20 @@ files = [ {file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"}, ] +[[package]] +name = "slack-sdk" +version = "3.33.1" +description = "The Slack API Platform SDK for Python" +optional = false +python-versions = ">=3.6" +files = [ + {file = "slack_sdk-3.33.1-py2.py3-none-any.whl", hash = "sha256:ef93beec3ce9c8f64da02fd487598a05ec4bc9c92ceed58f122dbe632691cbe2"}, + {file = "slack_sdk-3.33.1.tar.gz", hash = "sha256:e328bb661d95db5f66b993b1d64288ac7c72201a745b4c7cf8848dafb7b74e40"}, +] + +[package.extras] +optional = ["SQLAlchemy (>=1.4,<3)", "aiodns (>1.0)", "aiohttp (>=3.7.3,<4)", "boto3 (<=2)", "websocket-client (>=1,<2)", "websockets (>=9.1,<14)"] + [[package]] name = "sniffio" version = "1.3.1" @@ -5967,6 +5967,11 @@ files = [ {file = "triton-3.0.0-1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:34e509deb77f1c067d8640725ef00c5cbfcb2052a1a3cb6a6d343841f92624eb"}, {file = "triton-3.0.0-1-cp38-cp38-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:bcbf3b1c48af6a28011a5c40a5b3b9b5330530c3827716b5fbf6d7adcc1e53e9"}, {file = "triton-3.0.0-1-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:6e5727202f7078c56f91ff13ad0c1abab14a0e7f2c87e91b12b6f64f3e8ae609"}, + {file = "triton-3.0.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:39b052da883351fdf6be3d93cedae6db3b8e3988d3b09ed221bccecfa9612230"}, + {file = "triton-3.0.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cd34f19a8582af96e6291d4afce25dac08cb2a5d218c599163761e8e0827208e"}, + {file = "triton-3.0.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0d5e10de8c011adeb7c878c6ce0dd6073b14367749e34467f1cff2bde1b78253"}, + {file = "triton-3.0.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e8903767951bf86ec960b4fe4e21bc970055afc65e9d57e916d79ae3c93665e3"}, + {file = "triton-3.0.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:41004fb1ae9a53fcb3e970745feb87f0e3c94c6ce1ba86e95fa3b8537894bef7"}, ] [package.dependencies] @@ -6701,4 +6706,4 @@ type = ["pytest-mypy"] [metadata] lock-version = "2.0" python-versions = "~3.11" -content-hash = "077321dcaebc0346ae1669450ad6415aaa9b8c117e7f7154ed412eb127d75ecc" +content-hash = "aa4c1c16e4f7f2cca6e6a2820f4d8ba1dd12f692ec3f61ac91d6f0834a9ee988" diff --git a/pyproject.toml b/pyproject.toml index 10bea3ea2e..0a7e3e83d7 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -57,6 +57,7 @@ llama-index = "^0.11.10" llama-index-llms-cohere = "^0.3.0" llama-index-embeddings-cohere = "^0.2.1" google-cloud-texttospeech = "^2.18.0" +slack-sdk = "^3.33.1" [tool.poetry.group.dev] optional = true diff --git a/src/backend/config/settings.py b/src/backend/config/settings.py index 4e32b6bb91..f709ddb85d 100644 --- a/src/backend/config/settings.py +++ b/src/backend/config/settings.py @@ -146,6 +146,24 @@ class GDriveSettings(BaseSettings, BaseModel): ) +class SlackSettings(BaseSettings, BaseModel): + model_config = SETTINGS_CONFIG + client_id: Optional[str] = Field( + default=None, + validation_alias=AliasChoices("SLACK_CLIENT_ID", "client_id"), + ) + client_secret: Optional[str] = Field( + default=None, + validation_alias=AliasChoices("SLACK_CLIENT_SECRET", "client_secret"), + ) + user_scopes: Optional[str] = Field( + default=None, + validation_alias=AliasChoices( + "SLACK_USER_SCOPES", "scopes" + ), + ) + + class TavilyWebSearchSettings(BaseSettings, BaseModel): model_config = SETTINGS_CONFIG api_key: Optional[str] = Field( @@ -200,6 +218,11 @@ class ToolSettings(BaseSettings, BaseModel): default=HybridWebSearchSettings() ) + slack: Optional[SlackSettings] = Field( + default=SlackSettings() + ) + + class DatabaseSettings(BaseSettings, BaseModel): model_config = SETTINGS_CONFIG diff --git a/src/backend/config/tools.py b/src/backend/config/tools.py index 8a6e47b505..eef89f556b 100644 --- a/src/backend/config/tools.py +++ b/src/backend/config/tools.py @@ -14,6 +14,8 @@ PythonInterpreter, ReadFileTool, SearchFileTool, + SlackAuth, + SlackTool, TavilyWebSearch, WebScrapeTool, ) @@ -43,6 +45,7 @@ class ToolName(StrEnum): Google_Web_Search = GoogleWebSearch.NAME Brave_Web_Search = BraveWebSearch.NAME Hybrid_Web_Search = HybridWebSearch.NAME + Slack = SlackTool.NAME ALL_TOOLS = { @@ -239,6 +242,23 @@ class ToolName(StrEnum): category=Category.WebSearch, description="Returns a list of relevant document snippets for a textual query retrieved from the internet using a mix of any existing Web Search tools.", ), + ToolName.Slack: ManagedTool( + display_name="Slack", + implementation=SlackTool, + parameter_definitions={ + "query": { + "description": "Query to search slack.", + "type": "str", + "required": True, + } + }, + is_visible=True, + is_available=SlackTool.is_available(), + auth_implementation=SlackAuth, + error_message="SlackTool not available, please enable it in the SlackTool class.", + category=Category.DataLoader, + description="Returns a list of relevant document snippets from slack.", + ), } diff --git a/src/backend/tools/__init__.py b/src/backend/tools/__init__.py index 2546907601..e2e5fb83bf 100644 --- a/src/backend/tools/__init__.py +++ b/src/backend/tools/__init__.py @@ -6,6 +6,7 @@ from backend.tools.hybrid_search import HybridWebSearch from backend.tools.lang_chain import LangChainVectorDBRetriever, LangChainWikiRetriever from backend.tools.python_interpreter import PythonInterpreter +from backend.tools.slack import SlackAuth, SlackTool from backend.tools.tavily_search import TavilyWebSearch from backend.tools.web_scrape import WebScrapeTool @@ -23,4 +24,6 @@ "BraveWebSearch", "GoogleWebSearch", "HybridWebSearch", + "SlackTool", + "SlackAuth" ] diff --git a/src/backend/tools/slack/__init__.py b/src/backend/tools/slack/__init__.py new file mode 100644 index 0000000000..bd770a3a9f --- /dev/null +++ b/src/backend/tools/slack/__init__.py @@ -0,0 +1,11 @@ +from backend.tools.slack.auth import SlackAuth +from backend.tools.slack.constants import ( + SLACK_TOOL_ID, +) +from backend.tools.slack.tool import SlackTool + +__all__ = [ + "SlackAuth", + "SlackTool", + "SLACK_TOOL_ID", +] diff --git a/src/backend/tools/slack/auth.py b/src/backend/tools/slack/auth.py new file mode 100644 index 0000000000..98c2139411 --- /dev/null +++ b/src/backend/tools/slack/auth.py @@ -0,0 +1,151 @@ +import datetime +import json +import urllib.parse + +import requests +from fastapi import Request + +from backend.config.settings import Settings +from backend.crud import tool_auth as tool_auth_crud +from backend.database_models import ToolAuth +from backend.database_models.database import DBSessionDep +from backend.database_models.tool_auth import ToolAuth as ToolAuthModel +from backend.schemas.tool_auth import UpdateToolAuth +from backend.services.auth.crypto import encrypt +from backend.services.logger.utils import LoggerFactory +from backend.tools.base import BaseToolAuthentication +from backend.tools.slack.constants import SLACK_TOOL_ID +from backend.tools.utils.mixins import ToolAuthenticationCacheMixin + +logger = LoggerFactory().get_logger() + + +class SlackAuth(BaseToolAuthentication, ToolAuthenticationCacheMixin): + TOOL_ID = SLACK_TOOL_ID + AUTH_ENDPOINT = "https://slack.com/oauth/v2/authorize" + TOKEN_ENDPOINT = "https://slack.com/api/oauth.v2.access" + DEFAULT_BOT_SCOPES = ['search:read.public'] + DEFAULT_USER_SCOPES = ['search:read'] + + def __init__(self): + super().__init__() + self.SLACK_CLIENT_ID = Settings().tools.slack.client_id + self.SLACK_CLIENT_SECRET = Settings().tools.slack.client_secret + self.USER_SCOPES = Settings().tools.slack.user_scopes or self.DEFAULT_USER_SCOPES + self.REDIRECT_URL = f"{self.BACKEND_HOST}/v1/tool/auth" + + if ( + self.SLACK_CLIENT_ID is None + or self.SLACK_CLIENT_SECRET is None + ): + raise ValueError( + "SLACK_CLIENT_ID and SLACK_CLIENT_SECRET must be set to use Slack Tool Auth." + ) + + def get_auth_url(self, user_id: str) -> str: + key = self.insert_tool_auth_cache(user_id, self.TOOL_ID) + state = {"key": key} + + params = { + "client_id": self.SLACK_CLIENT_ID, + "user_scope": " ".join(self.USER_SCOPES), + "redirect_uri": self.REDIRECT_URL, + "state": json.dumps(state), + } + + return f"{self.AUTH_ENDPOINT}?{urllib.parse.urlencode(params)}" + + def retrieve_auth_token( + self, request: Request, session: DBSessionDep, user_id: str + ) -> str: + if request.query_params.get("error"): + error = request.query_params.get("error") + logger.error(event=f"[Slack Tool] Auth token error: {error}.") + return error + + body = { + "code": request.query_params.get("code"), + "client_id": self.SLACK_CLIENT_ID, + "client_secret": self.SLACK_CLIENT_SECRET, + } + + url_encoded_body = urllib.parse.urlencode(body) + headers = { + "Content-Type": 'application/x-www-form-urlencoded', + } + response = requests.post(self.TOKEN_ENDPOINT, data=url_encoded_body, headers=headers) + + response_body = response.json() + + if response.status_code != 200 or response_body.get("ok") is False: + logger.error( + event=f"[Slack Tool] Error retrieving auth token: {response_body}" + ) + return response + + token_data = response_body.get("authed_user", None) + if token_data is None: + logger.error( + event=f"[Slack Tool] Error retrieving auth token: {response_body}" + ) + return response + + tool_auth_crud.create_tool_auth( + session, + ToolAuthModel( + user_id=user_id, + tool_id=self.TOOL_ID, + token_type=token_data.get("token_type"), + encrypted_access_token=encrypt(token_data.get("access_token")), + encrypted_refresh_token=encrypt(token_data.get("refresh_token")), + expires_at=datetime.datetime.now() + + datetime.timedelta(seconds=token_data.get("expires_in")), + ), + ) + + def try_refresh_token(self, session: DBSessionDep, user_id: str, tool_auth: ToolAuth) -> bool: + body = { + "client_id": self.SLACK_CLIENT_ID, + "client_secret": self.SLACK_CLIENT_SECRET, + "refresh_token": tool_auth.refresh_token, + "grant_type": "refresh_token", + } + headers = { + "Content-Type": 'application/x-www-form-urlencoded', + } + url_encoded_body = urllib.parse.urlencode(body) + + response = requests.post(self.TOKEN_ENDPOINT, data=url_encoded_body, headers=headers) + response_body = response.json() + + if response.status_code != 200 or response_body.get("ok") is False: + logger.error( + event=f"[Slack Tool] Error refreshing token: {response_body}" + ) + return False + + token_data = response_body.get("authed_user", None) + if token_data is None: + logger.error( + event=f"[Slack Tool] Error retrieving auth token: {response_body}" + ) + return False + + existing_tool_auth = tool_auth_crud.get_tool_auth( + session, self.TOOL_ID, user_id + ) + tool_auth_crud.update_tool_auth( + session, + existing_tool_auth, + UpdateToolAuth( + user_id=user_id, + tool_id=self.TOOL_ID, + token_type=token_data.get("token_type"), + encrypted_access_token=encrypt(token_data.get("access_token")), + encrypted_refresh_token=encrypt(token_data.get("refresh_token")), + expires_at=datetime.datetime.now() + + datetime.timedelta(seconds=token_data.get("expires_in")), + ), + ) + + return True diff --git a/src/backend/tools/slack/client.py b/src/backend/tools/slack/client.py new file mode 100644 index 0000000000..28575b9b62 --- /dev/null +++ b/src/backend/tools/slack/client.py @@ -0,0 +1,10 @@ +from slack_sdk import WebClient + + +class SlackClient: + def __init__(self, auth_token, search_limit=20): + self.client = WebClient(token=auth_token) + self.search_limit = search_limit + + def search_all(self, query): + return self.client.search_all(query=query, count=self.search_limit) diff --git a/src/backend/tools/slack/constants.py b/src/backend/tools/slack/constants.py new file mode 100644 index 0000000000..cbd92faec2 --- /dev/null +++ b/src/backend/tools/slack/constants.py @@ -0,0 +1,2 @@ +SEARCH_LIMIT = 10 +SLACK_TOOL_ID = "slack" diff --git a/src/backend/tools/slack/tool.py b/src/backend/tools/slack/tool.py new file mode 100644 index 0000000000..10fc4763fd --- /dev/null +++ b/src/backend/tools/slack/tool.py @@ -0,0 +1,52 @@ +from typing import Any, Dict, List + +from google.auth.exceptions import RefreshError + +from backend.config.settings import Settings +from backend.crud import tool_auth as tool_auth_crud +from backend.services.logger.utils import LoggerFactory +from backend.tools.base import BaseTool +from backend.tools.slack.constants import SEARCH_LIMIT, SLACK_TOOL_ID +from backend.tools.slack.utils import get_slack_service + +logger = LoggerFactory().get_logger() + + +class SlackTool(BaseTool): + """ + Tool that searches Slack + """ + + NAME = SLACK_TOOL_ID + + CLIENT_ID = Settings().tools.slack.client_id + CLIENT_SECRET = Settings().tools.slack.client_secret + + @classmethod + def is_available(cls) -> bool: + return cls.CLIENT_ID is not None and cls.CLIENT_SECRET is not None + + def _handle_tool_specific_errors(self, error: Exception, **kwargs: Any): + message = "[Slack] Tool Error: {}".format(str(error)) + + if isinstance(error, RefreshError): + session = kwargs["session"] + user_id = kwargs["user_id"] + tool_auth_crud.delete_tool_auth( + db=session, user_id=user_id, tool_id=SLACK_TOOL_ID + ) + + logger.error( + event="[Slack] Auth token error: Please refresh the page and re-authenticate." + ) + raise Exception(message) + + async def call(self, parameters: dict, **kwargs: Any) -> List[Dict[str, Any]]: + user_id = kwargs.get("user_id") + query = parameters.get("query", "") + + # Search Slack + slack_service = get_slack_service(user_id=user_id, search_limit=SEARCH_LIMIT) + all_results = slack_service.search_all(query=query) + return slack_service.serialize_results(all_results) + diff --git a/src/backend/tools/slack/utils.py b/src/backend/tools/slack/utils.py new file mode 100644 index 0000000000..0ba14a91f6 --- /dev/null +++ b/src/backend/tools/slack/utils.py @@ -0,0 +1,78 @@ + +from backend.database_models.database import get_session +from backend.services.logger.utils import LoggerFactory +from backend.tools.slack import SlackAuth +from backend.tools.base import ToolAuthException +from backend.tools.slack.client import SlackClient +from backend.tools.slack.constants import SEARCH_LIMIT, SLACK_TOOL_ID + +logger = LoggerFactory().get_logger() + + +class SlackService: + def __init__(self, user_id: str, auth_token: str, search_limit=SEARCH_LIMIT): + self.user_id = user_id + self.auth_token = auth_token + self.client = SlackClient(auth_token=auth_token, search_limit=search_limit) + + def search_all(self, query: str): + return self.client.search_all(query=query) + + def serialize_results(self, response): + results = [] + for match in response["messages"]["matches"]: + document = self.extract_message_data(match) + results.append(document) + for match in response["files"]["matches"]: + document = self.extract_files_data(match) + results.append(document) + + return results + + @staticmethod + def extract_message_data(message_json): + document = {} + document["type"] = "message" + if "text" in message_json: + document["text"] = str(message_json.pop("text")) + if "permalink" in message_json: + document["url"] = str(message_json.pop("permalink")) + if "channel" in message_json and "name" in message_json["channel"]: + document["title"] = str(message_json["channel"]["name"]) + + return document + + @staticmethod + def extract_files_data(message_json): + document = {} + document["type"] = "file" + if "permalink" in message_json: + document["url"] = str(message_json.pop("permalink")) + if "title" in message_json: + document["title"] = str(message_json["title"]) + document["text"] = str(message_json["title"]) + + return document + + +def get_slack_service(user_id: str, search_limit=SEARCH_LIMIT) -> SlackService: + slack_auth = SlackAuth() + auth_token = None + session = next(get_session()) + if slack_auth.is_auth_required(session, user_id=user_id): + session.close() + raise ToolAuthException( + "Slack Tool auth Error: Agent creator credentials need to re-authenticate", + SLACK_TOOL_ID, + ) + + auth_token = slack_auth.get_token(session=session, user_id=user_id) + if auth_token is None: + session.close() + raise Exception("Slack Tool Error: No credentials found") + + service = SlackService(user_id=user_id, auth_token=auth_token, search_limit=search_limit) + session.close() + return service + + diff --git a/src/interfaces/assistants_web/src/app/(main)/settings/Settings.tsx b/src/interfaces/assistants_web/src/app/(main)/settings/Settings.tsx index d268ba0d27..156164b1d6 100644 --- a/src/interfaces/assistants_web/src/app/(main)/settings/Settings.tsx +++ b/src/interfaces/assistants_web/src/app/(main)/settings/Settings.tsx @@ -76,6 +76,7 @@ const Connections = () => (
+
); @@ -186,6 +187,65 @@ const GoogleDriveConnection = () => { ); }; +const SlackConnection = () => { + const { data } = useListTools(); + const { mutateAsync: deleteAuthTool } = useDeleteAuthTool(); + const notify = useNotify(); + const slackTool = data?.find((tool) => tool.name === 'slack'); + + if (!slackTool) { + return null; + } + + const handleDeleteAuthTool = async () => { + try { + await deleteAuthTool(slackTool.name!); + } catch (e) { + notify.error('Failed to delete Slack connection'); + } + }; + + const isSlackConnected = !slackTool.is_auth_required ?? false; + const authUrl = getToolAuthUrl(slackTool.auth_url); + + return ( +
+
+
+ + Slack +
+ +
+ + Connect to Slack + +
+ {isSlackConnected ? ( +
+
+
+
+ ) : ( +
+
+ ); +}; const StatusConnection: React.FC<{ connected: boolean }> = ({ connected }) => { const label = connected ? 'Connected' : 'Disconnected'; diff --git a/src/interfaces/assistants_web/src/assets/icons/index.ts b/src/interfaces/assistants_web/src/assets/icons/index.ts index e285d82e8f..1bb48b3dc5 100644 --- a/src/interfaces/assistants_web/src/assets/icons/index.ts +++ b/src/interfaces/assistants_web/src/assets/icons/index.ts @@ -58,3 +58,4 @@ export * from './UsersThree'; export * from './Volume'; export * from './Warning'; export * from './Web'; +export * from './Slack'; diff --git a/src/interfaces/assistants_web/src/components/UI/Icon.tsx b/src/interfaces/assistants_web/src/components/UI/Icon.tsx index 7e79d9fcfe..44ad90c455 100644 --- a/src/interfaces/assistants_web/src/components/UI/Icon.tsx +++ b/src/interfaces/assistants_web/src/components/UI/Icon.tsx @@ -60,6 +60,7 @@ import { Volume, Warning, Web, + Slack, } from '@/assets/icons'; import { cn } from '@/utils'; @@ -124,6 +125,7 @@ export const IconList = [ 'volume', 'warning', 'web', + 'slack', ] as const; export type IconName = (typeof IconList)[number]; @@ -468,5 +470,10 @@ const getIcon = (name: IconName, kind: IconKind): React.ReactNode => { ), + ['slack']: ( + + + + ), }[name]; }; diff --git a/src/interfaces/assistants_web/src/constants/tools.ts b/src/interfaces/assistants_web/src/constants/tools.ts index 8454fd0258..b325b73234 100644 --- a/src/interfaces/assistants_web/src/constants/tools.ts +++ b/src/interfaces/assistants_web/src/constants/tools.ts @@ -12,11 +12,13 @@ export const TOOL_WIKIPEDIA_ID = 'wikipedia'; export const TOOL_CALCULATOR_ID = 'toolkit_calculator'; export const TOOL_WEB_SCRAPE_ID = 'web_scrape'; export const TOOL_GOOGLE_DRIVE_ID = 'google_drive'; +export const TOOL_SLACK_ID = 'slack'; export const FILE_UPLOAD_TOOLS = [TOOL_SEARCH_FILE_ID, TOOL_READ_DOCUMENT_ID]; export const AGENT_SETTINGS_TOOLS = [ TOOL_HYBRID_WEB_SEARCH_ID, TOOL_PYTHON_INTERPRETER_ID, TOOL_WEB_SCRAPE_ID, + TOOL_SLACK_ID, ]; export const TOOL_FALLBACK_ICON = 'circles-four'; @@ -30,4 +32,5 @@ export const TOOL_ID_TO_DISPLAY_INFO: { [id: string]: { icon: IconName } } = { [TOOL_SEARCH_FILE_ID]: { icon: 'search' }, [TOOL_GOOGLE_DRIVE_ID]: { icon: 'google-drive' }, [TOOL_READ_DOCUMENT_ID]: { icon: 'desktop' }, + [TOOL_SLACK_ID]: { icon: 'slack' }, }; From 84967713754b0f52de551b5adc26183809730244 Mon Sep 17 00:00:00 2001 From: EugeneP Date: Wed, 23 Oct 2024 20:29:37 +0200 Subject: [PATCH 02/18] TLK-1725 - Slack tool --- src/backend/tools/base.py | 4 +- .../src/app/(main)/settings/Settings.tsx | 17 +------ .../AgentSettingsForm/StatusConnection.tsx | 17 +++++++ .../AgentSettingsForm/ToolsStep.tsx | 50 +++++++++++++------ .../components/AgentSettingsForm/index.tsx | 19 +++++++ .../assistants_web/src/constants/tools.ts | 2 + 6 files changed, 75 insertions(+), 34 deletions(-) create mode 100644 src/interfaces/assistants_web/src/components/AgentSettingsForm/StatusConnection.tsx diff --git a/src/backend/tools/base.py b/src/backend/tools/base.py index f3999c7471..f396360678 100644 --- a/src/backend/tools/base.py +++ b/src/backend/tools/base.py @@ -91,7 +91,7 @@ def is_auth_required(self, session: DBSessionDep, user_id: str) -> bool: return False # Refresh failed, delete existing Auth - tool_auth_crud.delete_tool_auth(session, self.TOOL_ID, user_id) + tool_auth_crud.delete_tool_auth(session, user_id, self.TOOL_ID) return True # Check access_token is retrievable @@ -100,7 +100,7 @@ def is_auth_required(self, session: DBSessionDep, user_id: str) -> bool: auth.refresh_token except Exception(): # Retrieval failed, delete existing Auth - tool_auth_crud.delete_tool_auth(session, self.TOOL_ID, user_id) + tool_auth_crud.delete_tool_auth(session, user_id, self.TOOL_ID) return True # ToolAuth retrieved and is not expired diff --git a/src/interfaces/assistants_web/src/app/(main)/settings/Settings.tsx b/src/interfaces/assistants_web/src/app/(main)/settings/Settings.tsx index 156164b1d6..638a369ea7 100644 --- a/src/interfaces/assistants_web/src/app/(main)/settings/Settings.tsx +++ b/src/interfaces/assistants_web/src/app/(main)/settings/Settings.tsx @@ -6,6 +6,7 @@ import { MobileHeader } from '@/components/Global'; import { Button, DarkModeToggle, Icon, ShowStepsToggle, Tabs, Text } from '@/components/UI'; import { useDeleteAuthTool, useListTools, useNotify } from '@/hooks'; import { cn, getToolAuthUrl } from '@/utils'; +import {StatusConnection} from "@/components/AgentSettingsForm/StatusConnection"; const tabs = [
@@ -206,7 +207,6 @@ const SlackConnection = () => { }; const isSlackConnected = !slackTool.is_auth_required ?? false; - const authUrl = getToolAuthUrl(slackTool.auth_url); return (
@@ -246,18 +246,3 @@ const SlackConnection = () => {
); }; - -const StatusConnection: React.FC<{ connected: boolean }> = ({ connected }) => { - const label = connected ? 'Connected' : 'Disconnected'; - return ( - - - {label} - - ); -}; diff --git a/src/interfaces/assistants_web/src/components/AgentSettingsForm/StatusConnection.tsx b/src/interfaces/assistants_web/src/components/AgentSettingsForm/StatusConnection.tsx new file mode 100644 index 0000000000..df5f912f41 --- /dev/null +++ b/src/interfaces/assistants_web/src/components/AgentSettingsForm/StatusConnection.tsx @@ -0,0 +1,17 @@ +import {Text} from "@/components/UI"; +import {cn} from "@/utils"; + +export const StatusConnection: React.FC<{ connected: boolean }> = ({ connected }) => { + const label = connected ? 'Connected' : 'Disconnected'; + return ( + + + {label} + + ); +}; diff --git a/src/interfaces/assistants_web/src/components/AgentSettingsForm/ToolsStep.tsx b/src/interfaces/assistants_web/src/components/AgentSettingsForm/ToolsStep.tsx index 1f65fd1c0a..a3fb38a40e 100644 --- a/src/interfaces/assistants_web/src/components/AgentSettingsForm/ToolsStep.tsx +++ b/src/interfaces/assistants_web/src/components/AgentSettingsForm/ToolsStep.tsx @@ -1,19 +1,23 @@ import Link from 'next/link'; import { ManagedTool } from '@/cohere-client'; -import { Icon, IconName, Switch, Text } from '@/components/UI'; +import { Icon, IconName, Switch, Text, Button } from '@/components/UI'; import { AGENT_SETTINGS_TOOLS, TOOL_FALLBACK_ICON, TOOL_ID_TO_DISPLAY_INFO } from '@/constants'; +import {StatusConnection} from "@/components/AgentSettingsForm/StatusConnection"; + type Props = { tools?: ManagedTool[]; activeTools?: string[]; setActiveTools: (tools: string[]) => void; + handleAuthButtonClick: (toolName: string) => void; }; -export const ToolsStep: React.FC = ({ tools, activeTools, setActiveTools }) => { +export const ToolsStep: React.FC = ({ tools, activeTools, setActiveTools, handleAuthButtonClick }) => { const availableTools = tools?.filter( (tool) => tool.name && AGENT_SETTINGS_TOOLS.includes(tool.name) ); + const toolsAuthRequired = tools?.filter((tool) => tool.is_auth_required && tool.auth_url); const handleUpdateActiveTools = (checked: boolean, name: string) => { if (checked) { @@ -25,19 +29,20 @@ export const ToolsStep: React.FC = ({ tools, activeTools, setActiveTools return (
- {availableTools?.map( - ({ name, description }) => - !!name && - description && ( - handleUpdateActiveTools(checked, name)} - /> - ) + {availableTools?.map(({name, description, is_auth_required, auth_url}) => + !!name && description && ( + handleUpdateActiveTools(checked, name)} + isAuthRequired={is_auth_required} + authUrl={auth_url} + handleAuthButtonClick={handleAuthButtonClick} + /> + ) )} Don‘t see the tool you need? {/* TODO: get tool request link from Elaine */} @@ -55,7 +60,10 @@ const ToolRow: React.FC<{ icon: IconName; checked: boolean; handleSwitch: (checked: boolean) => void; -}> = ({ name, description, icon, checked, handleSwitch }) => { + isAuthRequired?: boolean; + authUrl?: string; + handleAuthButtonClick?: (toolName: string) => void; +}> = ({ name, description, icon, checked, handleSwitch, isAuthRequired, authUrl, handleAuthButtonClick}) => { return (
@@ -67,13 +75,23 @@ const ToolRow: React.FC<{ {name}
+
!!name && handleSwitch(checked)} showCheckedState /> +
{description} + {!isAuthRequired && !!authUrl && ()} + {isAuthRequired && !!authUrl && ( +
); }; diff --git a/src/interfaces/assistants_web/src/components/AgentSettingsForm/index.tsx b/src/interfaces/assistants_web/src/components/AgentSettingsForm/index.tsx index a79999fa3a..243a1fe370 100644 --- a/src/interfaces/assistants_web/src/components/AgentSettingsForm/index.tsx +++ b/src/interfaces/assistants_web/src/components/AgentSettingsForm/index.tsx @@ -177,6 +177,24 @@ export const AgentSettingsForm: React.FC = (props) => { } }; + const handleAuthButtonClick = (tool_name) => { + const tool = listToolsData?.find((t) => t.name === tool_name); + if (!tool?.is_available) { + return; + } + if (tool?.is_auth_required && !!tool.auth_url) { + const state = JSON.stringify(fields); + + window.open( + getToolAuthUrl( + tool.auth_url, + `${window.location.href}?datasources=1&state=${btoa(state)}` + ), + '_self' + ); + } + } + return (
{/* Step 1: Define your assistant - name, description, instruction */} @@ -251,6 +269,7 @@ export const AgentSettingsForm: React.FC = (props) => { tools={listToolsData} activeTools={fields.tools ?? []} setActiveTools={(tools: string[]) => setFields({ ...fields, tools })} + handleAuthButtonClick={handleAuthButtonClick} /> setCurrentStep('visibility')} diff --git a/src/interfaces/assistants_web/src/constants/tools.ts b/src/interfaces/assistants_web/src/constants/tools.ts index b325b73234..c2274f3cfc 100644 --- a/src/interfaces/assistants_web/src/constants/tools.ts +++ b/src/interfaces/assistants_web/src/constants/tools.ts @@ -16,9 +16,11 @@ export const TOOL_SLACK_ID = 'slack'; export const FILE_UPLOAD_TOOLS = [TOOL_SEARCH_FILE_ID, TOOL_READ_DOCUMENT_ID]; export const AGENT_SETTINGS_TOOLS = [ TOOL_HYBRID_WEB_SEARCH_ID, + TOOL_READ_DOCUMENT_ID, TOOL_PYTHON_INTERPRETER_ID, TOOL_WEB_SCRAPE_ID, TOOL_SLACK_ID, + TOOL_GOOGLE_DRIVE_ID, ]; export const TOOL_FALLBACK_ICON = 'circles-four'; From b99397302f55dff28dc051dccea45fd7e2327fcd Mon Sep 17 00:00:00 2001 From: EugeneP Date: Thu, 24 Oct 2024 17:37:50 +0200 Subject: [PATCH 03/18] TLK-1725 - Slack tool --- README.md | 1 + docs/custom_tool_guides/slack.md | 122 ++++++++++++++++++ .../config/configuration.template.yaml | 3 + src/backend/config/secrets.template.yaml | 3 + src/backend/tools/slack/auth.py | 2 + .../assistants_web/src/constants/tools.ts | 3 - 6 files changed, 131 insertions(+), 3 deletions(-) create mode 100644 docs/custom_tool_guides/slack.md diff --git a/README.md b/README.md index 5c3bfafb55..b5a85b7a66 100644 --- a/README.md +++ b/README.md @@ -13,6 +13,7 @@ Toolkit is a deployable all-in-one RAG application that enables users to quickly - [How to add tools](/docs/custom_tool_guides/tool_guide.md) - [How to add auth to your tools](/docs/custom_tool_guides/tool_auth_guide.md) - [How to setup Google Drive](/docs/custom_tool_guides/google_drive.md) + - [How to setup Slack Tool](/docs/custom_tool_guides/slack.md) - [How to setup Google Text-to-Speech](/docs/text_to_speech.md) - [How to add authentication](/docs/auth_guide.md) - [How to deploy toolkit services](/docs/service_deployments.md) diff --git a/docs/custom_tool_guides/slack.md b/docs/custom_tool_guides/slack.md new file mode 100644 index 0000000000..aacfb2ce94 --- /dev/null +++ b/docs/custom_tool_guides/slack.md @@ -0,0 +1,122 @@ +# Slack Tool Setup + +To set up the Slack tool you will need a Slack application. Follow the steps below to set it up: + +## 1. Create a Slack App + +Head to the [Slack API](https://api.slack.com/apps) and create a new app. +After creating the app, you will see the `App Credentials` section. Copy the `Client ID` and `Client Secret` values. +That will be used for the environment variables specified above. + +## 2. Set up OAuth & Permissions +OAuth flow is required to authenticate users with Slack. +To enable it please set the following redirect URL to your app's settings: +```bash + https:///v1/tool/auth +``` +Please note that for the local development you will need to enable HTTPS. +See the [Setup HTTPS for Local Development](#5-setup-https-for-local-development) section for more details. +If you are using a local https setup, redirect url should be +``` + https://localhost:8000/v1/tool/auth +``` +Also, you can set up a proxy, such as [ngrok](https://ngrok.com/docs/getting-started/), to expose your local server to the internet. + +The Slack tool uses User Token Scopes to access the user's Slack workspace. +The required and the default permission scope is `search:read`. +Set it in the `OAuth & Permissions` section of your Slack app settings. + +To work with the Slack Tool Advanced token security via token rotation is required. +To enable it, go to the `OAuth & Permissions` section of your Slack app settings and click 'Opt in' button in the 'Advanced token security via token rotation' section. + +More information about the OAuth flow can be found [here](https://api.slack.com/authentication/oauth-v2). + +## 3. Set Up Environment Variables + +Then set the following environment variables. You can either set the below values in your `.env` file: + +```bash +SLACK_CLIENT_ID= +SLACK_CLIENT_SECRET= +``` + +or update your `secrets.yaml` configuration to contain: + +```bash +slack: + client_id: + client_secret: +``` + +## 4. Enable the Slack Tool in the Frontend + +To enable the Slack tool in the frontend, you will need to modify the `src/community/config/tools.py` file. Add the `TOOL_SLACK_ID` to the `AGENT_SETTINGS_TOOLS` list. + +```typescript +export const AGENT_SETTINGS_TOOLS = [ + TOOL_HYBRID_WEB_SEARCH_ID, + TOOL_PYTHON_INTERPRETER_ID, + TOOL_WEB_SCRAPE_ID, + TOOL_SLACK_ID, +]; +``` + +## 5. Setup HTTPS for Local Development + +To enable HTTPS for local development, the self-signed certificate needs to be generated. +Run the following command in the project root directory to generate the certificate and key: + +```bash + openssl req -x509 -newkey rsa:4096 -nodes -out cert.pem -keyout key.pem -days 365 +``` + +Then, update the backend Docker configuration(src/backend/Dockerfile) to use the generated certificate. +Just change next lines in the Dockerfile: +```Dockerfile +COPY pyproject.toml poetry.lock cert.pem key.pem ./ +``` +and +```Dockerfile +CMD uvicorn backend.main:app --reload --host 0.0.0.0 --port ${PORT} --timeout-keep-alive 300 --ssl-keyfile /workspace/key.pem --ssl-certfile /workspace/cert.pem +``` +Change NEXT_PUBLIC_API_HOSTNAME environment variable in the .env `https` protocol: +```bash +NEXT_PUBLIC_API_HOSTNAME=https://localhost:8000 +``` + +or in the configurations.yaml file: + +```yaml +auth: + backend_hostname: https://localhost:8000 +``` + +To run the Frontend with HTTPS, update the `start` script in the `package.json` file: + +```json +"scripts": { + "dev": "next dev --port 4000 --experimental-https", +.......... +} +``` + +Add the following line to the 'docker-compose.yml' file to the frontend environment variables: + +```yaml + NEXT_PUBLIC_API_HOSTNAME=https://localhost:8000 +``` + +and change the API_HOSTNAME to + +```yaml + API_HOSTNAME: https://localhost:8000 +``` +also change the src/interfaces/assistants_web/.env.development file env variables to use https. + +## 6. Run the Backend and Frontend + +run next command to start the backend and frontend: + +```bash +make dev +``` diff --git a/src/backend/config/configuration.template.yaml b/src/backend/config/configuration.template.yaml index 0fb3fc71e1..b5ffb72e17 100644 --- a/src/backend/config/configuration.template.yaml +++ b/src/backend/config/configuration.template.yaml @@ -34,6 +34,9 @@ tools: - tavily_web_search python_interpreter: url: http://terrarium:8080 + slack: + user_scopes: + - search:read feature_flags: # Experimental features use_agents_view: false diff --git a/src/backend/config/secrets.template.yaml b/src/backend/config/secrets.template.yaml index dbd9248b9c..1a596184e5 100644 --- a/src/backend/config/secrets.template.yaml +++ b/src/backend/config/secrets.template.yaml @@ -31,6 +31,9 @@ tools: google_web_search: api_key: cse_id: + slack: + client_id: + client_secret: auth: secret_key: google_oauth: diff --git a/src/backend/tools/slack/auth.py b/src/backend/tools/slack/auth.py index 98c2139411..c5494be9d0 100644 --- a/src/backend/tools/slack/auth.py +++ b/src/backend/tools/slack/auth.py @@ -24,6 +24,7 @@ class SlackAuth(BaseToolAuthentication, ToolAuthenticationCacheMixin): TOOL_ID = SLACK_TOOL_ID AUTH_ENDPOINT = "https://slack.com/oauth/v2/authorize" TOKEN_ENDPOINT = "https://slack.com/api/oauth.v2.access" + EXCHANGE_ENDPOINT = "https://slack.com/api/oauth.v2.exchange" DEFAULT_BOT_SCOPES = ['search:read.public'] DEFAULT_USER_SCOPES = ['search:read'] @@ -84,6 +85,7 @@ def retrieve_auth_token( return response token_data = response_body.get("authed_user", None) + if token_data is None: logger.error( event=f"[Slack Tool] Error retrieving auth token: {response_body}" diff --git a/src/interfaces/assistants_web/src/constants/tools.ts b/src/interfaces/assistants_web/src/constants/tools.ts index c2274f3cfc..0f99f7a093 100644 --- a/src/interfaces/assistants_web/src/constants/tools.ts +++ b/src/interfaces/assistants_web/src/constants/tools.ts @@ -16,11 +16,8 @@ export const TOOL_SLACK_ID = 'slack'; export const FILE_UPLOAD_TOOLS = [TOOL_SEARCH_FILE_ID, TOOL_READ_DOCUMENT_ID]; export const AGENT_SETTINGS_TOOLS = [ TOOL_HYBRID_WEB_SEARCH_ID, - TOOL_READ_DOCUMENT_ID, TOOL_PYTHON_INTERPRETER_ID, TOOL_WEB_SCRAPE_ID, - TOOL_SLACK_ID, - TOOL_GOOGLE_DRIVE_ID, ]; export const TOOL_FALLBACK_ICON = 'circles-four'; From e3bc9750c6744b17862a087baeeb7ea895188711 Mon Sep 17 00:00:00 2001 From: EugeneP Date: Thu, 24 Oct 2024 17:55:15 +0200 Subject: [PATCH 04/18] TLK-1725 - Slack tool lint --- src/backend/tools/slack/utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/backend/tools/slack/utils.py b/src/backend/tools/slack/utils.py index 0ba14a91f6..716e1bb1e0 100644 --- a/src/backend/tools/slack/utils.py +++ b/src/backend/tools/slack/utils.py @@ -1,8 +1,8 @@ from backend.database_models.database import get_session from backend.services.logger.utils import LoggerFactory -from backend.tools.slack import SlackAuth from backend.tools.base import ToolAuthException +from backend.tools.slack import SlackAuth from backend.tools.slack.client import SlackClient from backend.tools.slack.constants import SEARCH_LIMIT, SLACK_TOOL_ID From c7c8c44c4486778bb30a88e166ec49952ad61704 Mon Sep 17 00:00:00 2001 From: EugeneP Date: Thu, 24 Oct 2024 18:33:22 +0200 Subject: [PATCH 05/18] TLK-1725 - Slack tool pyright --- src/backend/config/settings.py | 2 -- src/backend/tools/slack/auth.py | 13 ++++++++----- src/backend/tools/slack/tool.py | 20 +++++++++++--------- 3 files changed, 19 insertions(+), 16 deletions(-) diff --git a/src/backend/config/settings.py b/src/backend/config/settings.py index f709ddb85d..1d14053987 100644 --- a/src/backend/config/settings.py +++ b/src/backend/config/settings.py @@ -213,11 +213,9 @@ class ToolSettings(BaseSettings, BaseModel): brave_web_search: Optional[BraveWebSearchSettings] = Field( default=BraveWebSearchSettings() ) - hybrid_web_search: Optional[HybridWebSearchSettings] = Field( default=HybridWebSearchSettings() ) - slack: Optional[SlackSettings] = Field( default=SlackSettings() ) diff --git a/src/backend/tools/slack/auth.py b/src/backend/tools/slack/auth.py index c5494be9d0..e63b7f77fa 100644 --- a/src/backend/tools/slack/auth.py +++ b/src/backend/tools/slack/auth.py @@ -30,9 +30,10 @@ class SlackAuth(BaseToolAuthentication, ToolAuthenticationCacheMixin): def __init__(self): super().__init__() - self.SLACK_CLIENT_ID = Settings().tools.slack.client_id - self.SLACK_CLIENT_SECRET = Settings().tools.slack.client_secret - self.USER_SCOPES = Settings().tools.slack.user_scopes or self.DEFAULT_USER_SCOPES + settings = Settings() + self.SLACK_CLIENT_ID = settings.tools.slack.client_id if settings.tools and settings.tools.slack else None + self.SLACK_CLIENT_SECRET = settings.tools.slack.client_secret if settings.tools and settings.tools.slack else None + self.USER_SCOPES = settings.tools.slack.user_scopes if settings.tools and settings.tools.slack else self.DEFAULT_USER_SCOPES self.REDIRECT_URL = f"{self.BACKEND_HOST}/v1/tool/auth" if ( @@ -82,7 +83,7 @@ def retrieve_auth_token( logger.error( event=f"[Slack Tool] Error retrieving auth token: {response_body}" ) - return response + return str(response) token_data = response_body.get("authed_user", None) @@ -90,7 +91,7 @@ def retrieve_auth_token( logger.error( event=f"[Slack Tool] Error retrieving auth token: {response_body}" ) - return response + return str(response) tool_auth_crud.create_tool_auth( session, @@ -105,6 +106,8 @@ def retrieve_auth_token( ), ) + return "" + def try_refresh_token(self, session: DBSessionDep, user_id: str, tool_auth: ToolAuth) -> bool: body = { "client_id": self.SLACK_CLIENT_ID, diff --git a/src/backend/tools/slack/tool.py b/src/backend/tools/slack/tool.py index 10fc4763fd..3466ee4194 100644 --- a/src/backend/tools/slack/tool.py +++ b/src/backend/tools/slack/tool.py @@ -1,7 +1,5 @@ from typing import Any, Dict, List -from google.auth.exceptions import RefreshError - from backend.config.settings import Settings from backend.crud import tool_auth as tool_auth_crud from backend.services.logger.utils import LoggerFactory @@ -18,18 +16,22 @@ class SlackTool(BaseTool): """ NAME = SLACK_TOOL_ID - - CLIENT_ID = Settings().tools.slack.client_id - CLIENT_SECRET = Settings().tools.slack.client_secret + CLIENT_ID = "" + CLIENT_SECRET = "" @classmethod def is_available(cls) -> bool: + settings = Settings() + cls.CLIENT_ID = settings.tools.slack.client_id if settings.tools and settings.tools.slack else None + cls.CLIENT_SECRET = settings.tools.slack.client_secret if settings.tools and settings.tools.slack else None + return cls.CLIENT_ID is not None and cls.CLIENT_SECRET is not None - def _handle_tool_specific_errors(self, error: Exception, **kwargs: Any): + @classmethod + def _handle_tool_specific_errors(cls, error: Exception, **kwargs: Any) -> None: message = "[Slack] Tool Error: {}".format(str(error)) - if isinstance(error, RefreshError): + if error: session = kwargs["session"] user_id = kwargs["user_id"] tool_auth_crud.delete_tool_auth( @@ -41,8 +43,8 @@ def _handle_tool_specific_errors(self, error: Exception, **kwargs: Any): ) raise Exception(message) - async def call(self, parameters: dict, **kwargs: Any) -> List[Dict[str, Any]]: - user_id = kwargs.get("user_id") + async def call(self, parameters: dict, ctx: Any, **kwargs: Any) -> List[Dict[str, Any]]: + user_id = kwargs.get("user_id", "") query = parameters.get("query", "") # Search Slack From a7c5b2c5838fd7856dbc45be220d3e3108dbebe5 Mon Sep 17 00:00:00 2001 From: EugeneP Date: Thu, 24 Oct 2024 18:40:35 +0200 Subject: [PATCH 06/18] TLK-1725 - Slack tool pyright --- src/backend/tools/slack/auth.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/backend/tools/slack/auth.py b/src/backend/tools/slack/auth.py index e63b7f77fa..14788e0a84 100644 --- a/src/backend/tools/slack/auth.py +++ b/src/backend/tools/slack/auth.py @@ -33,7 +33,7 @@ def __init__(self): settings = Settings() self.SLACK_CLIENT_ID = settings.tools.slack.client_id if settings.tools and settings.tools.slack else None self.SLACK_CLIENT_SECRET = settings.tools.slack.client_secret if settings.tools and settings.tools.slack else None - self.USER_SCOPES = settings.tools.slack.user_scopes if settings.tools and settings.tools.slack else self.DEFAULT_USER_SCOPES + self.USER_SCOPES = settings.tools.slack.user_scopes if settings.tools and settings.tools.slack else self.DEFAULT_USER_SCOPES or [] self.REDIRECT_URL = f"{self.BACKEND_HOST}/v1/tool/auth" if ( From 326a5a3f3a6442b25fe8a0f82275528bed422817 Mon Sep 17 00:00:00 2001 From: EugeneP Date: Thu, 24 Oct 2024 18:42:20 +0200 Subject: [PATCH 07/18] TLK-1725 - Slack tool pyright --- src/backend/tools/slack/auth.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/backend/tools/slack/auth.py b/src/backend/tools/slack/auth.py index 14788e0a84..6d01ad76e7 100644 --- a/src/backend/tools/slack/auth.py +++ b/src/backend/tools/slack/auth.py @@ -61,7 +61,7 @@ def retrieve_auth_token( self, request: Request, session: DBSessionDep, user_id: str ) -> str: if request.query_params.get("error"): - error = request.query_params.get("error") + error = request.query_params.get("error") or "Unknown error" logger.error(event=f"[Slack Tool] Auth token error: {error}.") return error From 8df6946ac50f9bc26abec8f45db036931a8ae6b2 Mon Sep 17 00:00:00 2001 From: EugeneP Date: Thu, 24 Oct 2024 18:46:29 +0200 Subject: [PATCH 08/18] TLK-1725 - Slack tool pyright --- src/backend/tools/slack/auth.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/backend/tools/slack/auth.py b/src/backend/tools/slack/auth.py index 6d01ad76e7..dd0b26d982 100644 --- a/src/backend/tools/slack/auth.py +++ b/src/backend/tools/slack/auth.py @@ -33,7 +33,7 @@ def __init__(self): settings = Settings() self.SLACK_CLIENT_ID = settings.tools.slack.client_id if settings.tools and settings.tools.slack else None self.SLACK_CLIENT_SECRET = settings.tools.slack.client_secret if settings.tools and settings.tools.slack else None - self.USER_SCOPES = settings.tools.slack.user_scopes if settings.tools and settings.tools.slack else self.DEFAULT_USER_SCOPES or [] + self.USER_SCOPES = settings.tools.slack.user_scopes if settings.tools and settings.tools.slack else self.DEFAULT_USER_SCOPES self.REDIRECT_URL = f"{self.BACKEND_HOST}/v1/tool/auth" if ( @@ -50,7 +50,7 @@ def get_auth_url(self, user_id: str) -> str: params = { "client_id": self.SLACK_CLIENT_ID, - "user_scope": " ".join(self.USER_SCOPES), + "user_scope": " ".join(self.USER_SCOPES or []), "redirect_uri": self.REDIRECT_URL, "state": json.dumps(state), } From c3fa93d3dab418f3b4a7cc3e8ae8ff99a70733d9 Mon Sep 17 00:00:00 2001 From: EugeneP Date: Thu, 24 Oct 2024 19:07:31 +0200 Subject: [PATCH 09/18] TLK-1725 - Slack tool - pyright --- src/backend/tools/slack/auth.py | 6 +++--- src/backend/tools/slack/tool.py | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/backend/tools/slack/auth.py b/src/backend/tools/slack/auth.py index dd0b26d982..3ce7db0183 100644 --- a/src/backend/tools/slack/auth.py +++ b/src/backend/tools/slack/auth.py @@ -31,9 +31,9 @@ class SlackAuth(BaseToolAuthentication, ToolAuthenticationCacheMixin): def __init__(self): super().__init__() settings = Settings() - self.SLACK_CLIENT_ID = settings.tools.slack.client_id if settings.tools and settings.tools.slack else None - self.SLACK_CLIENT_SECRET = settings.tools.slack.client_secret if settings.tools and settings.tools.slack else None - self.USER_SCOPES = settings.tools.slack.user_scopes if settings.tools and settings.tools.slack else self.DEFAULT_USER_SCOPES + self.SLACK_CLIENT_ID = settings.tools.slack.client_id if settings.tools and settings.tools.slack and settings.tools.slack.client_id else None + self.SLACK_CLIENT_SECRET = settings.tools.slack.client_secret if settings.tools and settings.tools.slack and settings.tools.slack.client_secret else None + self.USER_SCOPES = settings.tools.slack.user_scopes if settings.tools and settings.tools.slack and settings.tools.slack.user_scopes else self.DEFAULT_USER_SCOPES self.REDIRECT_URL = f"{self.BACKEND_HOST}/v1/tool/auth" if ( diff --git a/src/backend/tools/slack/tool.py b/src/backend/tools/slack/tool.py index 3466ee4194..f29c94ea77 100644 --- a/src/backend/tools/slack/tool.py +++ b/src/backend/tools/slack/tool.py @@ -22,8 +22,8 @@ class SlackTool(BaseTool): @classmethod def is_available(cls) -> bool: settings = Settings() - cls.CLIENT_ID = settings.tools.slack.client_id if settings.tools and settings.tools.slack else None - cls.CLIENT_SECRET = settings.tools.slack.client_secret if settings.tools and settings.tools.slack else None + cls.CLIENT_ID = settings.tools.slack.client_id if settings.tools and settings.tools.slack and settings.tools.slack.client_id else None + cls.CLIENT_SECRET = settings.tools.slack.client_secret if settings.tools and settings.tools.slack and settings.tools.slack.client_secret else None return cls.CLIENT_ID is not None and cls.CLIENT_SECRET is not None From 50bb431d16c3eca02347ee18a17aa8ded3b0442e Mon Sep 17 00:00:00 2001 From: EugeneP Date: Thu, 24 Oct 2024 20:54:03 +0200 Subject: [PATCH 10/18] TLK-1725 - Slack tool --- .../src/components/AgentSettingsForm/ToolsStep.tsx | 4 ++-- .../assistants_web/src/components/AgentSettingsForm/index.tsx | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/interfaces/assistants_web/src/components/AgentSettingsForm/ToolsStep.tsx b/src/interfaces/assistants_web/src/components/AgentSettingsForm/ToolsStep.tsx index a3fb38a40e..121a1efed2 100644 --- a/src/interfaces/assistants_web/src/components/AgentSettingsForm/ToolsStep.tsx +++ b/src/interfaces/assistants_web/src/components/AgentSettingsForm/ToolsStep.tsx @@ -39,7 +39,7 @@ export const ToolsStep: React.FC = ({ tools, activeTools, setActiveTools, checked={!!activeTools?.includes(name)} handleSwitch={(checked: boolean) => handleUpdateActiveTools(checked, name)} isAuthRequired={is_auth_required} - authUrl={auth_url} + authUrl={auth_url?.toString()} handleAuthButtonClick={handleAuthButtonClick} /> ) @@ -90,7 +90,7 @@ const ToolRow: React.FC<{ kind="outline" theme="mushroom" label="Authenticate" - onClick={() => handleAuthButtonClick(name)} + onClick={() => handleAuthButtonClick ? handleAuthButtonClick(name) : ''} />)}
); diff --git a/src/interfaces/assistants_web/src/components/AgentSettingsForm/index.tsx b/src/interfaces/assistants_web/src/components/AgentSettingsForm/index.tsx index 243a1fe370..d975f74939 100644 --- a/src/interfaces/assistants_web/src/components/AgentSettingsForm/index.tsx +++ b/src/interfaces/assistants_web/src/components/AgentSettingsForm/index.tsx @@ -177,7 +177,7 @@ export const AgentSettingsForm: React.FC = (props) => { } }; - const handleAuthButtonClick = (tool_name) => { + const handleAuthButtonClick = (tool_name: string) => { const tool = listToolsData?.find((t) => t.name === tool_name); if (!tool?.is_available) { return; From 8976f3f435412c5ce719625e45ac1d6f2d230a84 Mon Sep 17 00:00:00 2001 From: EugeneP Date: Fri, 25 Oct 2024 16:16:43 +0200 Subject: [PATCH 11/18] TLK-1725 - Slack tool small improvements --- docs/custom_tool_guides/slack.md | 6 ++++++ src/backend/tools/slack/auth.py | 9 ++++++--- src/backend/tools/slack/tool.py | 5 +++-- 3 files changed, 15 insertions(+), 5 deletions(-) diff --git a/docs/custom_tool_guides/slack.md b/docs/custom_tool_guides/slack.md index aacfb2ce94..860e7a34c8 100644 --- a/docs/custom_tool_guides/slack.md +++ b/docs/custom_tool_guides/slack.md @@ -120,3 +120,9 @@ run next command to start the backend and frontend: ```bash make dev ``` + +## 7. Troubleshooting + +If you encounter any issues with OAuth, please check the following [link](https://api.slack.com/authentication/oauth-v2#errors) +For example, if you see the `invalid_team_for_non_distributed_app` error, please make sure that the app is distributed or +just loggin in with the workspace owner account. diff --git a/src/backend/tools/slack/auth.py b/src/backend/tools/slack/auth.py index 3ce7db0183..e194c12167 100644 --- a/src/backend/tools/slack/auth.py +++ b/src/backend/tools/slack/auth.py @@ -30,10 +30,13 @@ class SlackAuth(BaseToolAuthentication, ToolAuthenticationCacheMixin): def __init__(self): super().__init__() + settings = Settings() - self.SLACK_CLIENT_ID = settings.tools.slack.client_id if settings.tools and settings.tools.slack and settings.tools.slack.client_id else None - self.SLACK_CLIENT_SECRET = settings.tools.slack.client_secret if settings.tools and settings.tools.slack and settings.tools.slack.client_secret else None - self.USER_SCOPES = settings.tools.slack.user_scopes if settings.tools and settings.tools.slack and settings.tools.slack.user_scopes else self.DEFAULT_USER_SCOPES + slack_settings = settings.tools.slack if settings.tools and settings.tools.slack else None + self.SLACK_CLIENT_ID = getattr(slack_settings, 'client_id', None) + self.SLACK_CLIENT_SECRET = getattr(slack_settings, 'client_secret', None) + self.USER_SCOPES = getattr(slack_settings, 'user_scopes', None) or self.DEFAULT_USER_SCOPES + self.REDIRECT_URL = f"{self.BACKEND_HOST}/v1/tool/auth" if ( diff --git a/src/backend/tools/slack/tool.py b/src/backend/tools/slack/tool.py index f29c94ea77..d516ec62ba 100644 --- a/src/backend/tools/slack/tool.py +++ b/src/backend/tools/slack/tool.py @@ -22,8 +22,9 @@ class SlackTool(BaseTool): @classmethod def is_available(cls) -> bool: settings = Settings() - cls.CLIENT_ID = settings.tools.slack.client_id if settings.tools and settings.tools.slack and settings.tools.slack.client_id else None - cls.CLIENT_SECRET = settings.tools.slack.client_secret if settings.tools and settings.tools.slack and settings.tools.slack.client_secret else None + slack_settings = settings.tools.slack if settings.tools and settings.tools.slack else None + cls.CLIENT_ID = getattr(slack_settings, 'client_id', None) + cls.CLIENT_SECRET = getattr(slack_settings, 'client_secret', None) return cls.CLIENT_ID is not None and cls.CLIENT_SECRET is not None From 609982191d32f6adb4fecc5635e83d8333b44a08 Mon Sep 17 00:00:00 2001 From: EugeneP Date: Fri, 25 Oct 2024 16:38:50 +0200 Subject: [PATCH 12/18] TLK-1725 - Slack tool --- docs/custom_tool_guides/slack.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/custom_tool_guides/slack.md b/docs/custom_tool_guides/slack.md index 860e7a34c8..ed6e418d16 100644 --- a/docs/custom_tool_guides/slack.md +++ b/docs/custom_tool_guides/slack.md @@ -124,5 +124,5 @@ make dev ## 7. Troubleshooting If you encounter any issues with OAuth, please check the following [link](https://api.slack.com/authentication/oauth-v2#errors) -For example, if you see the `invalid_team_for_non_distributed_app` error, please make sure that the app is distributed or -just loggin in with the workspace owner account. +For example, if you see the invalid_team_for_non_distributed_app error, +please ensure the app is distributed or try logging in with the workspace owner's account. \ No newline at end of file From 20ae54335437819a19d885b8e078200f06acac80 Mon Sep 17 00:00:00 2001 From: EugeneP Date: Tue, 29 Oct 2024 21:04:57 +0100 Subject: [PATCH 13/18] TLK-1725 - Slack tool review fixes --- docs/custom_tool_guides/slack.md | 15 ++++++--------- src/backend/tools/slack/auth.py | 8 ++++---- src/backend/tools/slack/tool.py | 2 +- 3 files changed, 11 insertions(+), 14 deletions(-) diff --git a/docs/custom_tool_guides/slack.md b/docs/custom_tool_guides/slack.md index ed6e418d16..7b78463886 100644 --- a/docs/custom_tool_guides/slack.md +++ b/docs/custom_tool_guides/slack.md @@ -33,20 +33,17 @@ More information about the OAuth flow can be found [here](https://api.slack.com/ ## 3. Set Up Environment Variables -Then set the following environment variables. You can either set the below values in your `.env` file: - -```bash -SLACK_CLIENT_ID= -SLACK_CLIENT_SECRET= -``` - -or update your `secrets.yaml` configuration to contain: - +Then set the following environment variables. You can either set the below values in your `secrets.yaml` file: ```bash slack: client_id: client_secret: ``` +or update your `.env` configuration to contain: +```bash +SLACK_CLIENT_ID= +SLACK_CLIENT_SECRET= +``` ## 4. Enable the Slack Tool in the Frontend diff --git a/src/backend/tools/slack/auth.py b/src/backend/tools/slack/auth.py index e194c12167..c01cd3b4f6 100644 --- a/src/backend/tools/slack/auth.py +++ b/src/backend/tools/slack/auth.py @@ -39,10 +39,10 @@ def __init__(self): self.REDIRECT_URL = f"{self.BACKEND_HOST}/v1/tool/auth" - if ( - self.SLACK_CLIENT_ID is None - or self.SLACK_CLIENT_SECRET is None - ): + if any([ + self.SLACK_CLIENT_ID is None, + self.SLACK_CLIENT_SECRET is None + ]): raise ValueError( "SLACK_CLIENT_ID and SLACK_CLIENT_SECRET must be set to use Slack Tool Auth." ) diff --git a/src/backend/tools/slack/tool.py b/src/backend/tools/slack/tool.py index d516ec62ba..2abbe0cb2a 100644 --- a/src/backend/tools/slack/tool.py +++ b/src/backend/tools/slack/tool.py @@ -12,7 +12,7 @@ class SlackTool(BaseTool): """ - Tool that searches Slack + Tool that searches Slack for messages and files based on a query. """ NAME = SLACK_TOOL_ID From 1fa44fa16164fdbdac412b254ff79fd5a3f3a67e Mon Sep 17 00:00:00 2001 From: EugeneP Date: Wed, 30 Oct 2024 00:21:56 +0100 Subject: [PATCH 14/18] TLK-1725 - Slack tool review fixes --- docs/custom_tool_guides/slack.md | 6 ++++++ .../src/components/MessagingContainer/Welcome.tsx | 8 +++++++- src/interfaces/assistants_web/src/constants/tools.ts | 3 +++ src/interfaces/assistants_web/src/hooks/use-tools.ts | 10 +++++++--- 4 files changed, 23 insertions(+), 4 deletions(-) diff --git a/docs/custom_tool_guides/slack.md b/docs/custom_tool_guides/slack.md index 7b78463886..1f20e78b44 100644 --- a/docs/custom_tool_guides/slack.md +++ b/docs/custom_tool_guides/slack.md @@ -58,6 +58,12 @@ export const AGENT_SETTINGS_TOOLS = [ ]; ``` +To enable the Slack tool in the frontend for Base Agent, you will need to modify the `src/community/config/tools.py` file. Remove the `TOOL_SLACK_ID` from the `BASE_AGENT_EXCLUDED_TOOLS` list. +By default, the Slack Tool is disabled for the Base Agent. Also if you need to exclude some tool from the Base Agent just add it to the `BASE_AGENT_EXCLUDED_TOOLS` list. +```typescript +export const BASE_AGENT_EXCLUDED_TOOLS = []; +``` + ## 5. Setup HTTPS for Local Development To enable HTTPS for local development, the self-signed certificate needs to be generated. diff --git a/src/interfaces/assistants_web/src/components/MessagingContainer/Welcome.tsx b/src/interfaces/assistants_web/src/components/MessagingContainer/Welcome.tsx index 10e83f9993..4af97c1dd9 100644 --- a/src/interfaces/assistants_web/src/components/MessagingContainer/Welcome.tsx +++ b/src/interfaces/assistants_web/src/components/MessagingContainer/Welcome.tsx @@ -8,6 +8,7 @@ import { CoralLogo, Icon, Text } from '@/components/UI'; import { useAgent, useBrandedColors, useListTools } from '@/hooks'; import { cn } from '@/utils'; import { checkIsBaseAgent } from '@/utils'; +import {BASE_AGENT_EXCLUDED_TOOLS} from "@/constants"; type Props = { show: boolean; @@ -23,6 +24,11 @@ export const Welcome: React.FC = ({ show, agentId }) => { const { contrastText, bg, contrastFill } = useBrandedColors(agentId); const isBaseAgent = checkIsBaseAgent(agent); + // Filter out tools that are excluded for the base agent + let toolsFiltered = [...tools]; + if (isBaseAgent) { + toolsFiltered = tools.filter((tool) => !BASE_AGENT_EXCLUDED_TOOLS.includes(tool.name ?? '')); + } return ( = ({ show, agentId }) => { Toggle Tools On/Off
)} - + ); diff --git a/src/interfaces/assistants_web/src/constants/tools.ts b/src/interfaces/assistants_web/src/constants/tools.ts index 0f99f7a093..79920f461e 100644 --- a/src/interfaces/assistants_web/src/constants/tools.ts +++ b/src/interfaces/assistants_web/src/constants/tools.ts @@ -20,6 +20,9 @@ export const AGENT_SETTINGS_TOOLS = [ TOOL_WEB_SCRAPE_ID, ]; +// Tools won't be available for the base agent +export const BASE_AGENT_EXCLUDED_TOOLS = [TOOL_SLACK_ID]; + export const TOOL_FALLBACK_ICON = 'circles-four'; export const TOOL_ID_TO_DISPLAY_INFO: { [id: string]: { icon: IconName } } = { [TOOL_WEB_SEARCH_ID]: { icon: 'web' }, diff --git a/src/interfaces/assistants_web/src/hooks/use-tools.ts b/src/interfaces/assistants_web/src/hooks/use-tools.ts index dd4062e7cd..3d6eede405 100644 --- a/src/interfaces/assistants_web/src/hooks/use-tools.ts +++ b/src/interfaces/assistants_web/src/hooks/use-tools.ts @@ -4,11 +4,12 @@ import useDrivePicker from 'react-google-drive-picker'; import type { PickerCallback } from 'react-google-drive-picker/dist/typeDefs'; import { AgentPublic, ApiError, ManagedTool, useCohereClient } from '@/cohere-client'; -import { DEFAULT_AGENT_TOOLS, TOOL_GOOGLE_DRIVE_ID } from '@/constants'; +import {BASE_AGENT_EXCLUDED_TOOLS, DEFAULT_AGENT_TOOLS, TOOL_GOOGLE_DRIVE_ID} from '@/constants'; import { env } from '@/env.mjs'; import { useNotify } from '@/hooks'; import { useParamsStore } from '@/stores'; import { ConfigurableParams } from '@/stores/slices/paramsSlice'; +import {checkIsBaseAgent} from "@/utils"; export const useListTools = (enabled: boolean = true) => { const client = useCohereClient(); @@ -94,9 +95,11 @@ export const useAvailableTools = ({ const { params, setParams } = useParamsStore(); const { tools: paramTools } = params; const enabledTools = paramTools ?? []; + const isBaseAgent = checkIsBaseAgent(agent); const unauthedTools = tools?.filter( - (tool) => tool.is_auth_required && tool.name && requiredTools?.includes(tool.name) + (tool) => tool.is_auth_required && tool.name && requiredTools?.includes(tool.name) && + !(isBaseAgent && BASE_AGENT_EXCLUDED_TOOLS.includes(tool.name)) ) ?? []; const availableTools = useMemo(() => { @@ -104,7 +107,8 @@ export const useAvailableTools = ({ (t) => t.is_visible && t.is_available && - (!requiredTools || requiredTools.some((rt) => rt === t.name)) + (!requiredTools || requiredTools.some((rt) => rt === t.name)) && + !(isBaseAgent && BASE_AGENT_EXCLUDED_TOOLS.some((rt) => rt === t.name)) ); }, [managedTools, requiredTools]); From ec17758a82f5fdb72e0e7ae1a37c583640becf8c Mon Sep 17 00:00:00 2001 From: EugeneP Date: Thu, 31 Oct 2024 12:47:32 +0100 Subject: [PATCH 15/18] TLK-1725 - Slack tool prettier --- .../src/app/(main)/settings/Settings.tsx | 10 ++- .../AgentSettingsForm/StatusConnection.tsx | 4 +- .../AgentSettingsForm/ToolsStep.tsx | 72 +++++++++++-------- .../components/AgentSettingsForm/index.tsx | 7 +- .../components/MessagingContainer/Welcome.tsx | 2 +- .../assistants_web/src/components/UI/Icon.tsx | 2 +- .../assistants_web/src/hooks/use-tools.ts | 11 +-- 7 files changed, 61 insertions(+), 47 deletions(-) diff --git a/src/interfaces/assistants_web/src/app/(main)/settings/Settings.tsx b/src/interfaces/assistants_web/src/app/(main)/settings/Settings.tsx index 638a369ea7..38f0900a8e 100644 --- a/src/interfaces/assistants_web/src/app/(main)/settings/Settings.tsx +++ b/src/interfaces/assistants_web/src/app/(main)/settings/Settings.tsx @@ -2,11 +2,11 @@ import { PropsWithChildren, useState } from 'react'; +import { StatusConnection } from '@/components/AgentSettingsForm/StatusConnection'; import { MobileHeader } from '@/components/Global'; import { Button, DarkModeToggle, Icon, ShowStepsToggle, Tabs, Text } from '@/components/UI'; import { useDeleteAuthTool, useListTools, useNotify } from '@/hooks'; import { cn, getToolAuthUrl } from '@/utils'; -import {StatusConnection} from "@/components/AgentSettingsForm/StatusConnection"; const tabs = [
@@ -133,7 +133,7 @@ const GoogleDriveConnection = () => { } }; - const isGoogleDriveConnected = !googleDriveTool.is_auth_required ?? false; + const isGoogleDriveConnected = !(googleDriveTool.is_auth_required ?? false); const authUrl = getToolAuthUrl(googleDriveTool.auth_url); return ( @@ -206,7 +206,7 @@ const SlackConnection = () => { } }; - const isSlackConnected = !slackTool.is_auth_required ?? false; + const isSlackConnected = !(slackTool.is_auth_required ?? false); return (
@@ -217,9 +217,7 @@ const SlackConnection = () => {
- - Connect to Slack - + Connect to Slack
{isSlackConnected ? (
diff --git a/src/interfaces/assistants_web/src/components/AgentSettingsForm/StatusConnection.tsx b/src/interfaces/assistants_web/src/components/AgentSettingsForm/StatusConnection.tsx index df5f912f41..126752e280 100644 --- a/src/interfaces/assistants_web/src/components/AgentSettingsForm/StatusConnection.tsx +++ b/src/interfaces/assistants_web/src/components/AgentSettingsForm/StatusConnection.tsx @@ -1,5 +1,5 @@ -import {Text} from "@/components/UI"; -import {cn} from "@/utils"; +import { Text } from '@/components/UI'; +import { cn } from '@/utils'; export const StatusConnection: React.FC<{ connected: boolean }> = ({ connected }) => { const label = connected ? 'Connected' : 'Disconnected'; diff --git a/src/interfaces/assistants_web/src/components/AgentSettingsForm/ToolsStep.tsx b/src/interfaces/assistants_web/src/components/AgentSettingsForm/ToolsStep.tsx index 121a1efed2..595dd0812b 100644 --- a/src/interfaces/assistants_web/src/components/AgentSettingsForm/ToolsStep.tsx +++ b/src/interfaces/assistants_web/src/components/AgentSettingsForm/ToolsStep.tsx @@ -1,10 +1,9 @@ import Link from 'next/link'; import { ManagedTool } from '@/cohere-client'; -import { Icon, IconName, Switch, Text, Button } from '@/components/UI'; +import { StatusConnection } from '@/components/AgentSettingsForm/StatusConnection'; +import { Button, Icon, IconName, Switch, Text } from '@/components/UI'; import { AGENT_SETTINGS_TOOLS, TOOL_FALLBACK_ICON, TOOL_ID_TO_DISPLAY_INFO } from '@/constants'; -import {StatusConnection} from "@/components/AgentSettingsForm/StatusConnection"; - type Props = { tools?: ManagedTool[]; @@ -13,7 +12,12 @@ type Props = { handleAuthButtonClick: (toolName: string) => void; }; -export const ToolsStep: React.FC = ({ tools, activeTools, setActiveTools, handleAuthButtonClick }) => { +export const ToolsStep: React.FC = ({ + tools, + activeTools, + setActiveTools, + handleAuthButtonClick, +}) => { const availableTools = tools?.filter( (tool) => tool.name && AGENT_SETTINGS_TOOLS.includes(tool.name) ); @@ -29,20 +33,22 @@ export const ToolsStep: React.FC = ({ tools, activeTools, setActiveTools, return (
- {availableTools?.map(({name, description, is_auth_required, auth_url}) => - !!name && description && ( - handleUpdateActiveTools(checked, name)} - isAuthRequired={is_auth_required} - authUrl={auth_url?.toString()} - handleAuthButtonClick={handleAuthButtonClick} - /> - ) + {availableTools?.map( + ({ name, description, is_auth_required, auth_url }) => + !!name && + description && ( + handleUpdateActiveTools(checked, name)} + isAuthRequired={is_auth_required} + authUrl={auth_url?.toString()} + handleAuthButtonClick={handleAuthButtonClick} + /> + ) )} Don‘t see the tool you need? {/* TODO: get tool request link from Elaine */} @@ -63,7 +69,16 @@ const ToolRow: React.FC<{ isAuthRequired?: boolean; authUrl?: string; handleAuthButtonClick?: (toolName: string) => void; -}> = ({ name, description, icon, checked, handleSwitch, isAuthRequired, authUrl, handleAuthButtonClick}) => { +}> = ({ + name, + description, + icon, + checked, + handleSwitch, + isAuthRequired, + authUrl, + handleAuthButtonClick, +}) => { return (
@@ -76,22 +91,23 @@ const ToolRow: React.FC<{
- !!name && handleSwitch(checked)} - showCheckedState - /> + !!name && handleSwitch(checked)} + showCheckedState + />
{description} - {!isAuthRequired && !!authUrl && ()} + {!isAuthRequired && !!authUrl && } {isAuthRequired && !!authUrl && ( -
); }; diff --git a/src/interfaces/assistants_web/src/components/AgentSettingsForm/index.tsx b/src/interfaces/assistants_web/src/components/AgentSettingsForm/index.tsx index d975f74939..83d26adb4b 100644 --- a/src/interfaces/assistants_web/src/components/AgentSettingsForm/index.tsx +++ b/src/interfaces/assistants_web/src/components/AgentSettingsForm/index.tsx @@ -186,14 +186,11 @@ export const AgentSettingsForm: React.FC = (props) => { const state = JSON.stringify(fields); window.open( - getToolAuthUrl( - tool.auth_url, - `${window.location.href}?datasources=1&state=${btoa(state)}` - ), + getToolAuthUrl(tool.auth_url, `${window.location.href}?datasources=1&state=${btoa(state)}`), '_self' ); } - } + }; return (
diff --git a/src/interfaces/assistants_web/src/components/MessagingContainer/Welcome.tsx b/src/interfaces/assistants_web/src/components/MessagingContainer/Welcome.tsx index 4af97c1dd9..b775bae8bc 100644 --- a/src/interfaces/assistants_web/src/components/MessagingContainer/Welcome.tsx +++ b/src/interfaces/assistants_web/src/components/MessagingContainer/Welcome.tsx @@ -5,10 +5,10 @@ import React from 'react'; import { AssistantTools } from '@/components/MessagingContainer'; import { CoralLogo, Icon, Text } from '@/components/UI'; +import { BASE_AGENT_EXCLUDED_TOOLS } from '@/constants'; import { useAgent, useBrandedColors, useListTools } from '@/hooks'; import { cn } from '@/utils'; import { checkIsBaseAgent } from '@/utils'; -import {BASE_AGENT_EXCLUDED_TOOLS} from "@/constants"; type Props = { show: boolean; diff --git a/src/interfaces/assistants_web/src/components/UI/Icon.tsx b/src/interfaces/assistants_web/src/components/UI/Icon.tsx index 44ad90c455..f0e679aaf1 100644 --- a/src/interfaces/assistants_web/src/components/UI/Icon.tsx +++ b/src/interfaces/assistants_web/src/components/UI/Icon.tsx @@ -48,6 +48,7 @@ import { Share, Show, SignOut, + Slack, Sparkle, Stop, Subtract, @@ -60,7 +61,6 @@ import { Volume, Warning, Web, - Slack, } from '@/assets/icons'; import { cn } from '@/utils'; diff --git a/src/interfaces/assistants_web/src/hooks/use-tools.ts b/src/interfaces/assistants_web/src/hooks/use-tools.ts index 3d6eede405..584e437f1d 100644 --- a/src/interfaces/assistants_web/src/hooks/use-tools.ts +++ b/src/interfaces/assistants_web/src/hooks/use-tools.ts @@ -4,12 +4,12 @@ import useDrivePicker from 'react-google-drive-picker'; import type { PickerCallback } from 'react-google-drive-picker/dist/typeDefs'; import { AgentPublic, ApiError, ManagedTool, useCohereClient } from '@/cohere-client'; -import {BASE_AGENT_EXCLUDED_TOOLS, DEFAULT_AGENT_TOOLS, TOOL_GOOGLE_DRIVE_ID} from '@/constants'; +import { BASE_AGENT_EXCLUDED_TOOLS, DEFAULT_AGENT_TOOLS, TOOL_GOOGLE_DRIVE_ID } from '@/constants'; import { env } from '@/env.mjs'; import { useNotify } from '@/hooks'; import { useParamsStore } from '@/stores'; import { ConfigurableParams } from '@/stores/slices/paramsSlice'; -import {checkIsBaseAgent} from "@/utils"; +import { checkIsBaseAgent } from '@/utils'; export const useListTools = (enabled: boolean = true) => { const client = useCohereClient(); @@ -98,8 +98,11 @@ export const useAvailableTools = ({ const isBaseAgent = checkIsBaseAgent(agent); const unauthedTools = tools?.filter( - (tool) => tool.is_auth_required && tool.name && requiredTools?.includes(tool.name) && - !(isBaseAgent && BASE_AGENT_EXCLUDED_TOOLS.includes(tool.name)) + (tool) => + tool.is_auth_required && + tool.name && + requiredTools?.includes(tool.name) && + !(isBaseAgent && BASE_AGENT_EXCLUDED_TOOLS.includes(tool.name)) ) ?? []; const availableTools = useMemo(() => { From e2371834e662cae3092944bba4816cca8659c5ab Mon Sep 17 00:00:00 2001 From: EugeneP Date: Thu, 31 Oct 2024 12:54:13 +0100 Subject: [PATCH 16/18] TLK-1725 - Slack tool --- .../assistants_web/src/assets/icons/Slack.tsx | 30 +++++++++++++++++++ 1 file changed, 30 insertions(+) create mode 100644 src/interfaces/assistants_web/src/assets/icons/Slack.tsx diff --git a/src/interfaces/assistants_web/src/assets/icons/Slack.tsx b/src/interfaces/assistants_web/src/assets/icons/Slack.tsx new file mode 100644 index 0000000000..bb23751e1a --- /dev/null +++ b/src/interfaces/assistants_web/src/assets/icons/Slack.tsx @@ -0,0 +1,30 @@ +import * as React from 'react'; +import { SVGProps } from 'react'; + +import { cn } from '@/utils'; + +export const Slack: React.FC> = ({ className, ...props }) => ( + + + + + + +); From e64bb61d46f9d87b01fc98ed9d7cfdda83cef018 Mon Sep 17 00:00:00 2001 From: EugeneP Date: Thu, 31 Oct 2024 13:03:23 +0100 Subject: [PATCH 17/18] TLK-1725 - Slack tool --- .../assistants_web/src/app/(main)/settings/Settings.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/interfaces/assistants_web/src/app/(main)/settings/Settings.tsx b/src/interfaces/assistants_web/src/app/(main)/settings/Settings.tsx index 38f0900a8e..ffcd5a7ae0 100644 --- a/src/interfaces/assistants_web/src/app/(main)/settings/Settings.tsx +++ b/src/interfaces/assistants_web/src/app/(main)/settings/Settings.tsx @@ -7,6 +7,7 @@ import { MobileHeader } from '@/components/Global'; import { Button, DarkModeToggle, Icon, ShowStepsToggle, Tabs, Text } from '@/components/UI'; import { useDeleteAuthTool, useListTools, useNotify } from '@/hooks'; import { cn, getToolAuthUrl } from '@/utils'; +import {TOOL_SLACK_ID} from "@/constants"; const tabs = [
@@ -192,7 +193,7 @@ const SlackConnection = () => { const { data } = useListTools(); const { mutateAsync: deleteAuthTool } = useDeleteAuthTool(); const notify = useNotify(); - const slackTool = data?.find((tool) => tool.name === 'slack'); + const slackTool = data?.find((tool) => tool.name === TOOL_SLACK_ID); if (!slackTool) { return null; From e6004b02a820d49a94379faff4f149708891a165 Mon Sep 17 00:00:00 2001 From: EugeneP Date: Thu, 31 Oct 2024 13:05:22 +0100 Subject: [PATCH 18/18] TLK-1725 - Slack tool --- .../assistants_web/src/app/(main)/settings/Settings.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/interfaces/assistants_web/src/app/(main)/settings/Settings.tsx b/src/interfaces/assistants_web/src/app/(main)/settings/Settings.tsx index ffcd5a7ae0..4e1f34bd3c 100644 --- a/src/interfaces/assistants_web/src/app/(main)/settings/Settings.tsx +++ b/src/interfaces/assistants_web/src/app/(main)/settings/Settings.tsx @@ -5,9 +5,9 @@ import { PropsWithChildren, useState } from 'react'; import { StatusConnection } from '@/components/AgentSettingsForm/StatusConnection'; import { MobileHeader } from '@/components/Global'; import { Button, DarkModeToggle, Icon, ShowStepsToggle, Tabs, Text } from '@/components/UI'; +import { TOOL_SLACK_ID } from '@/constants'; import { useDeleteAuthTool, useListTools, useNotify } from '@/hooks'; import { cn, getToolAuthUrl } from '@/utils'; -import {TOOL_SLACK_ID} from "@/constants"; const tabs = [