Now that we have a way of sending and receiving SMS messages, let’s put it to use with some natural language processing!
Tokenize and Stem
The first step towards any sort of natural language processing is normalizing the input, as to make it easier to understand, or at the very least, manipulate. In our case, we want to split the input into words, and then remove any alteration, as to bring them back to their most simple form, or stem. That means removing plural signs and verb conjugations, amongst other things. For this, I used the Apache OpenNLP library.
Now that our input is “tokenized”, it can be used to issue commands. To keep things simple, the chatbot will respond to commands with a predefined syntax. Possible commands will be registered with an expected word sequence, and will then be invoked when the corresponding sequence is received. When the program starts, a command tree is created, which can then be navigated, word by word, to find the associated method call.
To allow for more variation, all extraneous words are ignored. For instance, “Keep saying a very grateful hello forever.” will resolve to the same method.
The algorithm itself is quite simple, but can be expanded to make it a bit more powerful. For instance, passing parameters could be a nice addition. If those could be properly typed, that would be even better.
Using reserved keywords when registering a command, I can define parameters to be passed onto the invoked method. These keywords are user-defined, therefore completely customizable. At the moment, I have included most basic types, namely Integer and String, but in the future, one might see a command similar to “track package <UPS-tracking>”, or even “what is $ <int> in <currency>?”.
Here is what a command declaration looks like in Java.
Handling User Identities
Since all commands are tied to a phone number, we can use it to do some basic user authentication. In fact, it would be even nicer to have users belong into “groups”, or “roles” which then determine the commands they are allowed to access. When a user invokes a forbidden command, it can simply be ignored.
Users are saved to a MySQL database, using Hibernate ORM.
To make manipulating permissions easier, I added a CommandContext object which can be used in any command method to get contextual information, which includes the user information.
Repeating several times in a row “Say hello 5 times” can become somewhat tedious. A “say that again” command could be useful.
In fact, it would be great if our chatbot, like most modern assistants, had some contextual or conversational awareness. That way, we could define commands to be only accessible after a particular relevant command has been invoked. For instance, “Yes” and “No” commands could be enabled after a question, as part of the ongoing conversation.
This is achieved by using a “@Conversational” annotation on the new command, which defines the method call after which it should be enabled. Then, when a new message is received, the conversational commands are checked first for a match. This can be used as an effective way of “masking” default commands depending on the context.
As-is, the bot “works”, but does not have any useful functionality. You can say “hello” to introduce yourself, which will create a new user in the database. It can remember names too, which could theoretically be used to customize messages. There are, however, several issues which should be addressed before going further.
- The database access is NOT thread-safe. The bot works, as long as there is only one concurrent user.
- The actual command tree-searching methods are getting quite complicated. The class itself could definitely be more finely divided to improve readability.
- The goal is to, someday, listen on multiple communication endpoints. For instance, the bot could be used as a Skype or Messenger bot. This should definitely be feasible in its current implementation. We could probably go a step further, and use a proper SMS service, instead of one I hacked together using a spare iPhone 3GS.
- Right now, defining synonyms, or aliases for commands is quite clunky. It requires creating a brand new method, with its own @CommandHandler annotation. Allowing arrays or some other way to define aliases would be very useful. The same can be said of conversational commands.
- The bot needs a friendly name!
See on GitHub.