def df2_get(self, args, f_type) -> any: """ Return value stored in user variable :param args: :param f_type: :return: """ try: var_name = self.bot.resolve_arg(args[0], f_type, True) except IndexError: raise BBotException({ 'code': 120, 'function': 'get', 'arg': 0, 'message': 'Variable name missing.' }) if type(var_name) is not str: raise BBotException({ 'code': 121, 'function': 'get', 'arg': 0, 'message': 'Variable name should be a string.' }) return self.bot.session.get_var(self.bot.user_id, var_name)
def df2_lte(self, args, f_type): """ Evaluates lesser-than or equal :param args: :param f_type: :return: """ try: value1 = self.bot.resolve_arg(args[0], f_type) tmp = float(value1) except IndexError: raise BBotException({ 'code': 150, 'function': 'lte', 'arg': 0, 'message': 'Value in arg 0 is missing.' }) try: value2 = self.bot.resolve_arg(args[1], f_type) except IndexError: raise BBotException({ 'code': 151, 'function': 'lte', 'arg': 1, 'message': 'Value in arg 1 is missing.' }) return value1 <= value2
def suggestedActions(self, args, f_type): errors = [] try: actions = self.core.resolve_arg(args[0], f_type) except IndexError: errors.append({ 'code': 240, 'function': 'suggestedAction', 'arg': 0, 'message': 'suggestedAction actions missing.' }) if errors: raise BBotException(errors) if type(actions) is not list: actions = [actions] for i in range(len(actions)): if type( actions[i] ) is not box.Box: # if it's not an object (dict are box.Box class here) if type(actions[i] ) is str: # and it's a string, apply it as imBack actions[i] = self.imBack(actions[i]) else: raise BBotException( "suggestedActions function accepts strings or imBack objects only" ) bbot_response = {'suggestedActions': {'actions': actions}} self.core.add_output(bbot_response, True) # Adds suggestion to last output
def df2_eq(self, args, f_type): """ Evaluates equivalency between two values :return: """ try: value1 = self.bot.resolve_arg(args[0], f_type) except IndexError: raise BBotException({ 'code': 140, 'function': 'eq', 'arg': 0, 'message': 'Value in arg 0 is missing.' }) try: value2 = self.bot.resolve_arg(args[1], f_type) except IndexError: raise BBotException({ 'code': 141, 'function': 'eq', 'arg': 0, 'message': 'Value in arg 1 is missing.' }) return value1 == value2
def df2_gt(self, args, f_type): """ Evaluates greater-than :param args: :param f_type: :return: """ try: value1 = self.bot.resolve_arg(args[0], f_type) except IndexError: raise BBotException({ 'code': 150, 'function': 'gt', 'arg': 0, 'message': 'Value in arg 0 is missing.' }) try: value2 = self.bot.resolve_arg(args[1], f_type) except IndexError: raise BBotException({ 'code': 151, 'function': 'gt', 'arg': 1, 'message': 'Value in arg 1 is missing.' }) return value1 > value2
def df2_or(self, args, f_type): """ Logical operator OR. It accepts unlimited values :param args: :param f_type: :return: """ if len(args) == 0: raise BBotException({ 'code': 170, 'function': 'or', 'arg': 0, 'message': 'Value in arg 0 is missing.' }) if len(args) == 1: raise BBotException({ 'code': 171, 'function': 'or', 'arg': 1, 'message': 'Value in arg 1 is missing.' }) for arg in args: resolved_arg = self.bot.resolve_arg(arg, f_type) if resolved_arg: return True return False
def df2_code(self, args, f_type): """ This function is used to run any python code on conditions and responses objects. Of course you can use DotFlow2 functions inside it :param args: 0: string with python code 1: string. 'C' to get expression result (only expressions works). 'R' to run any kind of python code (no result is returned) :return: """ try: code = self.bot.resolve_arg(args[0], f_type) except IndexError: raise BBotException({ 'code': 130, 'function': 'code', 'arg': 0, 'message': 'Code missing.' }) if type(code) is not str: raise BBotException({ 'code': 131, 'function': 'code', 'arg': 0, 'message': 'Argument 0 should be string' }) self.logger.debug('$code: Running python code: "' + code + "'...") codeblock = self._get_codeblock(code, f_type) self.logger.debug('$code: Running template code block: "' + codeblock + "'...") response = self.bot.template_engine.render(codeblock) response = self._get_boolean(response, f_type) self.logger.debug('$code: Returning: ' + str(response)) return response
def chatscriptMatch(self, args, f_type): """ Evaluates ChatScript pattern @TODO add caching :param args: :param f_type: :return: """ try: pattern = self.bot.resolve_arg(args[0], f_type) except IndexError: raise BBotException({ 'code': 190, 'function': 'chatscriptMatch', 'arg': 0, 'message': 'Pattern in arg 0 is missing.' }) try: input_text = self.bot.resolve_arg(args[1], f_type) except IndexError: raise BBotException({ 'code': 191, 'function': 'chatscriptMatch', 'arg': 1, 'message': 'Text in arg 1 is missing.' }) try: entities_var_names = self.bot.resolve_arg(args[2], f_type) except IndexError: entities_var_names = [] # entities are optional result = False if len(input_text) > 0: # clear match variables first (ChatScript does not reset them when running testpattern) self.send(':do ^clearmatch()') # test the pattern cs_req = f":testpattern ({pattern}) {input_text}" #@TODO try sending direct text and running ^match later (it's faster. sends input text once) self.logger.debug("ChatScript request: " + cs_req) cs_res = self.send(cs_req) self.logger.debug('ChatScript response: \n' + str(cs_res)) if not self.has_error(cs_res): result = self.is_match(cs_res) if result: self.logger.info('It\'s a match!') else: self.logger.info('No match') # check if there are match variables set if self.has_match_variables(cs_res): self.store_variables_from_matched_variables( entities_var_names) else: self.logger.warning('Response returned with error') return result
def get_server_type(self): if self.dotbot.chatbot_engine['serverUrl'].startswith('http'): return 'rest' elif self.dotbot.chatbot_engine['serverUrl'].startswith('ws'): return 'socketio' else: raise BBotException('Wrong Rasa Server url')
def authenticate(self): # first check if we have access_token in database self.logger.debug("Looking for Azure AD access token in database...") stored_token = self.dotdb.get_azure_ad_access_token(self.dotbot.bot_id) if stored_token: expire_date = stored_token['expire_date'] if expire_date >= datetime.datetime.utcnow(): # got valid token self.access_token = stored_token['access_token'] self.logger.debug('Got valid token from db. Will expire in ' + str(stored_token['expire_date'])) return else: self.logger.debug('Got expired token. Will request new one') else: self.logger.debug('There is no token in database. Will request one') url = "https://login.microsoftonline.com/botframework.com/oauth2/v2.0/token" payload = { "grant_type": "client_credentials", "client_id": self.app_id, "client_secret": self.app_password, "scope": "https://api.botframework.com/.default" } self.logger.debug("Sending request to Microsoft OAuth with payload: " + str(payload)) response = requests.post(url, data=payload) msg = "Response from Microsoft OAuth: http code: " + str(response.status_code) + " message: " + str(response.text) if response.status_code != 200: raise BBotException(msg) self.logger.debug(msg) json_response = response.json() self.access_token = json_response['access_token'] expire_date = datetime.datetime.utcnow() + datetime.timedelta(0, json_response['expires_in']) # now plus x seconds to get expire date self.dotdb.set_azure_ad_access_token(self.dotbot.bot_id, self.access_token, expire_date)
def imBack(self, args, f_type): """ Returns BBot imBack object (must be used within suggestedAction) :param args: :return: """ errors = [] try: title = self.core.resolve_arg(args[0], f_type) except IndexError: errors.append({'code': 240, 'function': 'imBack', 'arg': 0, 'message': 'imBack title missing.'}) try: value = self.core.resolve_arg(args[1], f_type) except IndexError: value = title if errors: raise BBotException(errors) bbot_response = { "type": "Action.Submit", "title": title, "data": { "imBack": value } } return bbot_response
def openUrl(self, args, f_type): """ Returns BBot openUrl object (must be used within suggestedAction) :param args: :return: """ errors = [] try: title = self.core.resolve_arg(args[0], f_type) except IndexError: errors.append({ 'code': 240, 'function': 'openUrl', 'arg': 0, 'message': 'openUrl title missing.' }) try: value = self.core.resolve_arg(args[1], f_type) except IndexError: value = title if errors: raise BBotException(errors) response = {"type": "openUrl", "title": title, "value": value} return response
def df2_text(self, args, f_type): """ Returns BBot text output object :param args: :return: """ if len(args) == 0: raise BBotException({ 'code': 200, 'function': 'text', 'arg': 0, 'message': 'Text in arg 0 is missing.' }) msg_count = len(args) if msg_count > 1: # multiple output, choose a random one msg_idx = random.randint(0, msg_count - 1) else: msg_idx = 0 msg = self.bot.resolve_arg(args[msg_idx], f_type, True) # no need to resolve arg before this bbot_response = {'text': str(msg)} self.bot.add_output(bbot_response) return bbot_response
def df2_regexMatch(self, args, f_type): """ Evaluates regular expression. Supports named groups :param args: :param f_type: :return: """ try: pattern = self.bot.resolve_arg(args[0], f_type) except IndexError: raise BBotException({ 'code': 180, 'function': 'regexMatch', 'arg': 0, 'message': 'Pattern in arg 1 is missing.' }) try: text = self.bot.resolve_arg(args[1], f_type) except IndexError: raise BBotException({ 'code': 181, 'function': 'regexMatch', 'arg': 1, 'message': 'Text in arg 1 is missing.' }) m = re.search(pattern, text) if m: # look for named groups and store them for var_name, var_value in m.groupdict().items(): if var_value is not None: self.bot.session.set_var(self.bot.user_id, var_name, var_value) self.bot.detected_entities[var_name] = var_value self.logger.debug('$regexMatch: Storing named group "' + var_name + '" with value "' + var_value + '"') else: self.logger.debug('$regexMatch: Named group "' + var_name + '" not found.') return True else: return False
def _mediaCard(self, args, f_type, media_elm: str = "media"): """ Returns BBot video object :param args: :param f_type: :return: """ try: media_url = self.core.resolve_arg(args[0], f_type, True) except IndexError: raise BBotException({ 'code': 210, 'function': 'mediaCard', 'arg': 0, 'message': 'Media URL is missing.' }) try: title = self.core.resolve_arg(args[1], f_type, True) except IndexError: title = "" try: subtitle = self.core.resolve_arg(args[2], f_type, True) except IndexError: subtitle = "" try: text = self.core.resolve_arg(args[3], f_type, True) except IndexError: text = "" try: buttons = self.core.resolve_arg(args[4], f_type, True) except IndexError: buttons = [] try: image_url = self.core.resolve_arg(args[5], f_type, True) except IndexError: image_url = "" bbot_response = { 'type': 'message', 'attachments': [{ "contentType": "", "content": { "subtitle": subtitle, "text": text, "image": image_url, "title": title, media_elm: [{ "url": media_url }], "buttons": buttons } }] } return bbot_response
def heroCard(self, args, f_type): """ Returns BBot hero card :param args: :return: """ errors = [] try: image = self.core.resolve_arg(args[0], f_type) except IndexError: errors.append({ 'code': 240, 'function': 'heroCard', 'arg': 0, 'message': 'heroCard image missing.' }) try: title = self.core.resolve_arg(args[1], f_type) except IndexError: title = None try: subtitle = self.core.resolve_arg(args[2], f_type, True) except IndexError: subtitle = None try: text = self.core.resolve_arg(args[3], f_type, True) except IndexError: text = None try: buttons = self.core.resolve_arg(args[4], f_type) except IndexError: buttons = None if errors: raise BBotException(errors) content = {} content['images'] = [{'url': image}] if title: content['title'] = title if subtitle: content['subtitle'] = subtitle if text: content['text'] = text if buttons: content['buttons'] = buttons bbot_response = { 'type': 'message', 'attachments': [{ "contentType": "application/vnd.microsoft.card.hero", "content": content }] } self.core.add_output(bbot_response)
def df2_weather(self, args, f_type): """ Returns weather report @TODO return forecast based on args[1] date :param args: :param f_type: :return: """ try: location = self.bot.resolve_arg(args[0], f_type) except IndexError: raise BBotException({ 'code': 0, 'function': 'sendEmail', 'arg': 0, 'message': 'Location is missing.' }) try: date = args[1] except: date = 'today' # optional. default 'today' self.logger.debug(f'Retrieving weather for {location}') st = self.search_text(location) if not st: self.logger.debug("Location not found. Invalid location") return { 'text': '<No weather data or invalid location>', #@TODO should raise a custom exception which will be used for flow exceptions 'canonicalLocation': location } location_key = st[0].get('Key', None) self.logger.debug("Accuweather Location Key: " + location_key) canonical_location = st[0]['LocalizedName'] + ', ' + st[0]['Country'][ 'LocalizedName'] self.logger.debug("Canonical location: " + canonical_location) r = requests.get( f'http://dataservice.accuweather.com/currentconditions/v1/{location_key}?apikey={self.accuweather_api_key}&details=false' ) if r.status_code == 200: aw = r.json() return { 'text': aw[0]['WeatherText'], 'canonicalLocation': canonical_location } self.logger.error(r.text)
def directline_get_new_conversation_id(self): url = self.base_url + '/conversations' self.logger.debug('DirectLine requesting new conversation id') response = requests.post(url, headers=self.directline_get_headers()) self.logger.debug('DirectLine response: ' + response.text) if response.status_code == 201 or response.status_code == 200: jsonresponse = response.json() return jsonresponse['conversationId'] raise BBotException('Response error code: ' + str(response.status_code))
def rest_server(self, msg): server_url = self.dotbot.chatbot_engine['serverUrl'] self.logger.debug('Querying to Rasa Server ' + server_url) params = {"sender": self.user_id, "message": msg} r = requests.post(server_url + '/webhooks/rest/webhook', json=params) self.logger.debug('Rasa Server response code: ' + str(r.status_code) + ' - message: ' + str(r.text)[0:300]) if r.status_code == 200: aw = r.json() else: raise BBotException(r.text) return aw
def render(self, string: str) -> str: """ Renders any string with configured bbot custom functions and bot session vars """ # We still need a way to know if a string is a template or not, but Templator don't need enclosing # So for Templator, just enclose the whole string with {{ }} for BBot to know it is a template if re.search('({%).*(%})|({{.*}})', string) is None: self.logger.debug('Nothing to render') return string string = string.replace('{{', '') string = string.replace('}}', '') string = string.replace('{%', '') string = string.replace('%}', '') session_vars = {} if hasattr(self.core.bot, 'session'): session_vars = self.core.bot.session.get_var(self.core.bot.user_id) t_globals = session_vars # add predefined vars from publishers if hasattr(self.dotbot, 'botsubscription'): if hasattr(self.dotbot.botsubscription, 'predefined_vars'): t_globals = { **t_globals, **self.dotbot.botsubscription.predefined_vars } # get custom functions from extensions c_functions = self.get_functions() t_globals = {**t_globals, **c_functions} self.logger.debug('Rendering template: "' + str(string) + '"') templator_obj = Template(string, globals=t_globals) try: response = str(templator_obj()) except NameError as e: err_msg = 'Template error: ' + str(e) self.core.logger.debug(err_msg) raise BBotException({'message': err_msg}) if response[ -1:] == '\n': # Templator seems to add a trailing \n, remove it response = response[:-1] self.logger.debug('Template response: "' + response + '"') if response.find('<function BBotFunctionsProxy') is not -1: self.logger.error( 'Templator returned an invalid response. Botdev forgot to escape $?' ) response = '<TEMPLATE RENDERING ERROR. CHECK DEBUG DATA>' # @TODO add debug data in context to the instruction executed return response
def atalk(self, input_txt: str): params = { 'botkey': self.botkey, 'input': input_txt, } response = requests.post(self.url + '/atalk', params) self.logger.debug("Response status code: " + str(response.status_code)) if response.status_code == 200: return response.json() elif response.status_code == 401: raise BBotException(response.text) else: raise Exception(response.text)
def imageCard(self, args, f_type): try: image = self.core.resolve_arg(args[0], f_type, True) except IndexError: raise BBotException({'code': 210, 'function': 'imageCard', 'arg': 0, 'message': 'imageCard image is missing.'}) try: title = self.core.resolve_arg(args[1], f_type, True) except IndexError: title = "" try: buttons = self.core.resolve_arg(args[2], f_type, True) except IndexError: buttons = [] self.heroCard([image, title, buttons], 'R')
def df2_set(self, args, f_type): """ Stores values on user variables. :param args: :param f_type: :return: """ try: var_name = self.bot.resolve_arg(args[0], f_type, True) except IndexError: raise BBotException({ 'code': 110, 'function': 'set', 'arg': 0, 'message': 'Variable name missing.' }) if type(var_name) is not str: raise BBotException({ 'code': 111, 'function': 'set', 'arg': 0, 'message': 'Variable name should be a string.' }) try: var_value = self.bot.resolve_arg(args[1], f_type, True) except IndexError: raise BBotException({ 'code': 112, 'function': 'set', 'arg': 1, 'message': 'Variable value missing.' }) self.bot.session.set_var(self.bot.user_id, var_name, var_value)
def to_botframework(self, bbot_response): response_payload = copy.deepcopy(self.response_payload) for br in bbot_response['output']: r = {**response_payload, **br} self.logger.debug("Response sent back to BotFramework: " + str(r)) url = self.service_url + 'v3/conversations/' + r['conversation']['id'] + '/activities/' + r['id'] self.logger.debug("To url: " + url) response = requests.post(url, headers=self.directline_get_headers(), json=r) msg = "BotFramework response: http code: " + str(response.status_code) + " message: " + str(response.text) if response.status_code != 200: raise BBotException(msg) self.logger.debug(msg)
def talk(self, input_txt: str, sessionid: str, client_name: str): params = { 'botkey': self.botkey, 'input': input_txt, 'extra': True, 'sessionid': sessionid, 'client_name': client_name } response = requests.post(self.url + '/talk', params) self.logger.debug("Response status code: " + str(response.status_code)) if response.status_code == 200: return response.json() elif response.status_code == 401: raise BBotException(response.text) else: raise Exception(response.text)
def suggestedAction(self, args, f_type): errors = [] try: actions = self.core.resolve_arg(args[0], f_type) except IndexError: errors.append({'code': 240, 'function': 'suggestedAction', 'arg': 0, 'message': 'suggestedAction actions missing.'}) if errors: raise BBotException(errors) bbot_response = { "contentType": "application/vnd.microsoft.card.adaptive", "content": { "$schema": "https://adaptivecards.io/schemas/adaptive-card.json", "type": "AdaptiveCard", "version": "1.0", "actions": actions } } self.core.add_output(bbot_response)
def df2_button(self, args, f_type): """ Returns BBot button output object :param args: :return: """ errors = [] try: button_id = self.bot.resolve_arg(args[0], f_type) except IndexError: errors.append({ 'code': 240, 'function': 'button', 'arg': 0, 'message': 'Button ID missing.' }) try: text = self.bot.resolve_arg(args[1], f_type) except IndexError: errors.append({ 'code': 241, 'function': 'button', 'arg': 1, 'message': 'Button text missing.' }) try: postback = self.bot.resolve_arg(args[2], f_type) except IndexError: postback = None # postback is optional #@TODO revisit this. buttonId is not a good idea after all if errors: raise BBotException(errors) bbot_response = {'button': {'id': button_id, 'text': text}} if postback: bbot_response['button']['postback'] = postback self.bot.add_output(bbot_response) return bbot_response
def df2_image(self, args, f_type): """ Returns BBot image output object :param args: :param f_type: :return: """ try: image_url = self.bot.resolve_arg(args[0], f_type, True) except IndexError: raise BBotException({ 'code': 210, 'function': 'image', 'arg': 0, 'message': 'Image URL in arg 0 is missing.' }) bbot_response = {'image': {'url': image_url}} self.bot.add_output(bbot_response) return bbot_response
def df2_goto(self, args, f_type): # @TODO if target node of the goto does not need to wait for user input, it should execute it directly. If botdev doesn't want this, they should use $gotoAndWait """ Sets internal follow-up context. :param args: 0: Node ID where to go next :param f_type: :return: """ # Existence of node id should be done previously. Bot engine will raise an exception if can't find it try: node_id = self.bot.resolve_arg(args[0], f_type) except IndexError: raise BBotException({ 'code': 100, 'function': 'goto', 'arg': 0, 'message': 'Node ID missing.' }) self.bot.set_followup_context(node_id)
def df2_video(self, args, f_type): """ Returns BBot video object :param args: :param f_type: :return: """ try: video_url = self.bot.resolve_arg(args[0], f_type, True) except IndexError: raise BBotException({ 'code': 220, 'function': 'video', 'arg': 0, 'message': 'Video URL in arg 0 is missing.' }) bbot_response = {'video': {'url': video_url}} self.bot.add_output(bbot_response) return bbot_response