diff --git a/README.md b/README.md index ae1c5fdb05..d8f28f3e0b 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..1f20e78b44 --- /dev/null +++ b/docs/custom_tool_guides/slack.md @@ -0,0 +1,131 @@ +# 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 `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 + +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, +]; +``` + +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. +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 +``` + +## 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 ensure the app is distributed or try logging in with the workspace owner's account. \ No newline at end of file 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/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/config/settings.py b/src/backend/config/settings.py index 4e32b6bb91..1d14053987 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( @@ -195,10 +213,13 @@ 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() + ) + class DatabaseSettings(BaseSettings, BaseModel): 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/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/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..c01cd3b4f6 --- /dev/null +++ b/src/backend/tools/slack/auth.py @@ -0,0 +1,159 @@ +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" + EXCHANGE_ENDPOINT = "https://slack.com/api/oauth.v2.exchange" + DEFAULT_BOT_SCOPES = ['search:read.public'] + DEFAULT_USER_SCOPES = ['search:read'] + + def __init__(self): + super().__init__() + + settings = Settings() + 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 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." + ) + + 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 or []), + "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") or "Unknown 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 str(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 str(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")), + ), + ) + + return "" + + 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..2abbe0cb2a --- /dev/null +++ b/src/backend/tools/slack/tool.py @@ -0,0 +1,55 @@ +from typing import Any, Dict, List + +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 for messages and files based on a query. + """ + + NAME = SLACK_TOOL_ID + CLIENT_ID = "" + CLIENT_SECRET = "" + + @classmethod + def is_available(cls) -> bool: + settings = Settings() + 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 + + @classmethod + def _handle_tool_specific_errors(cls, error: Exception, **kwargs: Any) -> None: + message = "[Slack] Tool Error: {}".format(str(error)) + + if error: + 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, ctx: Any, **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..716e1bb1e0 --- /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.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 + +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..4e1f34bd3c 100644 --- a/src/interfaces/assistants_web/src/app/(main)/settings/Settings.tsx +++ b/src/interfaces/assistants_web/src/app/(main)/settings/Settings.tsx @@ -2,8 +2,10 @@ 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'; @@ -76,6 +78,7 @@ const Connections = () => (
+
); @@ -131,7 +134,7 @@ const GoogleDriveConnection = () => { } }; - const isGoogleDriveConnected = !googleDriveTool.is_auth_required ?? false; + const isGoogleDriveConnected = !(googleDriveTool.is_auth_required ?? false); const authUrl = getToolAuthUrl(googleDriveTool.auth_url); return ( @@ -186,18 +189,59 @@ const GoogleDriveConnection = () => { ); }; +const SlackConnection = () => { + const { data } = useListTools(); + const { mutateAsync: deleteAuthTool } = useDeleteAuthTool(); + const notify = useNotify(); + const slackTool = data?.find((tool) => tool.name === TOOL_SLACK_ID); + + 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 StatusConnection: React.FC<{ connected: boolean }> = ({ connected }) => { - const label = connected ? 'Connected' : 'Disconnected'; return ( - - - {label} - +
+
+
+ + Slack +
+ +
+ Connect to Slack +
+ {isSlackConnected ? ( +
+
+
+
+ ) : ( +
+
); }; 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 }) => ( + + + + + + +); 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/AgentSettingsForm/StatusConnection.tsx b/src/interfaces/assistants_web/src/components/AgentSettingsForm/StatusConnection.tsx new file mode 100644 index 0000000000..126752e280 --- /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..595dd0812b 100644 --- a/src/interfaces/assistants_web/src/components/AgentSettingsForm/ToolsStep.tsx +++ b/src/interfaces/assistants_web/src/components/AgentSettingsForm/ToolsStep.tsx @@ -1,19 +1,27 @@ import Link from 'next/link'; import { ManagedTool } from '@/cohere-client'; -import { Icon, IconName, Switch, Text } 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'; 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) { @@ -26,16 +34,19 @@ export const ToolsStep: React.FC = ({ tools, activeTools, setActiveTools return (
{availableTools?.map( - ({ name, description }) => + ({ name, description, is_auth_required, auth_url }) => !!name && description && ( handleUpdateActiveTools(checked, name)} + isAuthRequired={is_auth_required} + authUrl={auth_url?.toString()} + handleAuthButtonClick={handleAuthButtonClick} /> ) )} @@ -55,7 +66,19 @@ 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 +90,24 @@ const ToolRow: React.FC<{ {name}
- !!name && handleSwitch(checked)} - showCheckedState - /> +
+ !!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..83d26adb4b 100644 --- a/src/interfaces/assistants_web/src/components/AgentSettingsForm/index.tsx +++ b/src/interfaces/assistants_web/src/components/AgentSettingsForm/index.tsx @@ -177,6 +177,21 @@ export const AgentSettingsForm: React.FC = (props) => { } }; + const handleAuthButtonClick = (tool_name: string) => { + 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 +266,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/components/MessagingContainer/Welcome.tsx b/src/interfaces/assistants_web/src/components/MessagingContainer/Welcome.tsx index 10e83f9993..b775bae8bc 100644 --- a/src/interfaces/assistants_web/src/components/MessagingContainer/Welcome.tsx +++ b/src/interfaces/assistants_web/src/components/MessagingContainer/Welcome.tsx @@ -5,6 +5,7 @@ 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'; @@ -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/components/UI/Icon.tsx b/src/interfaces/assistants_web/src/components/UI/Icon.tsx index 7e79d9fcfe..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, @@ -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..79920f461e 100644 --- a/src/interfaces/assistants_web/src/constants/tools.ts +++ b/src/interfaces/assistants_web/src/constants/tools.ts @@ -12,6 +12,7 @@ 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, @@ -19,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' }, @@ -30,4 +34,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' }, }; diff --git a/src/interfaces/assistants_web/src/hooks/use-tools.ts b/src/interfaces/assistants_web/src/hooks/use-tools.ts index dd4062e7cd..584e437f1d 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,14 @@ 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 +110,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]);