A Blog

A Blog

Where I write anything about everything every once in a while

19 Apr 2016

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.

                 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 is now very simple:

    (defn parse
      ((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
      (println "Creating customer named" name))

    (defn list-customers
      (println "List customers"))

    (defn refresh-customer
      (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
      (->> (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?
      (= (type parsed) instaparse.gll/failure-type))

    user=> (println (failed? (execute "list custmer")))

    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.