Eliza 12 - Beefing Up DecompReassemblyRule

by Ravi Bhavnani


Until now, our complex patterns have all been of the form:

    <optional text before key phrase>  <key phrase>  <text after key phrase>

But Eliza's script also contains patterns that have multiple key phrases interspersed with optional text. For example, one of the decomposition rules in the "I" pattern is:

     decomp: * I * YOU *
        reasmb: Perhaps in your fantasies we (2) each other.
        reasmb: Do you wish to (2) me ?
        reasmb: You seem to need to (2) me.
        reasmb: Do you (2) anyone else ?

We need to beef up our DecompReassemblyRule class so it can handle rules that have multiple key phrases.

Making DecompReassemblyRule.CanDecompose() parse rules with multiple key phrases

At the heart of DecompReassemblyRule lies the CanDecompose() method, which checks whether the rule can decompose the input it's given. In order to handle a decomposition pattern that has multiple key phrases, this method needs to:

  1. Extract each element of the decomposition definition. If the decomposition was:
        * BLAH BLAH * BLAH BLAH BLAH *
    
    the extracted elements would be:
        Element 1 = *
        Element 2 = BLAH BLAH
        Element 3 = *
        Element 4 = BLAH BLAH BLAH
        Element 5 = *
    
  2. For each decomposition element, find the corresponding text fragment in the user's input. If a corresponding text fragment isn't found, return false to indicate the user's input can't be decomposed by this rule.

  3. Store the found fragments for later use when generating a response.

Extracting each element of the decomposition definition

Breaking up the decomposition definition into individual elements is done by inspecting each word in the definition and gathering them as we proceed. To break the definition into words, use the String.Split() method. You can simply pass null as a delimiter to handle the common case of using whitespace as word separators, as shown below:

    string decompPattern = "* BLAH BLAH * BLAH BLAH BLAH *";
    foreach(string word in decompPattern.Split(null)) {
        // Hard work comes here...
    }

When we're done, we should end up with a list of strings that contains:

    Element 1 = *
    Element 2 = BLAH BLAH
    Element 3 = *
    Element 4 = BLAH BLAH BLAH
    Element 5 = *
Think about how you'll gather up the * and key phrase elements from the individual words. When you're done, compare your solution with the algorithm below.

    for (each word in the decomposition definition) {
        if (1) this is the first word -or-
           (2) the word contains "*" -or-
           (3) the previously extracted element is "*" {
            add the word to the list of elements
        }
        else {
            concatenate the word to the last element in the list
        }
    }
Now try to write the above algorithm in C#. When you're done, compare your solution to the code shown below.

Checking if the extracted elements match the user's input

Once we've extracted the the elements of the decomposition definition, we need to check if they match the user's input. This is done by finding (in the user's input) the corresponding text fragment for each element.

Think about how you'll go about doing this. When you're finished, compare your solution to this algorithm.

    // Some initialization
    (1) define a list (e.g. "_textElements") to store matched text elements (strings);
    (2) define an index (an int) that points to the start of the user's input

    for (each decomposition element) {

        if (the element is "*" (which represents optional text)) {
            gather this text and add it to the _textElements list;
        }
        else {
            // Otherwise, the element is a key phrase we're trying to match

            if (the element isn't in the user's input) {
                return false;
            }

            // The key phrase was found in the user's input.  So:
            (1) store the user input text (that occured before the matched element)
                in the _textElements list;
            (2) store the matched element itself in the _textElements list;

            reposition the index to where we want to continue searching for elements
            in the user's input;
        }
    }

    // Since we haven't returned "false" from the previous loop, we've found
    // all elements.  So return true (to indicate a successful match).
    return true;
Try to write the above algorithm in C#. When you're done, compare your solution to the code shown below.

Have we broken anything?

Before we use our new and improved DecompReassemblyRule class to define patterns like the I pattern, pause for a minute and ask yourself if our changes have caused any existing behavior to break. As it turns out, they have.

The previous version of DecompReassemblyRule was simplistic and only stored text elements that occured before and after the key phrase being matched. These text elements were represented by the numbers (1) and (2) respectively, in the reassembled responses.

This allowed us to define the IF pattern as:

So any reassembled response that used text element (2) will now have to use (3), causing us to have to rewrite the IF pattern as:

MAKE THE APPROPRIATE CHANGES TO ALL CLASSES DERIVED FROM ComplexPattern. THEN, VERIFY THAT YOUR CHANGES ARE CORRECT BY TESTING EACH DECOMPOSITION RULE OF EACH COMPLEX PATTERN.

TESTING, TESTING, TESTING!  IT'S THE ONLY WAY TO WRITE SOFTWARE!

Time to use the new and improved DecompReassemblyRule!

It's time to put the new and improved DecompReassemblyRule to work. Let's start by creating the I pattern. For now, skip any decomposition rules with key phrases that have words that begin with the @ sign. For example:

  decomp: * i @desire *
    reasmb: What would it mean to you if you got (3) ?
    ...
    reasmb: What does wanting (3) have to do with this discussion ?

In the above rule, @desire refers to a set of synonyms like:

    want
    crave
    covet
    yearn for
    ...

We'll handle synonym sets in a future step.

Go ahead and write IPattern. When you're done, compare your work with the class in this step's solution.

Testing IPattern

Test your work by telling Eliza:

    I THINK I'M A KANGAROO.

This should cause IPattern's "* I AM *" decomposition rule to fire, generating the response:

    IS IT BECAUSE YOU ARE A KANGAROO THAT YOU CAME TO ME?

Instead, this happens:

WHAT WENT WRONG? HOW WILL YOU FIX IT?
Hint: To answer the second question, you must first answer the first one.

If you can't figure out the problem, run this step's solution and feed it the same input and see how Eliza responds.

Next steps

We've done a lot in this step! By making DecompReassemblyRule.CanDecompose() parse rules with multiple key phrases, we've come closer to following Eliza's original script, thereby generating more human-like responses.

In the next step, we'll attempt to make Eliza sound even more human!

 

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