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

Improve flexibility of Slack connector #4812

Merged
merged 31 commits into from
Feb 26, 2020
Merged

Improve flexibility of Slack connector #4812

merged 31 commits into from
Feb 26, 2020

Conversation

kearnsw
Copy link
Contributor

@kearnsw kearnsw commented Nov 21, 2019

Proposed changes:
By default:

  • Bot responds when mentioned with @ symbol
  • Bot responds when directly messaged

Previously, this functionality was either impossible or difficult for a new user to implement. See full description in #4811

Status (please check what you already did):

  • made PR ready for code review
  • added some tests for the functionality
  • updated the documentation
  • updated the changelog
  • reformat files using black (please check Readme for instructions)

tmbo
tmbo previously requested changes Nov 22, 2019
Copy link
Member

@tmbo tmbo left a comment

Choose a reason for hiding this comment

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

thanks a lot for the slack connector improvement 🚀

I added a couple suggestions and I think we need to take a closer look at that channel name being stored in the input channel.

or is_app_mention(output)
or channel == self.slack_channel
):
self.set_output_channel(channel)
Copy link
Member

Choose a reason for hiding this comment

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

we can't change the channel of the slack input. it is shared between all the async incoming messages so you might change it while another request is also getting processed (this problem might not surface right now, if the asyncs & awaits are at the right )

Copy link
Member

Choose a reason for hiding this comment

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

I think we should remove that object state on the input channel and directly pass it to the output channel

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@tmbo , I am using the metadata object to store this value as well as others related to a specific user request. I think we'll need to explore incorporating the new converation session feature from 1.6.0.

@@ -109,6 +109,20 @@ def _get_text_from_slack_buttons(buttons):
return super(SlackBot, self).api_call("chat.postMessage", **json_message)


def is_app_mention(user_message) -> bool:
Copy link
Member

Choose a reason for hiding this comment

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

please add tests for these as well as type annotations for the parameters & a doc string

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I've added the type annotations and standardized these with the other functions in the SlackInput channel. However, I'm not certain how to proceed with the test cases as the current channel tests for slack do not have a token. Is there a mock testing kit you'd prefer that I use for these?

@erohmensing
Copy link
Contributor

@kearnsw are you still planning to move forward with this PR?

@kearnsw
Copy link
Contributor Author

kearnsw commented Dec 17, 2019

@erohmensing: Yes, I am not intimately familiar with the way channels work so I need to spend some time reviewing that part of the codebase as @tmbo suggests.

@wochinge
Copy link
Contributor

@kearnsw Let us know if you have any questions or need feedback :-)

@kearnsw
Copy link
Contributor Author

kearnsw commented Jan 16, 2020

@kearnsw Let us know if you have any questions or need feedback :-)

There was a typing issue that slipped in when I made the prior updates. I've uploaded the latest after testing everything with pytype and formatted the code with black. @wochinge, can you provide feedback on the updates? I have taken @tmbo's suggestion and am no longer setting the output channel property, and instead using the metadata parameter to store request specific information. The current implementation will leave the output channel configured static, while allowing for async messages based on the request from the sender.

Copy link
Contributor

@wochinge wochinge left a comment

Choose a reason for hiding this comment

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

@kearnsw Thanks for your great work! Left a bit of feedback, but already looks great 🎉

rasa/core/channels/slack.py Outdated Show resolved Hide resolved
rasa/core/channels/slack.py Outdated Show resolved Hide resolved
rasa/core/channels/slack.py Outdated Show resolved Hide resolved
event = slack_event.get("event")

metadata = {}
metadata["out_channel"] = event.get("channel")
Copy link
Contributor

Choose a reason for hiding this comment

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

I just checked https://api.slack.com/types/event and there the channel param is part of item? Would also be awesome to add the link to https://api.slack.com/types/event as a comment in the code

Copy link
Contributor Author

Choose a reason for hiding this comment

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

That's odd. This is the event object that I receive from the slack connector:

{ 'client_msg_id': 'XXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX, 
  'type': 'message', 
  'text': 'hi', 
  'user': 'XXXXXXXXX', 
  'ts': '1579802617.000800', 
  'team': 'XXXXXXXXX', 
  'blocks': [{'type': 'rich_text', 'block_id': 'XXXXX', 'elements': [{'type': 'rich_text_section', 'elements': [{'type': 'text', 'text': 'hi'}]}]}], 
  'channel': 'XXXXXXXXX', 
  'event_ts': '1579802617.000800', 
  'channel_type': 'im'
}

output.get("event").get("user"),
metadata,
)
if (
Copy link
Contributor

Choose a reason for hiding this comment

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

what do we need this condition for?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

This is a holdover from the original codebase. I added the checks for direct message and app mention events. I think the response text should probably be set to "Received message on unsupported channel" or something to that effect instead of a blank response.

Copy link
Contributor

Choose a reason for hiding this comment

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

Ok 👍 But then let's move it up to the condition in line 386 right? you could also write a helper function is_supported_channel(output, metadata) and then have a condition like that:

elif self_is_user_message(output) and self._is_supported_channel(output):
  ... 
else:
   logger.warning(f"Received message on unsupported channel: {metadata["out_channel"]}")

Copy link
Contributor

Choose a reason for hiding this comment

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

I would rather log the "unsupport channel" case instead of returning it as part response to the HTTP request, because Slack is just throwing away this message, while logger.warning(...) will show it to the user.

@@ -311,6 +329,18 @@ def _get_interactive_response(action: Dict) -> Optional[Text]:

return response.text("")

def get_metadata(self, request: Request):
Copy link
Contributor

Choose a reason for hiding this comment

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

Can we please unit test this method?

@kearnsw
Copy link
Contributor Author

kearnsw commented Jan 23, 2020

@wochinge , can you advise on how to unit test the slack connector, since this requires API credentials. I do not see a precedent in the test suite currently. Thank you!

@wochinge
Copy link
Contributor

@kearnsw Which methods do you want to unit test? I suggest targeting the methods individually so you don't have to deal with any actual API request (e.g. is_app_mention can be tested with a dictionary which you pass in). Does that make sense?

@kearnsw
Copy link
Contributor Author

kearnsw commented Jan 23, 2020

@kearnsw Which methods do you want to unit test? I suggest targeting the methods individually so you don't have to deal with any actual API request (e.g. is_app_mention can be tested with a dictionary which you pass in). Does that make sense?

I can proceed that way. I wasn't sure if there would be interest in setting up the CI/CD pipeline with environment variables for testing the Slack interface, in case there are breaking changes to the Slack API. Thanks for the feedback, @wochinge!

@kearnsw kearnsw requested a review from tmbo January 28, 2020 18:49
@tmbo tmbo requested review from wochinge and removed request for tmbo January 28, 2020 19:18
@tmbo tmbo dismissed their stale review January 28, 2020 19:18

outdated

Copy link
Contributor

@wochinge wochinge left a comment

Choose a reason for hiding this comment

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

Thanks for your work and great documentation. Really appreciate it 🎉
Could you please also add a changelog? Thanks!

rasa/core/channels/slack.py Outdated Show resolved Hide resolved
rasa/core/channels/slack.py Outdated Show resolved Hide resolved
request: a `Request` object that contains a slack API event in the body.

Returns:
A `dict` containing the output channel for the response, the text from the user, the sender's ID, and
Copy link
Contributor

Choose a reason for hiding this comment

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

Let's avoid repeating the type in the docstring. Makes it harder to maintain if we ever change the return types. Note that you probably have to reformat my suggestion cause it's too long for a single line.

Suggested change
A `dict` containing the output channel for the response, the text from the user, the sender's ID, and
Metadata extracted from the sent event payload. This includes the output channel for the response, the text from the user and users that have installed the bot.

Copy link
Contributor

Choose a reason for hiding this comment

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

@kearnsw Have you seen that one?

output.get("event").get("user"),
metadata,
)
if (
Copy link
Contributor

Choose a reason for hiding this comment

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

Ok 👍 But then let's move it up to the condition in line 386 right? you could also write a helper function is_supported_channel(output, metadata) and then have a condition like that:

elif self_is_user_message(output) and self._is_supported_channel(output):
  ... 
else:
   logger.warning(f"Received message on unsupported channel: {metadata["out_channel"]}")

output.get("event").get("user"),
metadata,
)
if (
Copy link
Contributor

Choose a reason for hiding this comment

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

I would rather log the "unsupport channel" case instead of returning it as part response to the HTTP request, because Slack is just throwing away this message, while logger.warning(...) will show it to the user.

def get_output_channel(self) -> OutputChannel:
return SlackBot(self.slack_token, self.slack_channel)
def get_output_channel(self, channel: Optional[Text] = None) -> OutputChannel:
if channel is not None:
Copy link
Contributor

Choose a reason for hiding this comment

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

optional: I'd turn it around (if is None else ...) because it's a bit easier to read (you don't have to negate in your head), but that's really just a personal tase

tests/core/test_channels.py Show resolved Hide resolved
@@ -462,6 +462,50 @@ def test_botframework_attachments():
assert ch.add_attachments_to_metadata(payload, metadata) == updated_metadata


def test_slack_metadata():
Copy link
Contributor

Choose a reason for hiding this comment

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

Thanks for adding the test 🚀

rasa/core/channels/slack.py Outdated Show resolved Hide resolved
Co-Authored-By: Tobias Wochinger <mail@tobias-wochinger.de>
@@ -411,7 +411,8 @@ def blueprint(
return slack_webhook

def get_output_channel(self, channel: Optional[Text] = None) -> OutputChannel:
if channel is not None:
channel = channel or self.slack_channel
return SlackBot(self.slack_token, channel)
return SlackBot(self.slack_token, channel)
Copy link
Contributor

Choose a reason for hiding this comment

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

We should still delete the lines after

@kearnsw kearnsw requested a review from wochinge February 5, 2020 00:40
CHANGELOG.rst Outdated
@@ -54,6 +54,11 @@ Features
``intent_tokenization_flag``: Flag to check whether to split intents (default ``False``).
``intent_split_symbol``: Symbol on which intent should be split (default ``_``)

- `#4811 <https://github.com/RasaHQ/rasa/issues/4811>`_: Support invoking a ``SlackBot`` by direct messaging or @ mentions.
Copy link
Contributor

Choose a reason for hiding this comment

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

@kearnsw Please don't edit the Changelog.rst manually. Instead, create a file 4812.improvement.rst in the changelog directory (the process is also described in here https://github.com/RasaHQ/rasa/blob/master/changelog/README.md )

the argument to post DMs to the bot.
- The ``slack_channel`` is a channel that the bot will listen to for messages
in addition to direct messages and app mentions, i.e. "@app_name".
This can be a channel or an individual person.
Copy link
Contributor

Choose a reason for hiding this comment

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

what do you mean by "This can be a channel or an individual person."?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@wochinge, this is from the original documentation. I added the part about "in addition to direct messages and app mentions, i.e. "@app_name". I'll try to clarify this sentence overall.

slack_token="YOUR_SLACK_TOKEN", slack_channel="YOUR_SLACK_CHANNEL"
)

r = Request(None, None, None, None, None, app="test")
Copy link
Contributor

Choose a reason for hiding this comment

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

we recently discovered that this will lead to an error when we upgrade dependencies, let's rather do:

Suggested change
r = Request(None, None, None, None, None, app="test")
r = Mock()
r.json = direct_message_event

@kearnsw
Copy link
Contributor Author

kearnsw commented Feb 11, 2020

The build is failing on tests unrelated to the modified code. Can you advise @wochinge ?

@wochinge
Copy link
Contributor

Hi @kearnsw ,

can you please rebase to / merge in master ? We switched to GitHub actions which are more reliable and also faster.

CHANGELOG.rst Outdated
@@ -1,8 +1,8 @@
:desc: Rasa Open Source Changelog
:desc: Rasa OSS Changelog
Copy link
Contributor

Choose a reason for hiding this comment

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

can you please undo this change?

CHANGELOG.rst Outdated


Rasa Open Source Change Log
===========================
Rasa OSS Change Log
Copy link
Contributor

Choose a reason for hiding this comment

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

can you please undo this change?

CHANGELOG.rst Outdated
@@ -17,89 +17,6 @@ This project adheres to `Semantic Versioning`_ starting with version 1.0.

.. towncrier release notes start

[1.7.0] - 2020-01-29
Copy link
Contributor

Choose a reason for hiding this comment

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

can you please undo this change?

@@ -0,0 +1 @@
Support invoking a ``SlackBot`` by direct messaging or @ mentions.
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
Support invoking a ``SlackBot`` by direct messaging or @ mentions.
Support invoking a ``SlackBot`` by direct messaging or ``@<app name>`` mentions.

rasa/core/channels/slack.py Show resolved Hide resolved
event = slack_event.get("event", {})
return {
"out_channel": event.get("channel"),
"text": event.get("text"),
Copy link
Contributor

Choose a reason for hiding this comment

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

it feels a bit weird that the metadata also returns sender and text. The metadata is meant to provide additional information around a message. text and sender are the message. So in my opinion it would be a bit cleaner to keep this separated, what you think?

@kearnsw
Copy link
Contributor Author

kearnsw commented Feb 21, 2020

@wochinge, looks like the build environment errored out.

CHANGELOG.rst Outdated Show resolved Hide resolved
kearnsw and others added 2 commits February 24, 2020 07:01
Co-Authored-By: Tobias Wochinger <mail@tobias-wochinger.de>
@wochinge
Copy link
Contributor

@kearnsw Can you please update with the latest master again?

@wochinge
Copy link
Contributor

@kearnsw I'm sorry, but you have to merge in the latest master state again 🙈

@kearnsw
Copy link
Contributor Author

kearnsw commented Feb 26, 2020

Hi @wochinge , I’m not understanding the errors for the past few merge builds. Are these backend issues or problems with the submitted code?

@tmbo
Copy link
Member

tmbo commented Feb 26, 2020

Looks all good 👍

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants