Simple Chat Bot

While working on Stilpo, I wrote a simple chat bot. This post takes a look at how it works and how it could be expanded. It’s based on ideas of Paradigms of Artificial Intelligence Programming: Case Studies in Common Lisp by Peter Norvig.

Heart of the Tiny Rule Engine is unification and filling in assertions with variables picked up from unifications. Pattern matching isn’t limited to that, but can be used (among many, many other things) to write a little chat bot program in style of Eliza or Kalle Kotipsykiatri.

Idea behind the program is to have a set of patterns and responses to those patterns. Each pattern and response is written in most general way, which tries to ensure that it works in many different cases. For example, if user types “I like chocolade” or “I like strawberries”, computer might respond “In which way your life would be different if you didn’t like strawberries?”. This is expressed as a pattern: I like X and response as In which way your life would be different if you didn’t like X?. Having couple different variations of the response helps keeping the program not repeating itself.

We’ll express pattern and responses as s-expressions as they’re easy to manipulate by routines of Stilpo library. There are couple limitations here though. First of all, we need to way to transform user typed text into s-expression and response back to string. Second is that there are many special characters that have meaning in s-expression and we need to be careful not to allow those through. Luckily for us, Hy has function for us that translates string into s-expression: read-str (provided that it’s valid). So our main loop looks roughly like:

(-> (input "?: ")

input prompts user to input some text, which is then stripped of punctuation and wrapped in parentheses by encode. Resulting string is (hopefully) parsed into s-expression by read-str after which the actual work of locating and formulating a response is done by find-match. Finally reply converts s-expression back into string after which it’s printed on screen. Processing is now done and user can enter more text that gets fed through the machinery just like before.

In order to understand how find-match works, we first need to talk about how rules are expressed. Following is an example of a rule:

(, '(I need *x)
   (, '(Why do you say, you need *x ?)
      '(What would it mean to you, if you got *x ?)
      '(Would having *x make you happy ?)))

It’s a tuple of two elements. First of them defines pattern that user input needs to match for rest of the rule to trigger. Second element is tuple containing one or more possible responses. Wildcards are allowed (encouraged even) to allow writing rules that respond to more than just one specific input. find-match tries each of the rules and returns a random response of the first one that matches. In code that is expressed as:

(defn find-match [query]
  (->> (map (fn [rule]
              (setv response (unify query (first rule) {} (,)))
              (when (is-not None response)
                (fill-assertion (.choice random (second rule))
       (filter (fn [response]
                 (is-not None response)))

Note how fill-assertion is used to fill in the user entered text into response. This is how system can try to react to multiple different kinds of inputs, as long as they match to some rule.

In order to have some sort of response always available, we have defined two very general rules:

(, '(?x)
   (, '(You are not very talkative today)
      '(Could you elaborate that a bit?)))

(, '(*x)
   (, '(Please, elaborate)
      '(Interesting, please continue)))))

First one matches to single word input, while second one matches to anything. As these are defined as two very last entries on the list of rules, they serve as backup rules in cases where nothing else matches.

And example output from the program could be like this:

?: Hello
Greetings, nice to see you
?: I would like to talk about something
Please, elaborate
?: I need a new computer
Why do you say, you need a new computer ?
?: I’m not happy how slow the old one is
Image that you are happy how slow the old one is how does that make you feel?
?: pretty silly, truth to be told
Please, elaborate
?: I’m working on slow code and need faster computer
Are you happy when you are working on slow code and need faster computer ?
?: I’m not happy with slow code
Image that you are happy with slow code how does that make you feel?
?: This is silly
Interesting, please continue
?: bye

Mostly, almost, intelligent, save couple places where some extra punctuation would help. Of course if the user is unco-operative output wouldn’t be this decent.

If one wanted to improve the system, there would need to be quite a bit more rules than is in the example. There could be some for directing conversation to different topics and ones that try to wildly guess what the user had in mind: I was wondering, if you have any ideas about kite building?, I love kites! Tell me more!. Which would be expressed as rule:

(, '(*x kite *y)
   (, '(I love kites! Tell me more!)))

Another trick is to save keywords that program has recognized and use sentences based on them when the program fails to match anything sensible. If the user had mentioned kites earlier, program could later respond like:

?: ’Twas brillig, and the slithy toves; Did gyre and gimble in the wabe;
You mentioned kite earlier, would you like to tell me more?

Leave a Reply

Fill in your details below or click an icon to log in: Logo

You are commenting using your account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s