Learning Prolog: Semester 2 Lab 10 - Eliza
The lab numbering has gone a little bit strange this semester it seems (there was a lab 10 last semester), so I've added the semester number to the post title to avoid confusion.
This week's lab was all about Eliza. In case you don't know who (or what) an Eliza is, Eliza is an early attempt at building a chatbot. It isn't very clever, and relies on keywords to understand what you are saying. It's still used today though - mainly in NPCs in games, as they only have to deal with a very limited situation.
Anyway, let's begin building our very own Eliza. First of all, we need a prompt loop, to repeatedly ask the user for more input:
eliza :-
write('Hello! My name is eliza.'), nl,
eliza_loop.
eliza_loop :-
write('Eliza > '),
read(Input),
write('You said '), write(Input), nl,
eliza_loop.
Here I define eliza/0
, which welcomes the user and then jumps into eliza_loop/0
, which is the main input loop. The main loop simply writes a prompt, asks for input, echoes the input back, and goes back around all over again. Here's some example output:
eliza.
Hello! My name is eliza.
Eliza > test.
You said test
Eliza > cheese.
You said cheese
Eliza > [1,2,1,2,'testing...'].
You said [1,2,1,2,testing...]
Eliza >
This isn't very useful yet - it just echoes back what we say! Let's make it more complicated:
eliza :-
write('Hello! My name is eliza.'), nl,
eliza_loop.
eliza_loop :-
write('Eliza > '),
read(Input), respond(Input).
respond(Input) :-
member(Term, Input),
member(Term, [ quit, exit, leave ]),
write('Goodbye!').
respond([my,name,is,Name | _ ]) :-
write('Hello, '), write(Name), write('! Pleased to meet you.'), nl,
eliza_loop.
In the above I've changed the main loop up a bit to call respond/1
instead of blindly looping back around. This lets me write rules like those starting on lines #8 and #12 that do something depending on the user's input. We can get our eliza to say goodbye and exit by not looping back around if the user mentions the words "quit", "exit", or "leave", or say hello when someone tells us their name and loop back around again.
There are also two different ways of detecting keywords in the user's input: a double member/2
, or a preset list in the rule definition. Examples:
respond([my,name,is,Name | _ ]) :-
write('You started the input with "my name is".').
respond(Input) :-
member(Term, Input),
member(Term, [ quit, exit, leave ]),
write('Detected the word '), write(Term), write(' in the input.').
While detecting the items in a list is fairly straightforward, the double member isn't as obvious. It works by taking advantage of Prolog's backtracking. It provides a choice point for each word in the phrase, and then it loops over each of the keywords that we are searching for. If it finds that the current word isn't equal to any of the keywords that it is searching for, it will fail, backtrack, pick the next word, and try again. Another way of looking at it is that Prolog is trying all possible combinations of the input words and the keywords until it finds a match.
If you are following this post as a guide, at this point you can (and should!) add your own custom phrases to practice the techniques described here. Once you have done that, come back here.
Next up, we will add some 'fallback' phrases to our eliza to use when it doesn't understand what has been said. At the moment, it just fails with an impolite false.
, which isn't any good at all! To do this, we need a list of fallback phrases:
list_of_excuses(['I see.', 'Very interesting.', 'Tell me more.', 'Fascinating.']).
Next we need to get our eliza to loop through our list of fallback phrases in order. We can do this by making the fact that contains the list of fallback phrase dynamic, and then updating it every time we use one. To make something dynamic, you use the dynamic/1
predicate to tell Prolog that fact is dynamic and not static (this is because Prolog applies some clever optimisations to static things that don't work for dynamic things):
:- dynamic list_of_excuses/1
You can use this to tell Prolog that anything in your Prolog program is dynamic. Just change list_of_excuses
to the name of the fact or rule, and change the 1
to the arity of the fact or rule that you want to make dynamic.
Next, we need to add a 'catch all' rule at the bottom of our respond/1
definition:
respond([ _ ]) :-
retract(list_of_excuses([ Next | Rest ])),
append(Rest, [ Next ], NewExcuseList),
asserta(list_of_excuses(NewExcuseList)),
write(Next), nl,
eliza_loop.
Several things are happening here. firstly, we retract the list of fallback phrases from Prolog's knowledge database and split it up into the first item in the list, and the rest of the items in the list.
Next we create a new list that contains the phrase that we at the front at the end, and then we put this new list back into Prolog's knowledge database.
Lastly, we output the fallback phrase that was at the beginning of the list (but is now at the end) to the user as our reply, before looping back around again with exliza_loop/1
.
With that, we our eliza can respond to phrase containing certain keywords, exit when asked, and reply with a fallback phrase if it doesn't understand what was said. Below I have put a small questions and answers type section containing a few of the problems that I had whilst writing this. I've also included the complete Prolog source code that this post is based on.
Common Problems
I had several problems whilst writing this program. I've included a few of them below.
My eliza fails when it doesn't understand something instead of outputting an error.
I found that I didn't get an error message if I included :-
after the retract statement
. Removing it made it output the error below which I was then able to solve.
I get this error when writing the fallback phrase bit:
ERROR: No permission to modify static_procedure Name/Arity
I'm certain that I put the
dynamic name/arity
at the top of my prolog source code, but it doesn't work.
I got this because I forgot the :-
before the word dynamic
. If you are getting this error, this isthe reason why.
Source Code
:- dynamic list_of_excuses/1.
list_of_excuses(['I see.', 'Very interesting.', 'Tell me more.', 'Fascinating.']).
eliza :-
write('Hello! My name is eliza.'), nl,
eliza_loop.
eliza_loop :-
write('Eliza > '),
read(Input), respond(Input).
respond(Input) :-
member(Term, Input),
member(Term, [ quit, exit, leave ]),
write('Goodbye!').
respond([my,name,is,Name | _ ]) :-
write('Hello, '), write(Name), write('! Pleased to meet you.'), nl,
eliza_loop.
respond([my,Thing,is,called,Name | _ ]) :-
write(Name), write(' is a nice name for a '), write(Thing), write('.'), nl,
eliza_loop.
respond(Input) :-
member(Animal, Input),
member(Animal, [ cat, dog, fish, hamster, gerbil, snake, tortoise ]),
write('You just mentioned your '), write(Animal), write('. Tell me more about your '), write(Animal), nl,
eliza_loop.
respond(Input) :-
member(Term, Input),
member(Term, [ hate, dislike ]),
member(Term2, Input),
member(Term2, [ you ]),
write(':('), nl,
eliza_loop.
respond([ _ ]) :-
retract(list_of_excuses([ Next | Rest ])),
append(Rest, [ Next ], NewExcuseList),
asserta(list_of_excuses(NewExcuseList)),
write(Next), nl,
eliza_loop.