files: # Copyright 2019 Mycroft AI Inc.
  #
  # 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.
  #
  """The fallback skill implements a special type of skill handling
  utterances not handled by the intent system.
  """
  import operator
  from mycroft.metrics import report_timing, Stopwatch
  from mycroft.util.log import LOG
  
  
  from .mycroft_skill import MycroftSkill, get_handler_name
  
  
  class FallbackSkill(MycroftSkill):
      """Fallbacks come into play when no skill matches an Adapt or closely with
      a Padatious intent.  All Fallback skills work together to give them a
      view of the user's utterance.  Fallback handlers are called in an order
      determined the priority provided when the the handler is registered.
  
      ========   ========   ================================================
      Priority   Who?       Purpose
      ========   ========   ================================================
         1-4     RESERVED   Unused for now, slot for pre-Padatious if needed
           5     MYCROFT    Padatious near match (conf > 0.8)
        6-88     USER       General
          89     MYCROFT    Padatious loose match (conf > 0.5)
       90-99     USER       Uncaught intents
         100+    MYCROFT    Fallback Unknown or other future use
      ========   ========   ================================================
  
      Handlers with the numerically lowest priority are invoked first.
      Multiple fallbacks can exist at the same priority, but no order is
      guaranteed.
  
      A Fallback can either observe or consume an utterance. A consumed
      utterance will not be see by any other Fallback handlers.
      """
      fallback_handlers = {}
      wrapper_map = []  # Map containing (handler, wrapper) tuples
  
      def __init__(self, name=None, bus=None, use_settings=True):
          super().__init__(name, bus, use_settings)
  
          #  list of fallback handlers registered by this instance
          self.instance_fallback_handlers = []
  
      @classmethod
      def make_intent_failure_handler(cls, bus):
          """Goes through all fallback handlers until one returns True"""
  
          def handler(message):
              # indicate fallback handling start
              bus.emit(message.forward("mycroft.skill.handler.start",
                                       data={'handler': "fallback"}))
  
              stopwatch = Stopwatch()
              handler_name = None
              with stopwatch:
                  for _, handler in sorted(cls.fallback_handlers.items(),
                                           key=operator.itemgetter(0)):
                      try:
                          if handler(message):
                              #  indicate completion
                              handler_name = get_handler_name(handler)
                              bus.emit(message.forward(
                                       'mycroft.skill.handler.complete',
                                       data={'handler': "fallback",
                                             "fallback_handler": handler_name}))
                              break
                      except Exception:
                          LOG.exception('Exception in fallback.')
                  else:  # No fallback could handle the utterance
                      bus.emit(message.forward('complete_intent_failure'))
                      warning = "No fallback could handle intent."
                      LOG.warning(warning)
                      #  indicate completion with exception
                      bus.emit(message.forward('mycroft.skill.handler.complete',
                                               data={'handler': "fallback",
                                                     'exception': warning}))
  
              # Send timing metric
              if message.context.get('ident'):
                  ident = message.context['ident']
                  report_timing(ident, 'fallback_handler', stopwatch,
                                {'handler': handler_name})
  
          return handler
  
      @classmethod
      def _register_fallback(cls, handler, wrapper, priority):
          """Register a function to be called as a general info fallback
          Fallback should receive message and return
          a boolean (True if succeeded or False if failed)
  
          Lower priority gets run first
          0 for high priority 100 for low priority
  
          Arguments:
              handler (callable): original handler, used as a reference when
                                  removing
              wrapper (callable): wrapped version of handler
              priority (int): fallback priority
          """
          while priority in cls.fallback_handlers:
              priority += 1
  
          cls.fallback_handlers[priority] = wrapper
          cls.wrapper_map.append((handler, wrapper))
  
      def register_fallback(self, handler, priority):
          """Register a fallback with the list of fallback handlers and with the
          list of handlers registered by this instance
          """
  
          def wrapper(*args, **kwargs):
              if handler(*args, **kwargs):
                  self.make_active()
                  return True
              return False
  
          self.instance_fallback_handlers.append(handler)
          self._register_fallback(handler, wrapper, priority)
  
      @classmethod
      def _remove_registered_handler(cls, wrapper_to_del):
          """Remove a registered wrapper.
  
          Arguments:
              wrapper_to_del (callable): wrapped handler to be removed
  
          Returns:
              (bool) True if one or more handlers were removed, otherwise False.
          """
          found_handler = False
          for priority, handler in list(cls.fallback_handlers.items()):
              if handler == wrapper_to_del:
                  found_handler = True
                  del cls.fallback_handlers[priority]
  
          if not found_handler:
              LOG.warning('No fallback matching {}'.format(wrapper_to_del))
          return found_handler
  
      @classmethod
      def remove_fallback(cls, handler_to_del):
          """Remove a fallback handler.
  
          Arguments:
              handler_to_del: reference to handler
          Returns:
              (bool) True if at least one handler was removed, otherwise False
          """
          # Find wrapper from handler or wrapper
          wrapper_to_del = None
          for h, w in cls.wrapper_map:
              if handler_to_del in (h, w):
                  wrapper_to_del = w
                  break
  
          if wrapper_to_del:
              cls.wrapper_map.remove((h, w))
              remove_ok = cls._remove_registered_handler(wrapper_to_del)
          else:
              LOG.warning('Could not find matching fallback handler')
              remove_ok = False
          return remove_ok
  
      def remove_instance_handlers(self):
          """Remove all fallback handlers registered by the fallback skill."""
          self.log.info('Removing all handlers...')
          while len(self.instance_fallback_handlers):
              handler = self.instance_fallback_handlers.pop()
              self.remove_fallback(handler)
  
      def default_shutdown(self):
          """Remove all registered handlers and perform skill shutdown."""
          self.remove_instance_handlers()
          super(FallbackSkill, self).default_shutdown()
  
files: mycroft/skills/fallback_skill.py
files: # Copyright 2019 Mycroft AI Inc.
  #
  # 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.
  #
  """The fallback skill implements a special type of skill handling
  utterances not handled by the intent system.
  """
  import operator
  from mycroft.metrics import report_timing, Stopwatch
  from mycroft.util.log import LOG
  
  
  from .mycroft_skill import MycroftSkill, get_handler_name
  
  
  class FallbackSkill(MycroftSkill):
      """Fallbacks come into play when no skill matches an Adapt or closely with
      a Padatious intent.  All Fallback skills work together to give them a
      view of the user's utterance.  Fallback handlers are called in an order
      determined the priority provided when the the handler is registered.
  
      ========   ========   ================================================
      Priority   Who?       Purpose
      ========   ========   ================================================
         1-4     RESERVED   Unused for now, slot for pre-Padatious if needed
           5     MYCROFT    Padatious near match (conf > 0.8)
        6-88     USER       General
          89     MYCROFT    Padatious loose match (conf > 0.5)
       90-99     USER       Uncaught intents
         100+    MYCROFT    Fallback Unknown or other future use
      ========   ========   ================================================
  
      Handlers with the numerically lowest priority are invoked first.
      Multiple fallbacks can exist at the same priority, but no order is
      guaranteed.
  
      A Fallback can either observe or consume an utterance. A consumed
      utterance will not be see by any other Fallback handlers.
      """
      fallback_handlers = {}
  
      def __init__(self, name=None, bus=None, use_settings=True):
          super().__init__(name, bus, use_settings)
  
          #  list of fallback handlers registered by this instance
          self.instance_fallback_handlers = []
  
      @classmethod
      def make_intent_failure_handler(cls, bus):
          """Goes through all fallback handlers until one returns True"""
  
          def handler(message):
              # indicate fallback handling start
              bus.emit(message.forward("mycroft.skill.handler.start",
                                       data={'handler': "fallback"}))
  
              stopwatch = Stopwatch()
              handler_name = None
              with stopwatch:
                  for _, handler in sorted(cls.fallback_handlers.items(),
                                           key=operator.itemgetter(0)):
                      try:
                          if handler(message):
                              #  indicate completion
                              handler_name = get_handler_name(handler)
                              bus.emit(message.forward(
                                       'mycroft.skill.handler.complete',
                                       data={'handler': "fallback",
                                             "fallback_handler": handler_name}))
                              break
                      except Exception:
                          LOG.exception('Exception in fallback.')
                  else:  # No fallback could handle the utterance
                      bus.emit(message.forward('complete_intent_failure'))
                      warning = "No fallback could handle intent."
                      LOG.warning(warning)
                      #  indicate completion with exception
                      bus.emit(message.forward('mycroft.skill.handler.complete',
                                               data={'handler': "fallback",
                                                     'exception': warning}))
  
              # Send timing metric
              if message.context.get('ident'):
                  ident = message.context['ident']
                  report_timing(ident, 'fallback_handler', stopwatch,
                                {'handler': handler_name})
  
          return handler
  
      @classmethod
      def _register_fallback(cls, handler, priority):
          """Register a function to be called as a general info fallback
          Fallback should receive message and return
          a boolean (True if succeeded or False if failed)
  
          Lower priority gets run first
          0 for high priority 100 for low priority
          """
          while priority in cls.fallback_handlers:
              priority += 1
  
          cls.fallback_handlers[priority] = handler
  
      def register_fallback(self, handler, priority):
          """Register a fallback with the list of fallback handlers and with the
          list of handlers registered by this instance
          """
  
          def wrapper(*args, **kwargs):
              if handler(*args, **kwargs):
                  self.make_active()
                  return True
              return False
  
          self.instance_fallback_handlers.append(wrapper)
          self._register_fallback(wrapper, priority)
  
      @classmethod
      def remove_fallback(cls, handler_to_del):
          """Remove a fallback handler.
  
          Arguments:
              handler_to_del: reference to handler
          """
          for priority, handler in cls.fallback_handlers.items():
              if handler == handler_to_del:
                  del cls.fallback_handlers[priority]
                  return
          LOG.warning('Could not remove fallback!')
  
      def remove_instance_handlers(self):
          """Remove all fallback handlers registered by the fallback skill."""
          while len(self.instance_fallback_handlers):
              handler = self.instance_fallback_handlers.pop()
              self.remove_fallback(handler)
  
      def default_shutdown(self):
          """Remove all registered handlers and perform skill shutdown."""
          self.remove_instance_handlers()
          super(FallbackSkill, self).default_shutdown()
  
files: mycroft/skills/fallback_skill.py
files: from unittest import TestCase, mock
  
  from mycroft.skills import FallbackSkill
  
  
  def setup_fallback(fb_class):
      fb_skill = fb_class()
      fb_skill.bind(mock.Mock(name='bus'))
      fb_skill.initialize()
      return fb_skill
  
  
  class TestFallbackSkill(TestCase):
      def test_life_cycle(self):
          """Test startup and shutdown of a fallback skill.
  
          Ensure that an added handler is removed as part of default shutdown.
          """
          self.assertEqual(len(FallbackSkill.fallback_handlers), 0)
          fb_skill = setup_fallback(SimpleFallback)
          self.assertEqual(len(FallbackSkill.fallback_handlers), 1)
          self.assertEqual(FallbackSkill.wrapper_map[0][0],
                           fb_skill.fallback_handler)
          self.assertEqual(len(FallbackSkill.wrapper_map), 1)
  
          fb_skill.default_shutdown()
          self.assertEqual(len(FallbackSkill.fallback_handlers), 0)
          self.assertEqual(len(FallbackSkill.wrapper_map), 0)
  
      def test_manual_removal(self):
          """Test that the call to remove_fallback() removes the handler"""
          self.assertEqual(len(FallbackSkill.fallback_handlers), 0)
  
          # Create skill adding a single handler
          fb_skill = setup_fallback(SimpleFallback)
          self.assertEqual(len(FallbackSkill.fallback_handlers), 1)
  
          self.assertTrue(fb_skill.remove_fallback(fb_skill.fallback_handler))
          # Both internal trackers of handlers should be cleared now
          self.assertEqual(len(FallbackSkill.fallback_handlers), 0)
          self.assertEqual(len(FallbackSkill.wrapper_map), 0)
  
          # Removing after it's already been removed should fail
          self.assertFalse(fb_skill.remove_fallback(fb_skill.fallback_handler))
  
  
  class SimpleFallback(FallbackSkill):
      """Simple fallback skill used for test."""
      def initialize(self):
          self.register_fallback(self.fallback_handler, 42)
  
      def fallback_handler(self):
          pass
  
files: test/unittests/skills/test_fallback_skill.py
files: from unittest import TestCase, mock
  
  from mycroft.skills import FallbackSkill
  
  
  def setup_fallback(fb_class):
      fb_skill = fb_class()
      fb_skill.bind(mock.Mock(name='bus'))
      fb_skill.initialize()
      return fb_skill
  
  
  class TestFallbackSkill(TestCase):
      def test_life_cycle(self):
          """Test startup and shutdown of a fallback skill.
  
          Ensure that an added handler is removed as part of default shutdown.
          """
          self.assertEqual(len(FallbackSkill.fallback_handlers), 0)
          fb_skill = setup_fallback(SimpleFallback)
          self.assertEqual(len(FallbackSkill.fallback_handlers), 1)
          self.assertEqual(FallbackSkill.wrapper_map[0][0],
                           fb_skill.fallback_handler)
          self.assertEqual(len(FallbackSkill.wrapper_map), 1)
  
          fb_skill.default_shutdown()
          self.assertEqual(len(FallbackSkill.fallback_handlers), 0)
          self.assertEqual(len(FallbackSkill.wrapper_map), 0)
  
      def test_manual_removal(self):
          """Test that the call to remove_fallback() removes the handler"""
          self.assertEqual(len(FallbackSkill.fallback_handlers), 0)
  
          # Create skill adding a single handler
          fb_skill = setup_fallback(SimpleFallback)
          self.assertEqual(len(FallbackSkill.fallback_handlers), 1)
  
          fb_skill.remove_fallback(fb_skill.fallback_handler)
          # Both internal trackers of handlers should be cleared now
          self.assertEqual(len(FallbackSkill.fallback_handlers), 0)
          self.assertEqual(len(FallbackSkill.wrapper_map), 0)
  
  
  class SimpleFallback(FallbackSkill):
      """Simple fallback skill used for test."""
      def initialize(self):
          self.register_fallback(self.fallback_handler, 42)
  
      def fallback_handler(self):
          pass
  
files: test/unittests/skills/test_fallback_skill.py