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 , 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.
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.
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 .
I tested the program on an Arduino Mega 2560, but it should run on any of the platforms except the Arduino Uno, which doesn't have enough memory.
Here's some sample output from the Eliza chatbot. The human input is written after the ">" prompt, and the computer's replies are in brackets:
> hello (hello. what's up?) > I hate my computer (what makes you hate your computer ?) > I wish it would run faster (why would it be better if it would run faster ?) > I feel I waste a lot of time (do you often feel you waste a lot of time ?) > most days (tell me more.)
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 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
> (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) (print (dolist (r *rules*) (when (match (first r) input) (return (subs (random-elt (cdr r))))))) (terpri))))
Run it by typing:
To exit type:
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!
Here's the whole Eliza program in a single file: Eliza chatbot.
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.