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:
- Remember statements made by the user.
- 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:
-
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.
-
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.
|