5 Reasoning with rule-based systems – Durable Rules Engine

In the sense–act model, agents (or robots) typically perform an action as a direct response of a sensory input. In the ‘sense–think–act’ model, there is an element of deliberation in which the agent makes a choice about what action to perform based not just on the sensory input but also on other factors.

In the simplest case, this might be a value against which the input is compared, or it might be a much more elaborate decision process involving a wide range of factors.

In the previous notebook, you met a simple conversational rule-based agent in the form of Eliza. In this notebook, you will explore one possible architecture for implementing a deliberative ‘think’ model in the form of a more generic rule-based system within which you can write your own sets of rules.

Understanding the particular details of this notebook is not necessary for successful completion of this module. The aim is more for you to develop a broad understanding of the nature and potential power of a rule-based system.

5.1 Representing and using knowledge and beliefs

Artificial intelligence and robotics have the major problem of representing facts and knowledge, or at least beliefs, inside machines.

In humans, we have a vast amount of knowledge in our brains. This knowledge is distributed over the brain, rather than each fact being neatly stored in a single memory unit.

The structure of the human brain is completely different from the structure of a robot’s or a real computer’s ‘brain’, and roboticists have found it very difficult to implant a wide range of experiences (real-world data) into robot brains. Whilst significant progress has been made in artificial intelligence (AI) and machine learning (ML) approaches in recent years by using ever-more computational resources, these achievements are often quite limited in terms of domain or general applicability. They can also be hugely expensive in terms of the amount of data and computational effort, as well as the energy used to power the underlying computers that are required to create them. Ever larger and more complex natural-language processing (NLP) models are also proving effective in parsing natural-language statements and generating natural-language texts, albeit often in a ‘free-writing’ sense.

An alternative to the ‘self-learning’ neural network style of artificial intelligence, which you will meet later in the block, are approaches in which we try to explicitly encode knowledge using an approach known as rule-based systems.

5.2 Introducing the Durable Rules Engine

The Durable Rules Engine is a polyglot framework for creating rule-based systems capable of reasoning over large collections of factual statements.

To say that the framework is polyglot means that we can write programs for the same framework using different programming languages, specifically Python, Node.js (a flavour of JavaScript) and Ruby. Underneath, the same rules engine (which itself happens to be written in the C programming language) processes the facts and the rules to allow the system to reason.

Note that the Durable Rules Engine (durable-rules) is not available directly within our robot simulator programs. Instead, we call on it via the full Python environment associated with code cells that are not prefaced by the simulator magic.

The engine itself is rather more powerful than the engine used in the Eliza program example and can accept a wide range of rule definitions. It also makes use of a knowledge base of asserted facts (as well as ephemeral events) that are reasoned against using the rules.

To see how this more comprehensive version of a rule-based system works, let’s consider the example of reasoning over a set of ‘facts’ that are asserted as subject predicate object statements. Separate rules parse one or more of these statements and then try to make general additional statements as a logical consequence.

In many formal studies of intelligent agents, ‘knowledge’ is defined as ‘justified true belief’. An agent may ‘believe’ a fact, but that is only classed as ‘knowledge’:

  • if the fact is true

  • if the agent is justified in having the belief (that is, there is a good reason why it has that belief).

Facts might take the form Sam is a student where Sam is the subject of the statement, student is the object of the statement, and is a is a predicate that defines some sort of relationship between the subject and the object.

Rules test statements, and if they match the rule condition then the rule asserts another fact.

For example, if Sam is a student, then Sam can use the module forums.

Let’s see how that works in practice. Note that the following treatment uses a simplification of the syntax used by default in the durable-rules framework. (There is just too much clutter in the original syntax to see what’s going on!)

5.2.1 Setting up the Durable Rules Engine

Let’s import the packages we need and enable some magic:

from durable.lang import ruleset, when_all, assert_fact, c, m
from durable_rules_tools.rules_utils import new_ruleset, Set, Subject

%reload_ext durable_rules_tools

Some of the code used to define the rulesets looks quite cluttered. Much of the code is ‘boilerplate’ code and you do not need to know how to write it yourself. Instead, try to focus on the content of the rules and accept the code machinery around it as a given.

5.2.2 Defining a ruleset

The ruleset definition syntax is little bewildering, so just try to see the structural patterns that the various bits of syntax make.

So, let’s take a deep breath and dive in, looking at this pseudo-code abstraction of a possible rule:

if ?PERSON is student
    then ?PERSON can use forums

In this case, ?PERSON is a variable representing the subject, forums is the object, and can use is the predicate.

5.2.3 Encoding rules using the durable-rules framework (optional)

We can encode this formal rule using the durable-rules framework as follows:

@when_all(Subject("is", "student"))
def cm_forum_use(c):
    Set(c, '? : can use : forums' )

The @... statements are known as Python decorators; but that’s all you need to know in case you want to look them up them further (further investigation is definitely not required and not expected of you for the purposes of this module). Just regard it as ‘syntactic sugar’ intended to make the rule a bit more readable than it might otherwise be. So go with the flow and just try to read the rules as some sort of structured pattern you can recognise as performing some sort of magic…

The rule has the form:

@CONDITION
def RULENAME(TESTED_ASSERTION):
    ACTION

If you defocus your eyes, you can perhaps see how those elements might relate to a rule that could perhaps be more logically presented as:

RULENAME:
  if TESTED_ASSERTION meets CONDITION
  then ACTION

5.2.4 A simplified route to declaring durable-rules rules

To simplify rule creation, we can create a function that lets us declare simple rules that test facts relative to a specific subject, and assert new facts on the subject, in the following way:

create_simple_rule(IF_SUBJECT = ["is", "student"],
                   THEN_SUBJECT = ['can use', 'forums'] )

Let’s define the rule handler function so we can make use of it:

# You do not need to understand how the following code works
# It is just something you will find convenient to use
# as a provided function
def create_simple_rule(IF_SUBJECT, THEN_SUBJECT, comment=''):
    """Create a simple rule to run with durable rules engine."""
    when_all(Subject(IF_SUBJECT[0], IF_SUBJECT[1]))(lambda c: Set(c, f'? : {THEN_SUBJECT[0]} : {THEN_SUBJECT[1]}'))
    # We don't use the comment,
    # but it helps keep track of what the rule applies to

Run the following cell to define a new ruleset:

RULESET_1 = new_ruleset()

with ruleset(RULESET_1):
    # --- UTILITY RULES ---
    # Display all asserted facts
    # Just accept it as boilerplate!
    when_all(+m.subject)(lambda c: print('Fact: {0} {1} {2}'.format(c.m.subject, c.m.predicate, c.m.object)))
    
    # --- USER RULES ---
    create_simple_rule(IF_SUBJECT = ["is", "student"],
                       THEN_SUBJECT = ["can use", "forums"],
                       comment ='student_forum_use')

5.2.5 Asserting facts

We can now assert a couple of facts, and see what conclusions can be draw about them from an application of the rules.

Facts are asserted in the form: subject : predicate : object.

We assert facts in the context of a particular ruleset via a cell block magic, %%assert_facts -r RULESET_NAME.

Run the following cell to assert some facts against the RULESET_1 ruleset:

%%assert_facts -r RULESET_1
Sam : is : student
Chris : is : course manager

5.2.6 Creating a new ruleset with multiple rules

We can’t easily add rules to a pre-existing ruleset, so let’s create another ruleset, building on ideas used in the first, that contains another rule:

RULESET_2 = new_ruleset()
with ruleset(RULESET_2):
    # --- UTILITY RULES ---
    # Display all asserted facts
    when_all(+m.subject)(lambda c: print('Fact: {0} {1} {2}'.format(c.m.subject, c.m.predicate, c.m.object)))
    
    # --- USER RULES ---
    create_simple_rule(IF_SUBJECT = ["is", "course manager"],
                       THEN_SUBJECT =  ['can read', 'forum discussions'],
                       comment = 'cm_forum_use')
    
    # -- PREVIOUS RULES --
    create_simple_rule(IF_SUBJECT = ["is", "student"],
                       THEN_SUBJECT = ['can use', 'forums'],
                       comment = 'student_forum_use')
    

Let’s test our assertions again:

%%assert_facts -r RULESET_2
Sam : is : student
Chris : is : course manager

So, course managers can read forum discussions, but students can use forums. What might that entail?

5.2.7 A ruleset with different rules that test the same condition

In the following set, we define two rules that test the same condition, but with different actions:

RULESET_3 = new_ruleset()
with ruleset(RULESET_3):
    # --- UTILITY RULES ---
    # Display all asserted facts
    when_all(+m.subject)(lambda c: print('Fact: {0} {1} {2}'.format(c.m.subject, c.m.predicate, c.m.object)))
    
    # --- USER RULES ---
    
    create_simple_rule(IF_SUBJECT = ["can use", "forums"],
                       THEN_SUBJECT = ['can read', 'forum discussions'],
                       comment = 'forum_read'  )

    create_simple_rule(IF_SUBJECT = ["can use", "forums"],
                       THEN_SUBJECT = ['can post to', 'forum discussions'],
                       comment = 'forum_read'  )

    # -- PREVIOUS RULES --
    create_simple_rule(IF_SUBJECT = ["is", "course manager"],
                       THEN_SUBJECT = ['can read', 'forum discussions'],
                       comment = 'cm_forum_use'  )
    
    create_simple_rule(IF_SUBJECT = ["is", "student"],
                       THEN_SUBJECT = ['can use', 'forums'],
                       comment = 'student_forum_use' )
    

What can we determine now?

%%assert_facts -r RULESET_3
Sam : is : student
Chris : is : course manager

At the next level of complexity, we might want to draw some conclusions about multiple facts. Suppose, for example, that we wish to identify people who have ‘engaged’ with the forums. We might define such people as people who have read a forum post and who have posted to a forum.

%%assert_facts -r RULESET_3

Al : has read : forum post
Al : has posted to : forum

Sam : has posted to : forum

5.2.8 A ruleset with rules that test multiple conditions (optional)

The rules we have seen so far test just a single condition, so how do we test two conditions?

if ?PERSON has read forum post AND ?PERSON has posted to forum
then ?PERSON has engaged with forum

This is where things start getting trickier, and where we shall finish our quick introduction to creating rules with the durable-rules framework.

Note that you are not expected to write your own rules at this level of complexity. The intention is just to demonstrate that we can create such rules.

First, we will create another simple helper function that defines a Python dictionary containing subject, object and predicate terms, and then uses the dictionary to assert a fact described by those terms:

def rule_assert_fact(c, subj, pred, obj):
    """Assert a (subject, predicate, object) fact."""
    c.assert_fact({'subject': subj,
                   'predicate': pred,
                   'object': obj }
                 )

In order to define a rule that tests multiple conditions, we need to create a temporary reference to a fact (for example, c.first) when the fact matches a rule. When testing the rule against other facts, we can then use those temporary references to see whether all the rule conditions are met:

RULESET_4 = new_ruleset()
with ruleset(RULESET_4):
    
    # IF
    when_all(c.first << Subject('has read', 'forum post'),
             c.second << Subject('has posted to', 'forum') & (m.subject == c.first.subject))(
    # THEN
    lambda c: rule_assert_fact(c,
                               c.first.subject,
                               pred= 'has engaged with',
                               obj='forum' ))

    @when_all(+m.subject)
    def output(c):
        print('Fact: {0} {1} {2}'.format(c.m.subject, c.m.predicate, c.m.object))
 

Let’s now test the following assertions to see who has been identified as engaging with the forums:

%%assert_facts -r RULESET_4

Al : has read : forum post
Al : has posted to : forum

Sam : has posted to : forum

Hopefully, from these examples and the earlier Eliza example you have a feeling for how we can build up quite rich sequences of behaviour (conversations over time, logical reasoning over multiple facts, including over facts derived from earlier-presented facts) using quite simple rules. But while each rule might be quite simple, and the discrete actions performed by each rule might be quite simple, the emergent behaviour might be quite elaborate.

5.3 Trying out another ruleset (optional)

Let’s try another example, this time using one of the example rulesets provided in the durable-rules documentation.

We’ll also see how we can add another dimension to the rules and create a ruleset that speaks back to us.

You’ve already seen how we can get the simulated robot to speak, but how might we go about getting our notebooks to talk to us?

Once again, you are not expected to write your own rules at this level of complexity. The intention is simply to give you an impression of what sorts of thing we can achieve with a rule-based system.

5.3.1 Talking notebooks

To get the robot to speak in the simulator, we make use of the browser’s JavaScript speech engine. This speech engine was also used to allow Eliza to speak. It’s not too hard to pull together a simple Python package, intended for use in Jupyter notebooks, that makes it easy for us to call this engine from a single line of Python code running via a notebook code cell that is not prefixed with the simulator magic.

The following example demonstrates one such approach. The Python object that manages the speech actions also keeps track of how many messages have been posted and returns a visual count of utterances, alongside a transcript of each utterance.

from nb_simple_speech import Speech, browser_voicelist

Create a speaker…

speaker = Speech()

And listen to them talk:

speaker.say('Hello, how are you?')
speaker.say('All well, I hope?')

Run the following code cell to display a list of available browser voices:

print(browser_voicelist)

If no voices are listed, then your browser may not support the full range of speech commands. Try using a recent version of Google Chrome instead.

Change the voice by setting the desired voice number:

speaker.set_voice(49)
speaker.say('I can change my voice')

You can use the following command to reset the message count in the transcript:

speaker.reset_count()
speaker.say('hello again')

5.3.2 Adding support for speaking rules

The following function will speak aloud the condition and action for some successfully fired rules:

def create_simple_speaker_rule(IF, THEN, comment='', rules=[]):
    """Create a simple speaking rule to run with durable rules engine."""
    rule_name = f"RULE_{len(rules)}"
    rules.append(rule_name)
    
    @when_all(Subject(IF[0], IF[1]))
    def rule_name(c):
        speaker.say(f'Given {c.m.subject} {IF[0]} {IF[1]}')
        Set(c, f'? : {THEN[0]} : {THEN[1]}')
        speaker.say(f'then {c.m.subject} {THEN[0]} {THEN[1]}')
    # We don't use the comment,
    # but it helps keep track of what the rule applies to

5.3.3 Listening to rules as they reason

Now we can listen to the rules as they are fired, as well as seeing a report that shows the order in which they were fired.

RULESET = new_ruleset()
with ruleset(RULESET):
    
    # --- UTILITY RULES ---
    @when_all(+m.subject)
    def output(c):
        print('\nFact: {0} {1} {2}'.format(c.m.subject, c.m.predicate, c.m.object))

    # IF
    when_all(c.first << Subject('eats', 'flies'),
              Subject('lives', 'water') & (m.subject == c.first.subject))(
    # THEN
    lambda c: rule_assert_fact(c,
                               c.first.subject,
                               pred= 'is',
                               obj='frog' ))
    
    
    # IF
    when_all(c.first << Subject('eats', 'flies'),
              Subject('lives', 'land') & (m.subject == c.first.subject))(
    # THEN
    lambda c: rule_assert_fact(c,
                               c.first.subject,
                               pred= 'is',
                               obj='chameleon' ))

    
    
    create_simple_speaker_rule(IF = ['eats', 'worms'],
                               THEN = ['is', 'bird'])

    create_simple_speaker_rule(IF = ['is', 'frog'],
                               THEN =['is', 'green'],
                               comment = 'green'  )
    
    create_simple_speaker_rule(IF = ['is', 'chameleon'],
                               THEN =['is', 'grey'],
                               comment = 'grey'  )
        
    create_simple_speaker_rule(IF = ['is', 'bird'],
                               THEN = ['is', 'black'])

    create_simple_speaker_rule(IF = ['is', 'bird'],
                               THEN = ['can', 'fly'])   

Now let’s assert a fact and see (and hear!) how our rule-based system reasons about it:

%%assert_facts -r RULESET
Kermit : eats : worms

5.4 More general forms of rules

So far we have focused on reasoning about ‘facts’ in the form of statements with the form subject predicate object.

But this actually represents a more complicated form of reasoning than the rules engine actually employs because the atomic smallest-possible facts are not the subject predicate object triples at all, they are the individual properties: {subject: SUBJECT}, {predicate: PREDICATE} and {object: OBJECT}.

5.4.1 Facts versus events

Facts persist, whereas events are retracted once they have been evaluated. Events are particularly useful in a robotics context, where we may want to respond to repeated sensor events.

For example, imagine a case where we want to avoid a red line, because red lines indicate danger.

Note that the rules defined in the following ruleset follow the decorator declaration convention. Relax your eyes, and let the IF…THEN… pattern formed by the syntax of each rule reveal itself to you.

from durable.lang import post

EVENTRULESET = new_ruleset()
with ruleset(EVENTRULESET):
    # this rule will trigger as soon as three events match the condition
    @when_all(m.color=='red')
    def see_red(c):
        speaker.say(f'I see red')
        c.assert_fact({'status': 'danger'})
        
    @when_all(m.color!='red')
    def not_red(c):
        speaker.say(f'I see {c.m.color}')
        c.assert_fact({'status': 'safe'})

    @when_all( m.status == 'danger')
    def dangerous(c):
        speaker.say(f'That is dangerous.')
        c.retract_fact({'status': 'danger'})
        
    @when_all( m.status == 'safe')
    def safe(c):
        speaker.say(f'That is safe.')
        c.retract_fact({'status': 'safe'})
          

What happens if we detect a red colour?

post(EVENTRULESET, {'color': 'red' });

How about if we detect a green colour?

post(EVENTRULESET, {'color': 'green' });

What if we see red, then green quickly after?

post(EVENTRULESET, {'color': 'red' });
post(EVENTRULESET, {'color': 'green' });

5.5 How might rules be useful in a robot context?

Although we can easily create our own if... statements in the program downloaded to the simulator and control the robot’s behaviour that way, it may be more convenient to develop, and test, a large and possibly complex rule-based set of behaviours using a framework such as durable-rules.

This may be achieved by capturing sensor values from the robot in the simulator, passing them back to the notebook’s Python context, passing them as events to the durable-rules ruleset, applying the rules to create some statement of a desired motor action, and then returning this instruction to the simulated robot for execution there.

We will not pursue this approach further, here. However, you will have an opportunity to control the simulated robot in a similar way using a neural network running in the notebook context, rather than a rule-based system, in a later notebook.

5.6 Summary

The durable-rules framework provides an example of a system that can be used to generate a powerful rule-based reasoning system.

By reasoning about a set of persistent facts or ephemeral events, rule-based systems constructed using frameworks such as this can be used to implement a wide range of systems, from fraud-detection systems to systems that implement complex sets of business rules in a corporate context.

Rule-based systems can also be developed to implement actual robot controllers, with rules accepting events based on incoming sensor data as well as higher-level beliefs (that is, ‘facts’) derived from sensor data events and other facts.

This completes the practical activities for this week.