processors/slack.py (208 lines of code) (raw):

# Copyright 2023 Google LLC # # 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. from .base import Processor, NotConfiguredException from helpers.base import get_user_agent import requests import json import base64 class SlackProcessor(Processor): """ Slack processor for fetching messages. Args: token (str): A Slack Bot User OAuth Token. api (str): One of: conversations.list, conversations.history, conversations.replies mode (str, optional: api, processMessages or lastImage (default api) multimodal (bool, optional): Use multi-modal processing in processMessages. messages (list, optional): List of messages to process. appId (str, optional): The app ID to detect bot messages. prompt (str, optional): Initial message to append to the beginning of the conversation. request (dict): The API call body. """ def get_default_config_key(): return 'slack' def call_slack(self, api, token, request, urlencoded=False): self.logger.info('Calling Slack API: %s' % (api), extra={ "slack_api": api, }) api_path = 'https://slack.com/api/%s' % (api) if urlencoded: request_body = request else: request_body = json.dumps(request) headers = { 'User-Agent': get_user_agent(), 'Content-type': 'application/json; charset=utf-8', 'Authorization': 'Bearer %s' % (token) } if urlencoded: headers['Content-type'] = 'application/x-www-form-urlencoded' response = requests.post(api_path, data=request_body, headers=headers) response.raise_for_status() response_json = response.json() return response_json def download_slack(self, url, token): self.logger.info('Downloading from Slack: %s' % (url), extra={ "slack_url": url, }) headers = { 'User-Agent': get_user_agent(), 'Authorization': 'Bearer %s' % (token) } response = requests.get(url, headers=headers) response.raise_for_status() return base64.b64encode(response.content).decode('ascii') def _slack_message_to_parts(self, message, token, multi_modal, no_question): parts = [] if 'files' in message and multi_modal: for file in message['files']: if file['size'] >= 20971520: # 20MB limit self.logger.debug( 'Attachment too large to download from Slack (%d bytes).' % (file['size']), extra={'file': file}) continue if file['mimetype'][0:6] == 'image/': self.logger.info( 'Downloaded Slack image (720p version): %s (mime %s)' % (file['thumb_720'], file['mimetype']), extra={'mimetype': file['mimetype']}) if 'thumb_720' in file: parts.append({ 'inlineData': { 'mimeType': file['mimetype'], 'data': self.download_slack(file['thumb_720'], token) } }) elif 'url_private_download' in file: self.logger.info( 'Downloaded Slack image (full version): %s (mime %s)' % (file['url_private_download'], file['mimetype']), extra={'mimetype': file['mimetype']}) parts.append({ 'inlineData': { 'mimeType': file['mimetype'], 'data': self.download_slack( file['url_private_download'], token) } }) elif 'url_private_download' in file: self.logger.info( 'Downloaded Slack file: %s (mime %s)' % (file['url_private_download'], file['mimetype']), extra={'mimetype': file['mimetype']}) parts.append({ 'inlineData': { 'mimeType': file['mimetype'], 'data': self.download_slack( file['url_private_download'], token) } }) if 'text' in message and message['text'] != '': parts.append({'text': message['text']}) elif no_question != '': parts.append({'text': no_question}) return parts def process(self, output_var='slack'): if 'api' not in self.config and 'mode' not in self.config: raise NotConfiguredException('No Slack API call specified.') if 'token' not in self.config: raise NotConfiguredException('No Slack token specified.') if 'request' not in self.config: self.config['request'] = {} mode = self._jinja_expand_string( self.config['mode'], 'mode') if 'mode' in self.config else 'api' token = self._jinja_expand_string(self.config['token'], 'token') slack_api = None if mode == 'api': slack_api = self._jinja_expand_string(self.config['api'], 'api') request_params = self._jinja_expand_dict_all(self.config['request'], 'request') mode = self._jinja_expand_string( self.config['mode'], 'mode') if 'mode' in self.config else 'api' if mode == 'processMessages' or mode == 'lastImage': if 'messages' not in self.config: raise NotConfiguredException('No Slack messages specified.') if 'appId' not in self.config: raise NotConfiguredException('No Slack app ID specified.') app_id = self._jinja_expand_string(self.config['appId'], 'app_id') multi_modal = self._jinja_expand_bool( self.config['multimodal'], 'multimodal') if 'multimodal' in self.config else False messages = self._jinja_expand_expr(self.config['messages'], 'messages') no_question = self._jinja_expand_string( self.config['noQuestionPrompt'], 'no_question_prompt' ) if 'noQuestionPrompt' in self.config else "Answer the question in this audio clip or image." processed = [] if 'messages' in messages: messages = messages['messages'] for message in messages: new_message = None if 'app_id' in message: if message['app_id'] == app_id: parts = self._slack_message_to_parts( message, token, multi_modal, no_question) if len(parts) > 0: new_message = {'role': 'MODEL', 'parts': parts} else: parts = self._slack_message_to_parts( message, token, multi_modal, no_question) if len(parts) > 0: new_message = {'role': 'USER', 'parts': parts} if new_message: processed.append(new_message) # Prepend an initial prompt that can be instructions or such if 'prompt' in self.config: prompt_added = False for msg_idx, msg in enumerate(processed): if msg['role'] == 'USER': for part_idx, part in enumerate(msg['parts']): if 'text' in part: processed[msg_idx]['parts'][part_idx][ 'text'] = self._jinja_expand_string( self.config['prompt'], 'prompt') + " " + processed[msg_idx][ 'parts'][part_idx]['text'] prompt_added = True break if prompt_added: break if mode == 'lastImage': if len(processed) > 0: last_message = processed[len(processed) - 1] for part in last_message['parts']: if 'inlineData' in part: if part['inlineData']['mimeType'][0:6] == 'image/': return {output_var: part['inlineData']['data']} return {output_var: None} return {output_var: processed} if slack_api == 'conversations.list': slack_response = self.call_slack(self.config['api'], token, request_params, True) self.logger.debug('Slack API %s responded.' % (slack_api), extra={ 'slack_request': request_params, 'slack_response': slack_response }) return { output_var: slack_response, } if slack_api == 'conversations.history': slack_response = self.call_slack(self.config['api'], token, request_params) self.logger.debug('Slack API %s responded.' % (slack_api), extra={ 'slack_request': request_params, 'slack_response': slack_response }) return { output_var: slack_response, } if slack_api == 'conversations.replies': slack_response = self.call_slack(self.config['api'], token, request_params, True) self.logger.debug('Slack API %s responded.' % (slack_api), extra={ 'slack_request': request_params, 'slack_response': slack_response }) return { output_var: slack_response, } return { output_var: None, }