Attempt to retrieve a reply to the provided message
        :param user: The user / client. This can be a hostmask, IP address, database ID or any other unique identifier
        :type  user: str

        :param message: The message to retrieve a reply to
        :type  message: string_types

        :param groups: The trigger groups to search, defaults to only matching non-grouped triggers
        :type  groups: set or AnyGroup

        :rtype: str or None

def get_reply(self, user, message, groups=None):
        """
        Attempt to retrieve a reply to the provided message
        :param user: The user / client. This can be a hostmask, IP address, database ID or any other unique identifier
        :type  user: str

        :param message: The message to retrieve a reply to
        :type  message: string_types

        :param groups: The trigger groups to search, defaults to only matching non-grouped triggers
        :type  groups: set or AnyGroup

        :rtype: str or None
        """
        # Make sure triggers have been sorted since the most recent trigger was added
        if not self.sorted:
            self.sort()

        user = self.get_user(user)
        groups = groups or {None}

        # Log this request
        message = Message(self, message)
        request_log_entry = self.request_log.add(user, message, groups)

        # Fetch triggers in our topic and make sure we're not in an empty topic
        triggers = [trigger for trigger in self._sorted_triggers if user.topic == trigger.topic]

        if not triggers and user.topic is not None:
            self._log.warn('User "{user}" was in an empty topic: {topic}'.format(user=user.id, topic=user.topic))
            user.topic = None
            triggers = [trigger for trigger in self._sorted_triggers if user.topic == trigger.topic]

        # It's impossible to get anywhere if there are no empty topic triggers available to guide us
        if not triggers:
            raise AgentMLError('There are no empty topic triggers defined, unable to continue')

        # Fetch triggers in our group and make sure we're not in an empty topic
        if groups is not AnyGroup:
            triggers = [trigger for trigger in triggers if groups.issuperset(trigger.groups or {None})]

        if not triggers:
            if not user.topic:
                self._log.info('There are no topicless triggers matching the specific groups available, giving up')
                return

            self._log.info('The topic "{topic}" has triggers, but we are not in the required groups to match them. '
                           'Resetting topic to None and retrying'.format(topic=user.topic, groups=str(groups)))
            user.topic = None
            triggers = [trigger for trigger in self._sorted_triggers if user.topic == trigger.topic]

        for trigger in triggers:
            try:
                match = trigger.match(user, message)
            except ParserBlockingError:
                return

            if match:
                message = str(match)
                request_log_entry.response = self.response_log.add(message, request_log_entry)
                return message

        # If we're still here, no reply was matched. If we're in a topic, exit and retry
        if user.topic:
            self._log.info('No reply matched in the topic "{topic}", resetting topic to None and retrying'
                           .format(topic=user.topic))
            user.topic = None

            # noinspection PyTypeChecker
            return self.get_reply(user.id, message.raw)