Пример #1
0
 def clear_rules(self):
     """ Empty the rules database """
     log.debug("Rules database cleared")
     self.rules_db = RulesDB()
Пример #2
0
 def clear_rules(self):
     """ Empty the rules database """
     log.debug("Rules database cleared")
     self.rules_db = RulesDB()
Пример #3
0
class ChatbotEngine(object):
    """ Python Chatbot Reply Generator

    Loads pattern/reply rules, using a simplified regular expression grammar
    for the patterns, and decorated Python methods for the rules. Maintains
    rules, a variable dictionary for the chatbot's internal state plus one
    for each user conversing with the chatbot. Matches user input to its
    database of patterns to select a reply rule, which may recursively reference
    other reply patterns and rules.

    Public instance methods:
      load_script_directory: loads rules from a directory of python files
      clear_rules: empties the rule database
      reply: given a message, find the best matching rule, run it, and return
              the reply
    """
    def __init__(self, depth=50):
        """Initialize a new ChatbotEngine.

        Keyword argument:
        depth -- Recursion depth limit for replies that reference other replies
        """
        self._depth_limit = depth

        self._botvars = {}
        self._variables = {"b": self._botvars, "u": None}

        self._users = {}  # will contain UserInfo objects
        log.debug("Chatbot instance created.")
        self.clear_rules()

    def clear_rules(self):
        """ Empty the rules database """
        log.debug("Rules database cleared")
        self.rules_db = RulesDB()

    def load_script_directory(self, directory, ignore_errors=False):
        """ Load rules from *.py in a directory """
        self.rules_db.load_script_directory(directory, self._botvars,
                                            ignore_errors)

    def reply(self, user, user_dict, message):
        """ For the current topic, find the best matching rule for the message.
        Recurse as necessary if the first rule returns references to other
        rules. This method does setup and cleanup and passes the actual work
        to self._reply()

        Arguments:
        user -- any hashable value, used to identify to whom we are speaking
        user_dict -- dictionary of information about the user, to be passed
                        to rule methods
        message -- string (not bytestring!) to reply to

        Return value: string returned by rule(s)

        Exceptions:
        RecursionTooDeepError -- if recursion goes over depth limit passed
            to __init__
        """
        if not isinstance(message, text_type):
            raise TypeError("message argument must be string, not bytestring")

        self.rules_db.sort_rules()

        log.debug('Asked to reply to: "{0}" from {1}'.format(message, user))
        self._setup_user(user, user_dict)

        try:
            reply = self._reply(user, message, 0)
        except RecursionTooDeepError as e:
            e.args = ('Could not find reply to "{0}", due to rules '
                      "referencing other rules too many "
                      "times".format(message), )
            raise
        self._remember(user, message, reply)
        return reply

    def _reply(self, user, message, depth):
        """ Recursively construct replies """
        if depth > self._depth_limit:
            raise RecursionTooDeepError

        log.debug('Searching for rule matching "{0}", depth == {1}'.format(
            message, depth))
        userinfo = self._users[user]
        topic = userinfo.topic_name
        target = Target(message, self.rules_db.topics[topic].substitutions)
        reply = ""

        for rule in self.rules_db.topics[topic].sortedrules:
            m = rule.match(target, userinfo.repl_history, self._variables)
            if m is not None:
                reply = self._reply_from_rule(rule, m, userinfo)
                self._check_for_topic_change(user, rule, topic,
                                             userinfo.topic_name)
                break

        reply = self._recursively_expand_reply(user, reply, depth)
        if not reply:
            log.debug("Empty reply generated")
        else:
            log.debug("Generated reply: " + reply)
        return reply

    def _reply_from_rule(self, rule, rule_match, userinfo):
        """ Given a rule and the results from a successful match of the rule's
        pattern, call the rule method and return the results.
        """
        log.debug("Found match, rule {0}".format(rule.rulename))

        inst = get_method_self(rule.method)
        inst.userinfo = userinfo
        inst.match = rule_match.dict
        reply = rule.method()
        if not isinstance(reply, text_type):
            raise TypeError("Rule {0} returned something other than a "
                            "string.".format(rule.rulename))
        log.debug('Rule {0} returned "{1}"'.format(rule.rulename, reply))
        return reply

    def _recursively_expand_reply(self, user, reply, depth):
        """ Given a reply string from a rule, look for references to other
        rules enclosed within < > and recursively call _reply to get responses,
        and substitute those into the original string. Evaluates from left
        to right. Doesn't care if you match the <>'s or not.
        """
        matches = [m for m in re.finditer("<(.*?)>", reply, flags=re.UNICODE)]
        if matches:
            log.debug("Rule returned: " + reply)
        sub_replies = [
            self._reply(user,
                        m.groups()[0], depth + 1) for m in matches
        ]
        zipper = list(zip(matches, sub_replies))
        zipper.reverse()
        for m, sub_reply in zipper:
            reply = reply[:m.start()] + sub_reply + reply[m.end():]
        return reply

    def _check_for_topic_change(self, user, rule, old_topic, new_topic):
        """ Given a rule, and the topic set before and after its execution,
        make sure the change is legit and do appropriate debug logging.
        """
        if old_topic != new_topic:
            if new_topic not in self.rules_db.topics:
                log.warning("Rule {0} changed to empty topic {1}, "
                            "returning to 'all'".format(
                                rule.rulename, new_topic))
                new_topic = "all"
            log.debug("User {0} now in topic {1}".format(user, new_topic))

        self._users[user].topic_name = new_topic

    def _setup_user(self, user, user_dict):
        """ Set up the Script class to process a message from a user. If the
        user is new to us, create the UserInfo object for them, and call
        the setup_user method of all the script instances so they can
        initialize user variables.
        """
        new = (user not in self._users)
        if new:
            self._users[user] = UserInfo(user_dict)
        else:
            self._users[user].info.update(user_dict)

        self._variables["u"] = self._users[user].vars
        topic = self._users[user].topic_name
        if topic not in self.rules_db.topics:
            log.warning("User {0} is in empty topic {1}, "
                        "returning to 'all'".format(user, topic))
            topic = self._users[user].topic_name = "all"

        if new:
            log.debug("New user, running all scripts' setup_user methods")
            for inst in self.rules_db.script_instances:
                inst.userinfo = self._users[user]
                inst.setup_user(user)

    def _remember(self, user, message, reply):
        """ Save recent messages and replies, per user """
        user_info = self._users[user]
        topic_name = user_info.topic_name
        user_info.msg_history.appendleft(message)
        user_info.repl_history.appendleft(
            Target(reply, self.rules_db.topics[topic_name].substitutions))
Пример #4
0
class ChatbotEngine(object):
    """ Python Chatbot Reply Generator

    Loads pattern/reply rules, using a simplified regular expression grammar
    for the patterns, and decorated Python methods for the rules. Maintains
    rules, a variable dictionary for the chatbot's internal state plus one
    for each user conversing with the chatbot. Matches user input to its
    database of patterns to select a reply rule, which may recursively reference
    other reply patterns and rules.

    Public instance methods:
      load_script_directory: loads rules from a directory of python files
      clear_rules: empties the rule database
      reply: given a message, find the best matching rule, run it, and return
              the reply
    """

    def __init__(self, depth=50):
        """Initialize a new ChatbotEngine.

        Keyword argument:
        depth -- Recursion depth limit for replies that reference other replies
        """
        self._depth_limit = depth

        self._botvars = {}
        self._variables = {"b": self._botvars,
                           "u": None}

        self._users = {}  # will contain UserInfo objects
        log.debug("Chatbot instance created.")
        self.clear_rules()

    def clear_rules(self):
        """ Empty the rules database """
        log.debug("Rules database cleared")
        self.rules_db = RulesDB()

    def load_script_directory(self, directory, ignore_errors=False):
        """ Load rules from *.py in a directory """
        self.rules_db.load_script_directory(directory, self._botvars,
                                            ignore_errors)

    def reply(self, user, user_dict, message):
        """ For the current topic, find the best matching rule for the message.
        Recurse as necessary if the first rule returns references to other
        rules. This method does setup and cleanup and passes the actual work
        to self._reply()

        Arguments:
        user -- any hashable value, used to identify to whom we are speaking
        user_dict -- dictionary of information about the user, to be passed
                        to rule methods
        message -- string (not bytestring!) to reply to

        Return value: string returned by rule(s)

        Exceptions:
        RecursionTooDeepError -- if recursion goes over depth limit passed
            to __init__
        """
        if not isinstance(message, text_type):
            raise TypeError("message argument must be string, not bytestring")

        self.rules_db.sort_rules()

        log.debug('Asked to reply to: "{0}" from {1}'.format(message, user))
        self._setup_user(user, user_dict)

        try:
            reply = self._reply(user, message, 0)
        except RecursionTooDeepError as e:
            e.args = ('Could not find reply to "{0}", due to rules '
                      "referencing other rules too many "
                      "times".format(message),)
            raise
        self._remember(user, message, reply)
        return reply

    def _reply(self, user, message, depth):
        """ Recursively construct replies """
        if depth > self._depth_limit:
            raise RecursionTooDeepError

        log.debug('Searching for rule matching "{0}", depth == {1}'.format(
            message, depth))
        userinfo = self._users[user]
        topic = userinfo.topic_name
        target = Target(message, self.rules_db.topics[topic].substitutions)
        reply = ""

        for rule in self.rules_db.topics[topic].sortedrules:
            m = rule.match(target, userinfo.repl_history,
                           self._variables)
            if m is not None:
                reply = self._reply_from_rule(rule, m, userinfo)
                self._check_for_topic_change(user, rule, topic,
                                             userinfo.topic_name)
                break

        reply = self._recursively_expand_reply(user, reply, depth)
        if not reply:
            log.debug("Empty reply generated")
        else:
            log.debug("Generated reply: " + reply)
        return reply

    def _reply_from_rule(self, rule, rule_match, userinfo):
        """ Given a rule and the results from a successful match of the rule's
        pattern, call the rule method and return the results.
        """
        log.debug("Found match, rule {0}".format(rule.rulename))

        inst = get_method_self(rule.method)
        inst.userinfo = userinfo
        inst.match = rule_match.dict
        reply = rule.method()
        if not isinstance(reply, text_type):
            raise TypeError("Rule {0} returned something other than a "
                            "string.".format(rule.rulename))
        log.debug('Rule {0} returned "{1}"'.format(rule.rulename, reply))
        return reply

    def _recursively_expand_reply(self, user, reply, depth):
        """ Given a reply string from a rule, look for references to other
        rules enclosed within < > and recursively call _reply to get responses,
        and substitute those into the original string. Evaluates from left
        to right. Doesn't care if you match the <>'s or not.
        """
        matches = [m for m in re.finditer("<(.*?)>", reply, flags=re.UNICODE)]
        if matches:
            log.debug("Rule returned: " + reply)
        sub_replies = [self._reply(user, m.groups()[0], depth + 1)
                       for m in matches]
        zipper = list(zip(matches, sub_replies))
        zipper.reverse()
        for m, sub_reply in zipper:
            reply = reply[:m.start()] + sub_reply + reply[m.end():]
        return reply

    def _check_for_topic_change(self, user, rule, old_topic, new_topic):
        """ Given a rule, and the topic set before and after its execution,
        make sure the change is legit and do appropriate debug logging.
        """
        if old_topic != new_topic:
            if new_topic not in self.rules_db.topics:
                log.warning("Rule {0} changed to empty topic {1}, "
                            "returning to 'all'".format(
                                rule.rulename, new_topic))
                new_topic = "all"
            log.debug("User {0} now in topic {1}".format(user, new_topic))

        self._users[user].topic_name = new_topic

    def _setup_user(self, user, user_dict):
        """ Set up the Script class to process a message from a user. If the
        user is new to us, create the UserInfo object for them, and call
        the setup_user method of all the script instances so they can
        initialize user variables.
        """
        new = (user not in self._users)
        if new:
            self._users[user] = UserInfo(user_dict)
        else:
            self._users[user].info.update(user_dict)

        self._variables["u"] = self._users[user].vars
        topic = self._users[user].topic_name
        if topic not in self.rules_db.topics:
            log.warning("User {0} is in empty topic {1}, "
                        "returning to 'all'".format(user, topic))
            topic = self._users[user].topic_name = "all"

        if new:
            log.debug("New user, running all scripts' setup_user methods")
            for inst in self.rules_db.script_instances:
                inst.userinfo = self._users[user]
                inst.setup_user(user)

    def _remember(self, user, message, reply):
        """ Save recent messages and replies, per user """
        user_info = self._users[user]
        topic_name = user_info.topic_name
        user_info.msg_history.appendleft(message)
        user_info.repl_history.appendleft(
            Target(reply, self.rules_db.topics[topic_name].substitutions))