Part 11 — A complete beginner’s guide to Computer Programming with Clojure: Build a Chatbot.

Harvey Ellams
6 min readJan 6, 2021
Photo by Alexandr Popadin on Unsplash

Recently (2020), I was asked to help facilitate a workshop focused on the Legal community. The workshop explored the potential of Chatbots and Rules Engines. As a quick demo, I knocked up a simple Chatbot. My Chatbot centers around providing the User with assistance in obtaining digital evidence from different countries. Different countries have different procedures. For example, many countries make the process easy with a bilateral treaty such as the Mutual Legal Assistance Treaty or MLAT for short. For a number of European countries, there is another bilateral agreement known as a European Judicial Network agreement (EJN). So, where there is no bilateral agreement then you must seek advice from the UK’s crown prosecution service (CPS). Clearly, if you are in the UK and the digital evidence is also in the UK then no specific action required (Note, this post is from a UK perspective).

Let’s create a chatbot to help a User to ascertain whether a bilateral agreement is in place and if there is, identify the correct one. Our chatbot will guide the User and when the User submits a two-letter country-code, the chatbot will be able to inform the User of the appropriate action. For instance, if the two-letter country-code corresponds to a country inside the EJN then the chatbot will respond accordingly.

REPL.IT

Before we get into the code, let’s look at an online REPL. An online REPL is a handy tool for testing uncomplicated code snippets. By uncomplicated, not reliant upon external libraries.

REPL.IT is one such online REPL:

The code presented is simple enough to work in an online REPL.

Chatbot

Type Listing 1. into your REPL of choice.

Listing 1.

;;UK only
(def UK #{"UK"})

;;EU EJN countries
(def EJN #{"CH" "SI" "IT" "FI" "MK" "LI" "GR" "DK" "ME" "IS" "AT" "LT" "BG" "PT" "HR" "HU" "SE" "RO" "CY" "TR" "AL" "FR" "DE" "NO" "BE" "CZ" "NL" "EE" "LU" "IE" "LV" "SK" "ES" "RS" "MT" "PL"})
;;MLAT bilateral countries
(def MLAT #{"AU" "MY" "DZ" "UY" "GY" "SA" "BH" "JO" "VN" "PA" "LY" "AR" "BR" "CN" "PY" "PH" "BB" "CA" "TH" "KZ" "EC" "AG" "CL" "US" "UA" "GD" "IN" "HK" "CO" "NG" "AE" "MX"})
;;Positive responses
(def Positive #{"yeah" "Y" "yes" "OK" "y" "ok" "Yes" "Yeah"})
;;Negative responses
(def Negative #{"n" "nope" "not" "Nope" "Not" "N" "no" "No"})
;;Indifferent responses
(def Indifferent #{"Not sure" "maybe" "Don't understand" "dont understand" "Possibly" "possibly" "not sure" "Maybe"})
;;FUNCTIONS(defn start []
(println "Do you wish to obtain digital evidence and want to know what authority is required?")
(let [x (read-line) ]
(cond
(contains? Positive x) (Country)
(contains? Negative x) "Perhaps try a different ChatBot!"
(contains? Indifferent x) "Sounds like a training issue, this ChatBot is here to help you get the right authority for digital evidence"
:else (start))))
(defn Country []
(println "Do you know what country the evidence is located?")
(let [x (read-line) ]
(cond
(contains? Positive x) (CountryCode)
(contains? Negative x) "You need to do some more investigation, come back when you have identified the country of location holding the digital evidence"
(contains? Indifferent x) "You need to do some more investigation, come back when you have identified the country of location holding the digital evidence"
:else (start))))
(defn CountryCode []
(println "Do you know the two-letter code for the specific country?")
(let [x (read-line) ]
(cond
(contains? Positive x) (results)
(contains? Negative x) "Try looking up the country code here: https://www.iban.com/country-codes"
(contains? Indifferent x) "Try looking up the country code here: https://www.iban.com/country-codes"
:else (start))))
(defn results []
(println "Just type in the two-letter Contry code in uppercase")(let [x (read-line) ]
(cond
(contains? UK x) "It's in the UK, no special procedures or authorities required"
(contains? EJN x) "This is in the EU judicicial network, follow the European Judicial procedure"
(contains? MLAT x) "This is outside both the UK and EJN. You will have to follow the MLAT procedure"
:else "Unfortunately, this is outside of MLAT. Please seek advice from the Crown Prosecution Service (CPS)")))

To start the chatbot, simply run the (start) function

Let’s breakdown the code:

Sets

Recall, a set is a collection of unique values. Our sets contain either a two-letter country-code or a response type. For instance, Positive responses include, “yeah”, “yes”, and “OK”.

Functions

All the functions presented follow a similar format. That is, they use a conditional which is represented by the keyword cond. This simple, but powerful, format follows a logical system of truth assertion. This format begins with asking the User a question. The response is evaluated:

(println "Do you wish to obtain digital evidence and want to know what authority is required?")
(let [x (read-line) ]
(cond
(contains? Positive x) (Country)

So, the response is read in and stored in x and then compared to the Positive set. If the response is in the Positive set, then this function stops and the Country function is called. Else, it moves to the next line:

(contains? Negative x) "Perhaps try a different ChatBot!"

If none of the conditions are satisfied, then the final line beginning :else is called and executed:

:else (start)

In terms of our programming elements, each function’s initial lines of code are examples of sequence and selection. However, in all but one of the functions, the final :else calls the function (start). By calling this initial starting function, these code segments demonstrate repetition.

Improvements

One of the first issues identified is our chatbot requests two-letter country-codes in upper-case. We can improve our results function by automatically converting the User input into upper-case like so:

(defn results [](println “Just type in the two-letter Country code”)(let [x (clojure.string/upper-case (read-line)) ](cond

This could also be applied to all User input and allow us to simplify our sets. In other words, our sets would only need to contain words or abbreviations in upper-case.

To make it easier on the user, we could let the user input either the full country name or two-letter code. We could also include common errors or typos. For example, the UK set could look something like so:

;;UK only
(def UK #{"UK" "UNITED KINGDOM" "GREAT BRITAIN" "BRITAIN" "BRITISH ISLES" "BRIRAIN" "BRITIAN"})

The user may have a number of inquiries, so we may wish to control how the program ends. For example, you could create a number of functions to be called from within the results function like so:

(defn results []
(println "Just type in the two-letter Contry code in uppercase")(let [x (read-line) ]
(cond
(contains? UK x) (uk_ending)
(contains? EJN x) (ejn_ending)
(contains? MLAT x) (mlat_ending)
:else (ending))))

All these functions could follow a similar format to give the User an opportunity to run further inquiries.


:else (ending))))
.
.
.
(defn ending []
(println "Unfortunately, this is outside of MLAT. Please seek advice from the Crown Prosecution Service (CPS). Would you like to continue with another inquiry?")
(let [x (read-line) ]
(cond
(contains? Positive x) (start)
(contains? Negative x) "Bye"
(contains? Indifferent x) "Bye for now, just type (start) if you wish to continue"
:else "Didn't understand your response. Type (start) to continue")))

SUMMARY

Hopefully, you can see the potential of this pattern or template. For instance, instead of being a chatbot and reading in User responses, data could be brought in and processed. To demonstrate, a program could use the cond pattern to take temperature readings and test against a number of conditions:

(defn thermo_reg []
(let [ current_temp @temperature ]
(cond
(>= current_temp 25) (run_aircon)
(<= current_temp 10) (run_heater)
:else (get_new_temp_reading))))

;;demo of above template but using println statements and randomly generated test data.
(def current_temp (atom (rand-int 30)))

(defn thermo_reg []
(cond
(>= @current_temp 25) (println "run_aircon")
(<= @current_temp 10) (println "run_heater")
:else (println "Just Right")))

(reset! current_temp (rand-int 30)) (thermo_reg)
run_heater

(reset! current_temp (rand-int 30)) (thermo_reg)
run_aircon

(reset! current_temp (rand-int 30)) (thermo_reg)
Just Right

(reset! current_temp (rand-int 30)) (thermo_reg)
run_aircon

(reset! current_temp (rand-int 30)) (thermo_reg)
Just Right

(reset! current_temp (rand-int 30)) (thermo_reg)
Just Right

(reset! current_temp (rand-int 30)) (thermo_reg)
run_heater

The inbuilt function rand-int generates a random integer. The number 30 just defines the range. In other words, (rand-int 30) will generate a random number from 0 to 30.

Play around with code in this post and try to think of ways this pattern could be applied.

Previous

--

--