Instaparse Powered Slackbot
Bots are all the rage now. While building a conversational AI bot is a huge undertaking, building your own helpful Slackbot isn’t. These bots are great for performing simple tasks that don’t warrant a dedicated interface.
This is exactly what I needed at my job. Tasks like creating a customer, listing customers, and refreshing customer data were all repetitive tasks that were great candidates for automating into a bot.
I began by writing my own parsing code and this worked fine until things began to get more complicated. Having seen Instaparse before, this seemed like the time to dive in and try it.
The Commands
Here are the commands that we want.
- create customer “Acme, Inc.”
- list customers
- refresh customer 11
Defining the syntax
My programming languages class was many years ago. It took some REPL play and outright borrowing of examples around the web to build the BNF notation. Carin Meier has a good introductory post.
Here is the syntax we end up with for our custom language. Note that strings in our syntax are surrounded by double quotes.
(def syntax "EXPR = CREATE_CUST | LIST_CUST | REFRESH_CUST | HELP
HELP = <'help'>
CREATE_CUST = <'create customer'> SPACE+ STRING
LIST_CUST = <'list customers'>
REFRESH_CUST = <'refresh customer'> SPACE+ NUMBER
NUMBER = #'\\p{Digit}+'
STRING = <'\\\"'> #'([^\"\\\\]|\\\\.)*' <'\\\"'>
<SPACE> = <#'[ \t\n,]+'>")
Parsing
Parsing is now very simple:
(defn parse
[input]
((instaparse.core/parser syntax) input))
user=> (parse "list customers")
[:EXPR [:LIST_CUST]]
user=> (parse "create customer \"Acme, Inc.\"")
[:EXPR [:CREATE_CUST [:STRING "Acme, Inc."]]]
user=> (parse "refresh customer 11")
[:EXPR [:REFRESH_CUST [:NUMBER "11"]]]
This is excellent, we can traverse this tree to execute our custom tasks. What would be really neat is if we could connect this directly to functions that carry out our tasks. The functions might look like this:
(defn create-customer
[name]
(println "Creating customer named" name))
(defn list-customers
[]
(println "List customers"))
(defn refresh-customer
[id]
(println "Refreshing data for customer ID" id))
It turns out this is really easy to do with Instaparse.
Instaparse Transforms
Instaparse includes the ability to apply transformations to our AST. We do this by supplying functions to be called at each node in the AST. The transforms are a map from the node types to functions. Through some clever mappings for our non-task nodes, we can automagically route our syntax to tasks.
(def syntax-routing
{:NUMBER read-string
:STRING str
:EXPR identity
:LIST_CUST list-customers
:CREATE_CUST create-customer
:REFRESH_CUST refresh-customer})
(defn execute
[input]
(->> (parse input)
(instaparse.core/transform syntax-routing)))
user=> (execute "list customers")
List customers
user=> (execute "create customer \"Acme, Inc.\"")
Creating customer named Acme, Inc.
user=> (execute "refresh customer 11")
Refreshing data for customer ID 11
Syntax Errors
You might be wondering what happens when the input isn’t parsed. Instaparse won’t throw an exception, it will return a failure instance. Even better, when you print the failure it will provide a user acceptable message.
(defn failed?
[parsed]
(= (type parsed) instaparse.gll/failure-type))
user=> (println (failed? (execute "list custmer")))
true
user=> (println (execute "list custmer"))
Parse error at line 1, column 1:
list custmer
^
Expected one of:
help (followed by end-of-string)
list customers (followed by end-of-string)
refresh customer
create customer
Instaparse Rocks
This turns out to be remarkably little code to power our bot interactions. This just scratches the surface of the awesome Instaparse library, but it hopefully serves as a great introduction.