Eliza chatbot

This is a psychotherapist chatbot called Eliza, written in uLisp. It's a simple version of the classic artificial intelligence program ELIZA, written by Joseph Weizenbaum between 1964 and 1966 at the MIT Artificial Intelligence Laboratory [1], which can reasonably claim to be the mother of all chatbots.

Like a good psychotherapist, Eliza generally attempts to turn around what you say to it, and give it back to you, to sound like it has human understanding but without giving you any real information. The program here can easily be extended to give Eliza more topics of conversation, subject to the amount of memory available to uLisp.

It's based on a small Eliza program I wrote on my first 6800-based Lisp implementation, and is updated with ideas from the Eliza chapter in Peter Norvig's classic book on AI [2].

Updates

24th March 2018: Slightly changed the definition of match to make it easier to understand.

10th August 2019: Added a note about the correct Serial Monitor line ending setting.

10th February 2023: Improved the program to produce the output in a nicer format, and added more information about how to run the program.

Running the Eliza chatbot

The Eliza chatbot will run in uLisp on most platforms except the Arduino Uno, which doesn't have enough memory.

  • Download the latest version of uLisp for the platform from Download uLisp.
  • Use the Arduino IDE to upload it to board; see Download uLisp - Installing uLisp.
  • Open the Serial Monitor in the Arduino IDE to display the uLisp prompt.
  • Display the source of the Eliza program here: Eliza chatbot.
  • Select the text of the program, copy it, paste it into the Arduino IDE Serial Monitor input field, and press Return.
  • Optionally save an image of the game onto the board's non-volatile memory by typing:
(save-image)
  • Run the game by typing the following command into the Arduino IDE Serial Monitor input field, followed by Return:
(eliza)
  • Give your commands after the ':' prompt by typing them into the Arduino IDE Serial Monitor input field, followed by Return.
  • To exit type:
: bye

For best results type your input in lower-case. If you're using the Arduino Serial Monitor the line ending should be set to Newline. Have fun!

Sample output

Here's some sample output. The human input is written after the ":" prompt, and the computer's replies are in brackets:

> (eliza)
: hello
hello. what's up?
: I hate my computer
what makes you hate your computer ?
: I wish it ran faster
why would it be better if it ran faster ?
: I feel I waste a lot of time
do you often feel you waste a lot of time ?
: most days
tell me more.
: bye
nil

With the small sample set of rules it will soon start to repeat itself, but you can easily add more rules to extend its repertoire.

The program

The main routine in the program is the pattern matching routine, match, which looks for patterns in the user's input. It takes a pattern string and an input string, and returns t if there's a match and nil otherwise:

> (match '(all is well) '(all is well))
t

> (match '(all is well) '(all not well))
nil
The pattern can contain wildcard characters "*" each of which matches zero or more words; each wildcard is followed by a variable name that gets assigned the matched substring in the global variable *bindings*: 
> (match '(* x chase * y) '(dogs chase cats and sheep))
t

> *bindings*
((x dogs) (y cats and sheep))

Here's the definition of match:

(defun match (pat in)
  (cond
   ((null pat) (null in))
   ((eq (car pat) '*) (wildcard pat in))
   ((eq (car pat) (car in)) (match (cdr pat) (cdr in)))
   (t nil)))

When it encounters an asterisk it calls wildcard to handle the wildcard match:

(defun wildcard (pat in)
  (cond
   ((match (cddr pat) in) (bind (cadr pat) nil) t)
   ((null in) nil)
   ((match pat (cdr in)) (bind (cadr pat) (car in)) t)
   (t nil)))

The routine wildcard calls bind to make the bindings in *bindings*.

(defun bind (var value)
  (cond
   ((assoc var *bindings*)
    (push (swap value) (cdr (assoc var *bindings*))))
   (t (push (cons var (swap value)) *bindings*))))

The routine subs takes a list and substitutes the values of the variables from *bindings*. So, assuming the value of *bindings* shown above we could write:

> (subs '(I think y are chased by x))
(i think cats and sheep are chased by dogs)

When strings are matched by match, their viewpoint is changed by the substitution of words from the list of word pairs in *viewpoint*:

(defvar *viewpoint* '((I you) (you I) (me you) (am are) (was were) (my your)))

This gets the matched strings ready to be echoed back by the program. The substitution is performed by swap:

(defun swap (value)
  (let ((a (assoc value *viewpoint*)))
    (if a (cadr a) value)))

The Eliza rules are defined in the variable *rules*:

(defvar *rules*
  '(((* x hello * y) (hello. what's up?))
    ((* x i want * y) (what would it mean if you got y ?) (why do you want y ?))
    ((* x i wish * y) (why would it be better if y ?))
    ((* x i hate * y) (what makes you hate y ?))
    ((* x if * y)
     (do you really think it is likely that y)
     (what do you think about y))
    ((* x no * y) (why not?))
    ((* x i was * y) (why do you say x you were y ?))
    ((* x i feel * y) (do you often feel y ?))
    ((* x i felt * y) (what other feelings do you have?))
    ((* x) (you say x ?) (tell me more.))))

Each rule consists of:

A pattern: for example:

(* x i want * y)

A list of one or more responses: for example:

(what would it mean if you got y ?)
(why do you want y ?)

The program finds the first rule that matches, and then chooses one of the responses at random, using the routine random-elt:

(defun random-elt (list)
  (nth (random (length list)) list))

Finally, here's the program that runs Eliza:

(defun eliza ()
  (loop
   (princ ": ")
   (let* ((line (read-line))
          (input (read-from-string (concatenate 'string "(" line ")"))))
     (when (string= line "bye") (return))
     (setq *bindings* nil)
     (let ((reply (princ-to-string
                   (dolist (r *rules*)
                     (when (match (first r) input)
                       (return 
                        (subs (random-elt (cdr r)))))))))
       (terpri)
       (princ (subseq reply 1 (1- (length reply))))
       (terpri)))))

It finds the response from the matching rule in reply, converts it from a list to a string using princ-to-string, and then removes the bracket at the start and end using subseq.

Resources

Here's the whole Eliza program in a single file: Eliza chatbot.

Further suggestions

As presented here the program takes a string typed into the Serial Monitor, and displays the response in the window below. However, it could be used for a chatbot via SMS text messages, based on something like the uLisp GSM server, or via a web interface, using the wi-fi interface available in ESP8266.


  1. ^ ELIZA on Wikipedia.
  2. ^ Norvig, Peter "Paradigms of Artificial Intelligence Programming" Morgan Kaufmann Publishers, Inc, San Francisco, 1992, pp 151-174, available as a PDF paip-lisp on GitHub.

Previous: Animals

Next: Simple arcade game