Eliza 8 - Introducing complex patterns

by Ravi Bhavnani


WARNING: THIS STEP IS LONG, COMPLEX AND LOADS OF FUN!
ARM YOURSELF WITH SUFFICIENT PIZZA, CAFFEINATED SODA AND TIME BEFORE STARTING IT.

Thus far, Eliza has the capability to recognize and respond using format-free patterns, i.e. patterns that generate responses that don't care about the format of the user's input. In this step, we're going to build our first "complex" pattern - i.e. a pattern whose response will include part of the user's input. This is what gives the illusion that Eliza actually understands what the user says.

For example, the input:

    SOMETIMES I REMEMBER MY CHILDHOOD FRIEND BOBBY.

might cause Eliza to respond with:

    WHY DO YOU RECOLLECT YOUR CHILDHOOD FRIEND BOBBY JUST NOW?

Pretty cool, huh? Let's see how to make this magic happen.

Inside a complex pattern

There are two important differences between a complex pattern and a format-free pattern:

  1. The response generated by a complex pattern references part of the user's input (the user's childhood friend Bobby, in the above example).

  2. The response generated by the complex pattern doesn't simply duplicate the referenced part. Instead, the referenced part "MY CHILDHOOD FRIEND BOBBY" is transformed into "YOUR CHILDHOOD FRIEND BOBBY" before the response is generated.

Other than that, complex patterns resemble format-free patterns. Both types match one or more phrases and both types have a rank.

Creating the complex pattern class

We'll create the complex pattern class by:

  1. First, simply copying the contents of the format-free pattern class.

  2. Next, adding new functionality as needed.

  3. Finally, moving code that's common to both complex and format-free patterns to a separate base class, and making the complex and format-free pattern classes derive from that class.

    This process is called refactoring and is done to get rid off duplicate code. By reducing the amount of code in your app, you reduce the number of things that can go wrong, thereby improving your app's stability. Refactoring is also known as the DRY principle of software engineering. DRY stands for "Don't Repeat Yourself" - i.e. don't let the same code exist in more than one place!

    The guy who wrote the book (literally!) on refactoring is Martin Fowler.

But wait - what's this "format-free pattern" class? All we have so far is a Pattern class (and classes that derive from it). OK, so let's first rename the Pattern class to FormatFreePattern (and rename all the references to Pattern to FormatFreePattern). While we're at it, let's move all the format-free pattern classes to their own folder. When you're done, your project should look like this:

Then, let's add the ComplexPattern class which will start out by being an exact clone of FormatFreePattern. After doing this, your project should look like this:

Note that we don't yet have any instances of the type ComplexPattern. That will come later, once we've analyzed how a complex pattern works.

How a complex pattern generates a response

A FormatFreePattern generates a response by simply picking the next one from its list of responses.

A ComplexPattern doesn't have a list of responses. Instead, it has a a list of "decomposition" and "reassembly" rules. Let's see what these are by examining the definition of the REMEMBER complex pattern in Eliza's script .

    key: remember 5

      decomp: * i remember *
        reasmb: Do you often think of (2)?
        reasmb: Does thinking of (2) bring anything else to mind?
        reasmb: What else do you recollect?
        reasmb: Why do you recollect (2) just now?
        reasmb: What in the present situation reminds you of (2)?
        reasmb: What is the connection between me and (2)?

      decomp: * do you remember *
        reasmb: Did you think I would forget (2)?
        reasmb: Why do you think I should recall (2) now?
        reasmb: What about (2)?
        reasmb: goto what // <-- Ignore this for now
        reasmb: You mentioned (2)?

The above definition says:

  1. This pattern handles the case when the user's input contains the word "REMEMBER".

  2. The rank of this pattern is 5.

  3. If the user's input matches (i.e. can be decomposed into) "* I REMEMBER *" (where * means "zero or more words") then the pattern will create (i.e. assemble) a response by choosing one of the following reassembly rules:

        reasmb: Do you often think of (2)?
        reasmb: Does thinking of (2) bring anything else to mind?
        reasmb: What else do you recollect?
        reasmb: Why do you recollect (2) just now?
        reasmb: What in the present situation reminds you of (2)?
        reasmb: What is the connection between me and (2)?

    (2) stands for the second set of words ("*") in the decomposition rule.

  4. If the user's input matches (i.e. can be decomposed into) "* DO YOU REMEMBER *" (where * means "zero or more words") then the pattern will create (assemble) a response by choosing one of these reassembly rules:

        Did you think I would forget (2)?
        Why do you think I should recall (2) now?
        What about (2)?
        You mentioned (2)?

    As before, (2) stands for the second set of words ("*") in the decomposition rule.

Walking through a complex pattern's response generation

Let's try this out. Pretend we have a class called RememberPattern that derives from ComplexPattern, and represents the REMEMBER pattern in Eliza's script.

Suppose you told Eliza:

    I REMEMBER WHEN I WAS A CHILD AND TASTED COCA COLA FOR THE FIRST TIME.

Your input would match the RememberPattern, because it contains the word "REMEMBER".

  1. RememberPattern would look for the first decomposition rule that matched your input (ignoring any differences in case for now), which is:

        * I REMEMBER *

    The first * would map to nothing (since nothing precedes "I REMEMBER" in your input), while the second * would map to "WHEN I WAS A CHILD AND TASTED COCA COLA FOR THE FIRST TIME".

  2. RememberPattern would then transform some words in the phrase that mapped to (2). Specifically, it would change "I WAS" to "YOU WERE", causing the phrase to now read "WHEN YOU WERE A CHILD AND TASTED COCA COLA FOR THE FIRST TIME".

  3. Then, just like FormatFreePattern, RememberPattern would look for the next response to select. If this is the first time RememberPattern was running, it would select the first response:

        DO YOU OFTEN THINK OF (2)?

  4. Finally, RememberPattern would replace "(2)" with the transformed phrase, generating the output:

        DO YOU OFTEN THINK OF WHEN YOU WERE A CHILD AND TASTED COCA COLA FOR THE FIRST TIME?

How cool is that!

HOW WOULD ELIZA RESPOND IF WE TOLD IT "Sometimes I remember when there was hardly any traffic in Bombay." USE THE ABOVE ALGORITHM TO FIGURE THIS OUT.

The DecompReassemblyRule class

At the heart of a complex pattern is the process of decomposing the user's input and reassembling it into a response. In the previous example, the process was defined by this script segment:

    decomp: * i remember *

      reasmb: Do you often think of (2)?
      reasmb: Does thinking of (2) bring anything else to mind?
      reasmb: What else do you recollect?
      reasmb: Why do you recollect (2) just now?
      reasmb: What in the present situation reminds you of (2)?
      reasmb: What is the connection between me and (2)?

A complex pattern can have many such segments. Let's create a class that represents the decomposition/reassembly bundle. We'll call it DecompReassemblyRule. This class will need to hold:

  1. The decomposition string.
  2. A list of reassembly strings.
  3. The index of the last used reassembly string (similar to how FormatFreePattern tracks its last used response string).

GO AHEAD AND WRITE THIS CLASS. WHEN YOU'VE FINISHED, COMPARE IT TO THE CODE BELOW.

You probably created something like this:

The DecompReassemblyRule class we just created doesn't do much more than store some data. But it's a start. We'll add functionality to it later, as needed. For now, let's use DecompReassemblyRule in ComplexPattern so we can construct one of Eliza's complex patterns.

Using DecompReassemblyRule in ComplexPattern

You may recall that format free patterns (like BecausePattern) could be constructed because FormatFreePattern provided its subclasses with an Initialize() method that set up the class for use. Looking at the constructor of BecausePattern may jog your memory.

We need something similar in order to construct complex patterns. In other words, we need an Initialize() method for ComplexPattern that looks like this:

The parameters passed to Initialize() will need to be stored in ComplexPattern's private variables (just as is done in FormatFreePattern). So let's add these variables to ComplexPattern.

YOU SHOULD NOW BE ABLE TO WRITE THE INTERNALS OF ComplexPattern.Initialize().

Writing the ComplexPattern.GenerateResponse() method

Remember that we cloned ComplexPattern from FormatFreePattern? Obviously we can't use the cloned GenerateResponse() method we stole from FormatFreePattern because the two classes have different private variables. So let's take a stab at writing ComplexPattern.GenerateResponse().

    /// <summary>
    /// Generates a human-like response, if possible.
    /// </summary>
    /// <param name="input">The user's input.</param>
    /// <returns>
    /// The response or <see langword="null"/> if no response could be generated.
    /// </returns>
    public string GenerateResponse
        (string input)
    {
        // Find the first decomp/reassembly rule (if any) that can decompose the user's input.
        // DecompReassemblyRule doesn't yet have a CanDecompose() method, so we'll need to
        // write it.
        DecompReassemblyRule selectedRule = null;
        foreach(DecompReassemblyRule rule in this._decompReassemblyRules) {
            if (rule.CanDecompose(input)) {
                selectedRule = rule;
                break;
            }
        }

        // If none, return a null string
        if (selectedRule == null) {
           return null;
        }

        // Otherwise, delegate the work of generating the response to the rule.
        // DecompReassemblyRule doesn't yet have a GenerateResponse() method, so we'll need to
        // write it.
        string response = selectedRule.GenerateResponse(input);

        // Return the response
        return response;
    }

Well, that was easy, because we broke down the problem of writing ComplexPattern.GenerateResponse() into 2 smaller problems that we conveniently (heh heh) delegated to DecompReassemblyRule.

  • DecompReassemblyRule.CanDecompose()
  • DecompReassemblyRule.GenerateResponse()

Good old stepwise refinement in action!

DecompReassemblyRule.CanDecompose() and DecompReassemblyRule.GenerateResponse()

Ready to tackle these methods? I'm not, so I'm going to be lazy and just stub them for now, like so:

  • DecompReassemblyRule.CanDecompose() will always return true.
  • DecompReassemblyRule.GenerateResponse() will always return the string "I LOVE MASALA DOSAS."  Heck, why not?

We're stubbing these methods because they require some thought in order to write them. So rather than building them, let's at least get our latest version of Eliza to compile and run correctly, so we can rest assured that the changes we've made in this step haven't caused Eliza to stop working properly.

This is what our stubbed methods look like.

Our Eliza project should now compile without errors. But we'll see this warning about a private variable in DecompReassemblyRule. Don't worry about it for now.

Allowing Eliza to consume ComplexPatterns

We're now almost ready to give Eliza our first ComplexPattern. But if you take a look at Eliza.cs, you'll notice that Eliza contains a list of FormatFreePattern instances. How do we get this list to store instances of ComplexPattern?

By refactoring (moving the common stuff out of) ComplexPattern and FormatFreePattern into a new Pattern base class, and making Eliza store a list of Pattern instances instead of a list of FormatFreePattern instances. Because ComplexPattern and FormatFreePattern both derive from Pattern, we can add instances of either class to the list. This is what the Pattern | FormatFreePattern | ComplexPattern hierarchy looks like:

    Pattern

        - FormatFreePattern
            - AlikePattern
            - AlwaysPattern
            ...
            - YesPattern

        - ComplexPattern
            - RememberPattern
            ...

Real-world example

Isaac Newton School has elementary school, middle school and high school students. Each student is an instance of one of the following C# classes:

  • ElementarySchoolStudent
  • MiddleSchoolStudent
  • HighSchoolStudent

Instead of creating three types of lists to track its enrollment, the smart folk at Isaac Newton created the following class hierarchy:

    Student
        - ElementarySchoolStudent
        - MiddleSchoolStudent
        - HighSchoolStudent

and used a single list of the type:

    List<Student> allStudents = new List<Student>();

to store any kind of Student, allowing the list to be populated by writing code like:

    allStudents.Add (new ElementarySchoolStudent("Bobby Smith"));
    allStudents.Add (new MiddleSchoolStudent("Mary Doe"));
    allStudents.Add (new HighSchoolStudent("Ishan Shastri"));
    allStudents.Add (new ElementarySchoolStudent("Joe Johnson"));
    allStudents.Add (new ElementarySchoolStudent("Melissa White"));
    allStudents.Add (new HighSchoolStudent("Bob Howard"));
    allStudents.Add (new MiddleSchoolStudent("Gary Browne"));
    ...

TAKE A LOOK AT Eliza.cs FROM THE PREVIOUS STEP. HOW WOULD YOU CHANGE IT SO THAT IT CAN STORE INSTANCES OF BOTH FormatFreePattern AND ComplexPattern CLASSES?

Testing our changes

We've made quite a few changes to Eliza. Although we haven't yet created an instance of ComplexPattern, let's make sure we haven't broken anything. Run Eliza and verify that it continues to be able to conduct the following conversation.

It's time to create the REMEMBER ComplexPattern and reap the benefits of all the hard work we've done in this step.  We'll do that in the next step!

 

Most of the content at this site is copyright © Ravi Bhavnani.
Questions or comments?  Send mail to ravib@ravib.com