diff --git a/changelog/10086.misc.md b/changelog/10086.misc.md new file mode 100644 index 000000000000..11274b45cb86 --- /dev/null +++ b/changelog/10086.misc.md @@ -0,0 +1 @@ +A new additional slack options `conversation_granularity` is now available that allows to choose whether interactions across all channels ("sender"), within a channel ("channel") or only within a thread ("thread") will be treated as a single conversation. \ No newline at end of file diff --git a/docs/docs/connectors/slack.mdx b/docs/docs/connectors/slack.mdx index 2766e16f7a89..53c8d013612d 100644 --- a/docs/docs/connectors/slack.mdx +++ b/docs/docs/connectors/slack.mdx @@ -164,6 +164,7 @@ slack: slack_retry_number_header: "x-slack-retry-num" # Slack HTTP header name indicating the attempt number. This configuration is optional. errors_ignore_retry: None # Any error codes given by Slack included in this list will be ignored. Error codes are listed [here](https://api.slack.com/events-api#errors). use_threads: False # If set to True, bot responses will appear as a threaded message in Slack. This configuration is optional and set to False by default. + conversation_granularity: "sender" # sender allows 1 conversation per user (across channels), channel allows 1 conversation per user per channel, thread allows 1 conversation per user per thread. This configuration is optional and set to sender by default. ``` Make sure to restart your Rasa X or Rasa Open Source server after changing the diff --git a/rasa/core/channels/slack.py b/rasa/core/channels/slack.py index 2cd7bfa41996..4a8570368d89 100644 --- a/rasa/core/channels/slack.py +++ b/rasa/core/channels/slack.py @@ -139,6 +139,7 @@ def from_credentials(cls, credentials: Optional[Dict[Text, Any]]) -> InputChanne credentials.get("errors_ignore_retry", None), credentials.get("use_threads", False), credentials.get("slack_signing_secret", ""), + credentials.get("conversation_granularity", "sender"), ) def __init__( @@ -151,6 +152,7 @@ def __init__( errors_ignore_retry: Optional[List[Text]] = None, use_threads: Optional[bool] = False, slack_signing_secret: Text = "", + conversation_granularity: Optional[Text] = "sender", ) -> None: """Create a Slack input channel. @@ -182,7 +184,10 @@ def __init__( slack_signing_secret: Slack creates a unique string for your app and shares it with you. This allows us to verify requests from Slack with confidence by verifying signatures using your signing secret. - + conversation_granularity: conversation granularity for slack conversations. + sender allows 1 conversation per user (across channels) + channel allows 1 conversation per user per channel + thread allows 1 conversation per user per thread """ self.slack_token = slack_token self.slack_channel = slack_channel @@ -192,6 +197,7 @@ def __init__( self.retry_num_header = slack_retry_number_header self.use_threads = use_threads self.slack_signing_secret = slack_signing_secret + self.conversation_granularity = conversation_granularity self._validate_credentials() @@ -506,6 +512,11 @@ async def webhook(request: Request) -> HTTPResponse: user_message = event.get("text", "") sender_id = event.get("user", "") metadata = self.get_metadata(request) + channel_id = metadata.get("out_channel") + thread_id = metadata.get("thread_id") + conversation_id = self._get_conversation_id( + sender_id, channel_id, thread_id + ) if "challenge" in output: return response.json(output.get("challenge")) @@ -528,7 +539,7 @@ async def webhook(request: Request) -> HTTPResponse: request, on_new_message, text=self._sanitize_user_message(user_message, metadata["users"]), - sender_id=sender_id, + sender_id=conversation_id, metadata=metadata, ) elif content_type == "application/x-www-form-urlencoded": @@ -541,8 +552,13 @@ async def webhook(request: Request) -> HTTPResponse: text = self._get_interactive_response(payload["actions"][0]) if text is not None: metadata = self.get_metadata(request) + channel_id = metadata.get("out_channel") + thread_id = metadata.get("thread_id") + conversation_id = self._get_conversation_id( + sender_id, channel_id, thread_id + ) return await self.process_message( - request, on_new_message, text, sender_id, metadata + request, on_new_message, text, conversation_id, metadata ) if payload["actions"][0]["type"] == "button": # link buttons don't have "value", don't send their clicks to @@ -557,6 +573,24 @@ async def webhook(request: Request) -> HTTPResponse: return slack_webhook + def _get_conversation_id( + self, + sender_id: Optional[Text], + channel_id: Optional[Text], + thread_id: Optional[Text], + ) -> Optional[Text]: + conversation_id = sender_id + if self.conversation_granularity == "channel" and sender_id and channel_id: + conversation_id = sender_id + "_" + channel_id + if ( + self.conversation_granularity == "thread" + and sender_id + and channel_id + and thread_id + ): + conversation_id = sender_id + "_" + channel_id + "_" + thread_id + return conversation_id + def _is_supported_channel(self, slack_event: Dict, metadata: Dict) -> bool: return ( self._is_direct_message(slack_event) diff --git a/tests/core/channels/test_slack.py b/tests/core/channels/test_slack.py index e9cdb3e8d232..4afcc8b5c2d9 100644 --- a/tests/core/channels/test_slack.py +++ b/tests/core/channels/test_slack.py @@ -312,15 +312,85 @@ def test_slack_init_token_channel_parameters(): ch = SlackInput("xoxb-test", "test", slack_signing_secret="foobar") assert ch.slack_token == "xoxb-test" assert ch.slack_channel == "test" + assert ch.conversation_granularity == "sender" + + +def test_slack_init_token_channel_conversation_granularity_parameters(): + ch = SlackInput( + "xoxb-test", + "test", + slack_signing_secret="foobar", + conversation_granularity="channel", + ) + assert ch.slack_token == "xoxb-test" + assert ch.slack_channel == "test" + assert ch.conversation_granularity == "channel" def test_slack_init_token_channel_threads_parameters(): ch = SlackInput( - "xoxb-test", "test", slack_signing_secret="foobar", use_threads=True + "xoxb-test", + "test", + slack_signing_secret="foobar", + use_threads=True, + conversation_granularity="thread", ) assert ch.slack_token == "xoxb-test" assert ch.slack_channel == "test" assert ch.use_threads is True + assert ch.conversation_granularity == "thread" + + +def test_get_conversation_id_sender_id(): + ch = SlackInput( + "xoxb-test", + "test", + slack_signing_secret="foobar", + use_threads=True, + conversation_granularity="sender", + ) + conversation_id = ch._get_conversation_id( + "test_sender_id", "test_channel_id", "test_thread_id" + ) + assert conversation_id == "test_sender_id" + + +def test_get_conversation_id_channel_id(): + ch = SlackInput( + "xoxb-test", + "test", + slack_signing_secret="foobar", + use_threads=True, + conversation_granularity="channel", + ) + conversation_id = ch._get_conversation_id("test_sender_id", "test_channel_id", None) + assert conversation_id == "test_sender_id_test_channel_id" + + conversation_id = ch._get_conversation_id("test_sender_id", None, "test_thread_id") + assert conversation_id == "test_sender_id" + + +def test_get_conversation_id_thread_id(): + ch = SlackInput( + "xoxb-test", + "test", + slack_signing_secret="foobar", + use_threads=True, + conversation_granularity="thread", + ) + conversation_id = ch._get_conversation_id( + "test_sender_id", "test_channel_id", "test_thread_id" + ) + assert conversation_id == "test_sender_id_test_channel_id_test_thread_id" + + conversation_id = ch._get_conversation_id("test_sender_id", None, "test_thread_id") + assert conversation_id == "test_sender_id" + + conversation_id = ch._get_conversation_id("test_sender_id", "test_channel_id", None) + assert conversation_id == "test_sender_id" + + conversation_id = ch._get_conversation_id("test_sender_id", None, None) + assert conversation_id == "test_sender_id" def test_is_slack_message_none():