Eliza 16 - Improving Eliza's responses

by Ravi Bhavnani


In this step, we're going to improve some of Eliza’s responses by making them more human. Specifically, we’re going to:

  • Improve IPattern’s catch-all rule
  • Improve IPattern’s “I CAN'T” rule
  • Give Eliza the ability to recall earlier statements made by the user

Improving IPattern’s catch-all rule

Eliza uses the IPattern class to handle input that contains the keyword “I”. If none of the rules in IPattern match the user’s input, the catch-all rule (rule # 11) is used to decompose the input and reassemble a response. Here's what the rule looks like.

Which causes Eliza to respond like this.

While not incorrect, these responses appear simplistic. If we change the decomposition from “*” to “* I *” and tweak the generated responses, we get a slightly more interesting conversation.

Improving IPattern’s “I CAN'T” rule

IPattern’s “I CAN'T” rule (rule # 6) causes Eliza to generate the following responses:

We’ll add a few more responses to make them sound less monotonous.

Recalling earlier statements

When Eliza can't match the user's input to a pattern, it uses GenericResponsePattern to respond with one of these phrases:
    I'M NOT SURE I UNDERSTAND YOU FULLY.
    PLEASE GO ON.
    WHAT DOES THAT SUGGEST TO YOU?
    DO YOU FEEL STRONGLY ABOUT DISCUSSING SUCH THINGS?

It would be more realistic if Eliza instead recalled something the user had said earlier in the conversation, giving the illusion that it's trying to associate a previously spoken statement with what the user has just entered. In order to do this, Eliza needs to:

  1. Remember statements made by the user.
  2. Recall them when it can't find a pattern that matches the user's input.

In order to remember the user's statements, we'll give Eliza a memory, which is simply an object that can store and recall text. It would also be useful to also know if the memory is empty. So let's create the class Memory which looks like this:

We'll make an instance of the Memory class a property of Eliza. Eliza will use its memory in the following manner:

  1. Decompostion rules that recognize statements (not questions) spoken by the user will store a form of the recognized statement in Eliza's memory for possible use later on.
  2. The GenericResponsePattern class that until now contained the four responses shown above will be enhanced to include responses that reference statements in Eliza's memory.

Storing statements in memory

Take a look at this decomposition rule in the IPattern class.

This rule maps an input like this:

    I WANT TO EAT ICE CREAM FOR BREAKFAST.

to a response like this:

    WHAT WOULD IT MEAN TO YOU IF YOU GOT TO EAT ICE CREAM FOR BREAKFAST?

Clearly the user is making a statement that he or she wants to eat ice cream for breakfast. The decomposition rule needs to store this statement in Eliza's memory. The statement happens to be the third text element in the pattern

    * I @DESIRE *

When the statement is recalled from memory, Eliza will say something like:

    EARLIER YOU SAID <statement-in-memory>.

Although the third text element is "TO EAT ICE CREAM FOR BREAKFAST", what should be stored in memory is "YOU WANT TO EAT ICE CREAM FOR BREAKFAST". In other words, the third text element should be formatted into a larger string that can be represented as:

    YOU WANT TO EAT ICE CREAM FOR BREAKFAST

or better, yet:

    YOU WANT {3}

The DecompReassemblyRule constructor will therefore need additional information that identifies what (if anything) the rule should store in memory. This is what the new version of DecompReassemblyRule's constructor looks like:

Notice that the new parameter is an optional parameter. A parameter is identified as optional by specifying the default value to be used when the parameter is omitted. Making the new parameter optional allows it to be omitted by patterns that derive from ComplexPattern that don't need to store the user's input in Eliza's memory. Only complex patterns that recognize statements made by the user will need to store content in Eliza's memory.

For a brief tutorial on optional C# parameters, see the article C# | Optional Parameters.

DecompReassemblyRule's constructor stores the new parameter in a local variable _patternOfTextToStoreInMemory and its CanDecompose() method stores the required text in Eliza's memory, if the parameter isn't null. Here's how it does it:

Retrieving statements from memory

Statements will be retrieved from memory by referencing them in GenericResponsePattern's responses. GenericResponsePattern, which until now has been a FormatFreePattern will derive from ComplexPattern (because it uses decomposition rules).

We need to extend the syntax of a decomposition rule's response to include the ability to reference Eliza's memory. Previously, a response looked like this:

    WHAT WOULD IT MEAN TO YOU IF YOU GOT (3)?

where "(3)" represents the third text element. We'll introduce a new symbol "($)" to represent the oldest statement stored in Eliza's memory. This allows us to write a response such as:

    EARLIER YOU SAID ($).

which will cause the following response to be generated:

    EARLIER YOU SAID YOU WANT TO EAT ICE CREAM FOR BREAKFAST.

We'll see how this is done in the following section.

Modifying DecompReassemblyRule's response generation logic

DecompReassemblyRule's response generation used to look like this:

which essentially did this:

    select the next response;

    replace any numbered placeholders with the corresponding text elements,
    after transforming them;

The new form of response generation does the following:

    repeat {
        select the next response;
        if the response references the memory {
            if there's text in memory, use it to generate the response;
        }
        else {
            replace any numbered placeholders with the corresponding text elements,
            after transforming them
        }
    }
    until a response has been generated;

which is coded as:

Patterns that write to Eliza's memory

Eliza's memory is written to when a statement is detected. Examples of statements are:

    I WANT TO EAT ICE CREAM FOR BREAKFAST.
    I'M A PROGRAMMER.
    SOMETIMES I SHARPEN PENCILS.
    YESTERDAY I ATE TOAST.
    MY CAR IS VERY OLD.
    ONCE I WAS YOUNG.

Statements are detected by rules in the following patterns. We've modified these rules to add a form of the detected statement to Eliza's memory.

    AmPattern
    CanPattern
    IPattern
    MyPattern
    RememberPattern
    WasPattern

An example of a modified rule is rule #1 in IPattern, which we transformed from this:

to this:

The optional argument "YOU WANT (3)" tells Eliza to store the third text element in its memory, if this rule matched the user's input. So if the user entered:

    I CRAVE CHOCOLATE CAKE.

this rule would match, causing the phrase "YOU WANT CHOCOLATE CAKE" to be stored in Eliza's memory. If GenericResponsePattern later responded with a response that refers to Eliza's memory, e.g.

    EARLIER YOU SAID ($).

Eliza would say:

    EARLIER YOU SAID YOU WANT CHOCOLATE CAKE.

Testing our changes

We've added a lot of new code to Eliza. But are we sure we haven't made any mistakes? Well, we can never be sure, but we can certainly check to ensure we haven't made any obvious errors, by updating our tests to verify the improvements made to GenericResponsePattern.

We do this by giving Eliza a series of inputs, and verifying that we get the expected responses. These inputs will test the improvements to GenericResponsePattern. We'll add the inputs and expected responses to our test program.

If our test succeeds, the test app will display this reassuring message:

Where we've reached

In this step, we improved IPattern's catch-all and "I CAN'T" rules and gave Eliza the power of memory, improving upon the illusion that it's an intelligent being, which of course, it isn't. It's just a program, but an entertaining one, nevertheless!

Next steps

In the next step, we'll add a mechanism to enable Eliza to reveal its thinking process. This will help us diagnose any problems we may encounter with our patterns.

 

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