Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Update slack_bot #1353

Closed
wants to merge 26 commits into from
Closed
Show file tree
Hide file tree
Changes from 5 commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
b395261
update slack_bot
Asher-hss Dec 20, 2024
e7c4780
update
Asher-hss Dec 27, 2024
20eec2d
Merge branch 'master' into update_slack_app
Asher-hss Dec 27, 2024
6391bb4
update test
Asher-hss Dec 27, 2024
632709c
update token
Asher-hss Dec 27, 2024
7584a8d
update
Asher-hss Jan 10, 2025
a993f45
Merge branch 'master' into update_slack_app
Asher-hss Jan 15, 2025
ccf824a
update
Asher-hss Jan 16, 2025
bd62499
update
Asher-hss Jan 16, 2025
17d62a3
update
Asher-hss Jan 17, 2025
c33fc14
update
Asher-hss Jan 17, 2025
9150fdc
docs:add huggingface access token annotation in cot data gen cookbook…
zjrwtx Jan 15, 2025
cfe9030
docs: Cookbook for dynamic travel planner (#1450)
Wendong-Fan Jan 15, 2025
fbfccc4
docs: Cookbook for self-instruct (#1451)
Wendong-Fan Jan 15, 2025
afc26bb
fix: document link mapping issue to maintain access to old links (#1449)
koch3092 Jan 15, 2025
5aa3043
fix: cookbook url for redirection (#1452)
Wendong-Fan Jan 15, 2025
09f1120
fix: remove redirect url for cot_data_gen
Wendong-Fan Jan 15, 2025
c60c3cf
chore:change the unstructured docs link (#1454)
zjrwtx Jan 16, 2025
9ba76fe
docs: Reward model cookbook (#1332)
Asher-hss Jan 17, 2025
8ed9442
docs: Qwen data-gen cookbook (#1358)
MuggleJinx Jan 17, 2025
6e80581
docs: Camel HumanLayer cookbook (#1420)
MuggleJinx Jan 17, 2025
2fe501d
feat: structured loader (#1395)
Wendong-Fan Jan 17, 2025
f12547b
update
Asher-hss Jan 17, 2025
759ec6e
update
Asher-hss Jan 17, 2025
c18e0bf
update
Asher-hss Jan 17, 2025
c74b5bd
update
Asher-hss Jan 17, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
139 changes: 110 additions & 29 deletions camel/bots/slack/slack_app.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
# ========= Copyright 2023-2024 @ CAMEL-AI.org. All Rights Reserved. =========
import logging
import os
from typing import TYPE_CHECKING, Any, Dict, Optional
from typing import TYPE_CHECKING, Any, Callable, Dict, Optional, Union

from slack_sdk.oauth.installation_store.async_installation_store import (
AsyncInstallationStore,
Expand Down Expand Up @@ -61,18 +61,21 @@ class SlackApp:
def __init__(
self,
token: Optional[str] = None,
app_token: Optional[str] = None,
scopes: Optional[str] = None,
signing_secret: Optional[str] = None,
client_id: Optional[str] = None,
client_secret: Optional[str] = None,
redirect_uri_path: str = "/slack/oauth_redirect",
installation_store: Optional[AsyncInstallationStore] = None,
socket_mode: bool = True,
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hi @Asher-hss , when we create a new slack app, socket mode is turned off by default. Let's set socket_mode to False to maintain consistency with the official configuration.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

sure

) -> None:
r"""Initializes the SlackApp instance by setting up the Slack Bolt app
and configuring event handlers and OAuth settings.

Args:
token (Optional[str]): The Slack API token.
app_token (Optional[str]): The Slack app token.
scopes (Optional[str]): The scopes for Slack app permissions.
signing_secret (Optional[str]): The signing secret for verifying
requests.
Expand All @@ -82,7 +85,13 @@ def __init__(
(default is "/slack/oauth_redirect").
installation_store (Optional[AsyncInstallationStore]): An optional
installation store for OAuth installations.
socket_mode (bool): A flag to enable socket mode for the Slack app,
defaults to True. if False, you must set request URL in the
slack website.
"""
from slack_bolt.adapter.socket_mode.async_handler import (
AsyncSocketModeHandler,
)
from slack_bolt.adapter.starlette.async_handler import (
AsyncSlackRequestHandler,
)
Expand All @@ -100,38 +109,57 @@ def __init__(
self.client_secret: Optional[str] = client_secret or os.getenv(
"SLACK_CLIENT_SECRET"
)
self.app_token: Optional[str] = app_token or os.getenv(
"SLACK_APP_TOKEN"
)
self.custom_handler: Optional[Callable[[str], str]] = None
self.socket_mode: bool = socket_mode
self._handler: Optional[
Union[AsyncSlackRequestHandler, AsyncSocketModeHandler]
] = None
if not self.socket_mode:
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hi @Asher-hss , the slack app contains OAuth verification and Message communication. In socket mode, the OAuth process also needs to be allowed. I don't think the verification here is appropriate.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The purpose here is:

  1. Different modes correspond to different validation procedures to avoid confusion in validation.
  2. Use default validation in socket mode to reduce the learning curve for users.

if not all([self.token, self.scopes, self.signing_secret]):
raise ValueError(
"`SLACK_TOKEN`, `SLACK_SCOPES`, and `SLACK_SIGNING_SECRET`"
"environment variables must be set. Get it here: "
"`https://api.slack.com/apps`."
)

# Setup OAuth settings if client ID and secret are provided
if self.client_id and self.client_secret:
self._app = AsyncApp(
oauth_settings=AsyncOAuthSettings(
client_id=self.client_id,
client_secret=self.client_secret,
scopes=self.scopes,
redirect_uri_path=redirect_uri_path,
),
logger=logger,
signing_secret=self.signing_secret,
installation_store=installation_store,
token=self.token,
)
else:
# Initialize Slack Bolt AsyncApp with settings
self._app = AsyncApp(
logger=logger,
signing_secret=self.signing_secret,
installation_store=installation_store,
token=self.token,
)

self._handler = AsyncSlackRequestHandler(self._app)
else:
if not self.app_token:
raise ValueError(
"`SLACK_APP_TOKEN` environment variable must be set. "
"Get it here: `https://api.slack.com/apps`."
)

if not all([self.token, self.scopes, self.signing_secret]):
raise ValueError(
"`SLACK_TOKEN`, `SLACK_SCOPES`, and `SLACK_SIGNING_SECRET` "
"environment variables must be set. Get it here: "
"`https://api.slack.com/apps`."
)

# Setup OAuth settings if client ID and secret are provided
if self.client_id and self.client_secret:
self._app = AsyncApp(
oauth_settings=AsyncOAuthSettings(
client_id=self.client_id,
client_secret=self.client_secret,
scopes=self.scopes,
redirect_uri_path=redirect_uri_path,
),
logger=logger,
signing_secret=self.signing_secret,
installation_store=installation_store,
token=self.token,
)
else:
# Initialize Slack Bolt AsyncApp with settings
self._app = AsyncApp(
logger=logger,
signing_secret=self.signing_secret,
installation_store=installation_store,
token=self.token,
)

self._handler = AsyncSlackRequestHandler(self._app)
self.setup_handlers()

def setup_handlers(self) -> None:
Expand All @@ -144,6 +172,27 @@ def setup_handlers(self) -> None:
self._app.event("app_mention")(self.app_mention)
self._app.event("message")(self.on_message)

async def start(self) -> None:
r"""Starts the Slack Bolt app asynchronously."""
from slack_bolt.adapter.socket_mode.async_handler import (
AsyncSocketModeHandler,
)
from slack_bolt.adapter.starlette.async_handler import (
AsyncSlackRequestHandler,
)

if not self._handler:
self._handler = AsyncSocketModeHandler(
self._app, app_token=self.app_token
)
await self._handler.start_async()
elif isinstance(self._handler, AsyncSlackRequestHandler):
logger.info(
"AsyncSlackRequestHandler does not support "
"start_async.Ensure it is integrated with "
"a web framework."
)

def run(
self,
port: int = 3000,
Expand Down Expand Up @@ -175,7 +224,24 @@ async def handle_request(
Returns:
The response generated by the Slack Bolt handler.
"""
return await self._handler.handle(request)
from slack_bolt.adapter.socket_mode.async_handler import (
AsyncSocketModeHandler,
)

if self._handler is None:
logger.error("Handler is not initialized.")
return responses.Response(
status_code=500, content="Handler not initialized."
)
if isinstance(self._handler, AsyncSocketModeHandler):
logger.info("Skipping processing for AsyncSocketModeHandler.")
return responses.Response(
status_code=200, content="Socket mode request skipped."
)
response = await self._handler.handle(request)
if response is None:
return responses.Response(status_code=400, content="Bad Request")
return response

async def app_mention(
self,
Expand Down Expand Up @@ -204,6 +270,9 @@ async def app_mention(
logger.info(f"app_mention, event_profile: {event_profile}")
logger.info(f"app_mention, event_body: {event_body}")
logger.info(f"app_mention, say: {say}")
if self.custom_handler:
response = self.custom_handler(event_profile.text)
await say(response)

async def on_message(
self,
Expand Down Expand Up @@ -236,6 +305,9 @@ async def on_message(
logger.info(f"on_message, say: {say}")

logger.info(f"Received message: {event_profile.text}")
if self.custom_handler:
response = self.custom_handler(event_profile.text)
await say(response)

def mention_me(
self, context: "AsyncBoltContext", body: SlackEventBody
Expand All @@ -253,3 +325,12 @@ def mention_me(
bot_user_id = context.bot_user_id
mention = f"<@{bot_user_id}>"
return mention in message

def set_custom_handler(self, handler: Callable[[str], str]) -> None:
"""Sets a custom message handler for the Slack app.

Args:
handler (Callable[[str], str]): A custom message handler that
takes a message string as input and returns a response string.
"""
self.custom_handler = handler
48 changes: 48 additions & 0 deletions examples/bots/slack_bot_socket.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
# ========= Copyright 2023-2024 @ CAMEL-AI.org. All Rights Reserved. =========
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# ========= Copyright 2023-2024 @ CAMEL-AI.org. All Rights Reserved. =========

import asyncio

from camel.agents import ChatAgent
from camel.bots.slack.slack_app import SlackApp
from camel.configs import ChatGPTConfig
from camel.models import ModelFactory
from camel.types import ModelPlatformType, ModelType

slack_bot = SlackApp(
token="please input your slack token",
app_token="please input your slack app token",
socket_mode=True,
)

o1_model = ModelFactory.create(
model_platform=ModelPlatformType.OPENAI,
model_type=ModelType.GPT_4O,
model_config_dict=ChatGPTConfig(temperature=0.0).as_dict(),
)
agent = ChatAgent(
system_message="you are a helpful assistant",
message_window_size=10,
model=o1_model,
)


def custom_handler(message: str) -> str:
response = agent.step(message)
return response.msg.content


slack_bot.set_custom_handler(custom_handler)

asyncio.run(slack_bot.start())
5 changes: 5 additions & 0 deletions examples/test/bots/test_slack_app.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
class TestSlackApp(unittest.TestCase):
def setUp(self):
# Temporarily set environment variables for testing
os.environ["SLACK_APP_TOKEN"] = "fake_app_token"
os.environ["SLACK_TOKEN"] = "fake_token"
os.environ["SLACK_SCOPES"] = "channels:read,chat:write"
os.environ["SLACK_SIGNING_SECRET"] = "fake_signing_secret"
Expand All @@ -31,6 +32,7 @@ def tearDown(self):
# Clean up environment variables after the tests, if they exist
for var in [
"SLACK_TOKEN",
"SLACK_APP_TOKEN",
"SLACK_SCOPES",
"SLACK_SIGNING_SECRET",
"SLACK_CLIENT_ID",
Expand All @@ -45,6 +47,7 @@ def test_init_without_token_raises_error(self, mock_async_app):
# to test the ValueError
for var in [
"SLACK_TOKEN",
"SLACK_APP_TOKEN",
"SLACK_SCOPES",
"SLACK_SIGNING_SECRET",
]:
Expand All @@ -58,13 +61,15 @@ def test_init_without_token_raises_error(self, mock_async_app):
def test_init_with_token(self, mock_async_app):
app = SlackApp(
token="fake_token1",
app_token="fake_app_token1",
scopes="channels:read,chat:write,commands",
signing_secret="fake_signing_secret1",
client_id="fake_client_id1",
client_secret="fake_client_secret1",
)
# Assert correct initialization of SlackApp attributes
self.assertEqual(app.token, "fake_token1")
self.assertEqual(app.app_token, "fake_app_token1")
self.assertEqual(app.scopes, "channels:read,chat:write,commands")
self.assertEqual(app.signing_secret, "fake_signing_secret1")
self.assertEqual(app.client_id, "fake_client_id1")
Expand Down
Loading