Keeping up with the conversation
A less obscure cultural reference
In the Part 1 of this tutorial we set up our page on Facebook and hooked it to a Sinatra server running on our machine through ngrok. Then we introduced the facebook-messenger gem by the guys at Hyperoslo. We also taught our bot to use Google Geocoding API in order to lookup GPS coordinates for any given address. This is what our bot will look like after we meddle with it some more:
Chatting with the bot onĀ mobile
Talk toĀ me
OK, letās get first things about Messenger bots straight. They adhere to Victorian etiquette for little kids:
āDo not speak unless spokenĀ toā
This is a good rule, because it helps to avoid unsolicited messages aka spam. But how do we provide solicited messages? In the first minimalistic version of our bot we just assumed that the user will give us something we can send directly to Google Geocoding API, wait for response and display the result. That assumes userās prior knowledge about our botās intent, which is a very bad way to go about UX. Also, we did not provide any fallbacks in case user types anything that doesnāt have GPS coordinates (surprisingly, āHelloā will yield a result, as Google will just assume weāre looking for Hella, Iceland)āāāin that case, our bot will completely lose its marbles. We need some kind of conversational loop.
Poorly Drawn Lines by Reza Farazmand is licensed under a CC BY-NCĀ 3.0
First thing I learned while trying to implement it with facebook-messenger gem: Bot.onĀ :message { |ā¦| block }
call can be made only once per scope, otherwise we face all sorts of unexpected behaviour (correct me in replies if Iām wrong). So we need to create more scopes! Fine, letās define separate methods to handle different ways our conversation can go, starting with the most obvious one. Inside bot.rb delete your Bot.on method that takes a block and replace it with this:
Now we need to add a process_coordinates
method referenced in our wait_for_user_input
. Put this into your bot.rb:
Now at the bottom of bot.rb call our waiting method. It will launch the conversation loop:
wait_for_user_input
From now on we will only engage user if he mentions ācoordinatesā (ācoordā as a shortcut) or āgpsā in any context. Then we will ask user to provide a destination for look-up, then we will call Google and display the result. If no result is found, we will notify user and wait for another command. At this stage, our code.rb looks like this. Hit Ctrl-C
in your tab with rackup
running and restart the server to test it in Messenger.
OK, it seems to work. Letās add features!
If we dig inside a JSON we get back from Google, weāll see that thereās a key called formatted_address
that will return a full postal address for any destination Google can find. Letās add this as a feature to our bot.
First, add another when
clause to our wait_for_user_input
method:
Note that that we thought of user misspelling āaddressā: if he/she types āI need a full adressā (a type of mistake Iād make) our regexp will still match and the show_full_address
function will be called. Put it inside bot.rb too:
Finally, add this helper method:
Great. This is your code at this point. Restart the server and test.
Time to refactor!
Our bot surely works and can handle two different commands, but its code could certainly use some refactoring: process_coordinates
and show_full_address
methods are 80% identical, that means we can do better.
Refactoring can sure getĀ messy
We know that Ruby methods can accept blocks. It seems we may benefit from this language feature to make our code DRY-er. Letās define another method that will abstract out most of API handling functionality:
Then we can change both process_coordinates
and show_full_address
like so:
Great. Now our methods only do stuff that is actually specific to their purpose, the rest is handled by our generic method that handles API-related logic.
I know, the concept of Ruby yield
with arguments can be tricky to wrap your head around, especially if you are a beginner coder like me, but I like to think of it as time-space warp that connects two methods, and you can pass variables back and forth.
This is how I imagine Ruby yield that takes an argument inside a methodĀ (Almost)
While we are at it, letās also create an IDIOMS
constant, so we can have all our botās phrases in one place. Add it on top of your bot.rb:
IDIOMS = {not_found: 'There were no resutls. Ask me again, please',ask_location: 'Enter destination'}
Now replace your hard coded replies with a reference to IDIOMS
like so:
message.reply(text: IDIOMS[:ask_location])
This is your code at this stage. Restart your server and test test test.
Not justĀ talk
Great, out bot still works and we used some clever Ruby, but it is still not the best user experience. How the user is supposed to know what to do? Can we guide him? The promise of conversational AI did not really fulfil itself yet, and for now we are stuck with mixed interfaces. Everyone is doing it. Facebook Messenger has currently three (that I know of) ways to show āmenusā to the user:
- A persistent menu that can always be called from the icon next to text entry field.
- A āButtonā template that letās you tie action buttons to a message bubble.
- A set of āquick repliesā.
Three Facebook-approved ways of presenting button menus toĀ user
We will pick quick replies as I find them more user-friendly. They are also treated as regular messages by the API, while other buttons implement āpostbackā call to your webhook, which is a bit different concept, as we will see later.
First, create constant to keep your quick replies in one place:
āPayloadā sounds confusing, but this is really just another identifier you can assign to user interaction. Think of it as a constant, so we use CAPITAL_LETTERS
for our payloads.
Okay, now we are going to write a little method that will help us a lot in the future to adopt more declarative approach in the way we code botās messages. Remember I told you bots only speak when they are spoken to? Well, thatās true up to a point. A user must do something that will expose his ID. Like, sending first message or using a link to your Facebookās page address (where your bot lives and handles its Messenger interaction).
So, in addition to Bot.onĀ :message { |ā¦| block }
that we saw earlier, facebook-messenger gem creators also gave us Bot.deliver
method that lets us send messages to user out of the blue once we have his Facebook ID. We are going to wrap it into our own say
helper:
It will take a userās id, a text for the message we want to send and optional argument for an array of quick replies. If an argument is supplied, message will be sent with quick replies attached to it, if notāāāit will be just a text.
From this great article in Chatbotās Life we know that most users strike a conversation with a bot with some form of āhiā or āhelloā. We want our bot to present whatās on the menu ASAP, without any unnecessary meet & greet, so we will cheat a little: weāll introduce the wait_for_any_input
method that will present our array of quick replies to whatever user sends us, be it āhiā, ābonjourā or āŠæŃŠøŠ²ŠµŃā. Add it to bot.rb:
message.sender[āidā]
is how we get userās ID after he opened the conversation.
Also, letās change the name of our old wait_for_user_input
method to wait_for_command
to avoid any ambiguity. Make sure to change the definition and all possible calls.
Also, make sure that at the very end of our bot.rb you now call wait_for_any_input
:
# launch the loopwait_for_any_input
Letās update our IDIOMS
:
IDIOMS = {not_found: 'There were no resutls. Ask me again, please',ask_location: 'Enter destination',unknown_command: 'Sorry, I did not recognize your command',menu_greeting: 'What do you want to look up?'}
Then letās define our show_replies_menu
method:
It uses our say
helper to send user the menu. Then it calls our wait_for_command
to handle whatever user does next. Note that we picked the wording of our quick replies to match all possible regexp in wait_for_command
. At the same time, user is not obliged to pick any of quick replies. He can shout at us something like āGIVE ME COORDINATES YOU STUPID BOT!ā and his command will still be handled, as our regexp is case-insensitive. Now letās add an else
clause to our switch statement to handle the case where none of the commands known to us have been matched. Here is our new and improved wait_for_command
:
Up until now, when the API request failed, user had to type something like ācoordinatesā or āfull addressā yet again to restart the whole process. That is not very cool, so letās update our handle_api_request
to retry the last command if API did not return any results. For that, we will use some metaprogramming (one of my favourite things about Ruby!). Look:
By theĀ thread
By now your bot should be testable. At last, we will adjust some thread settings, so our user is properly greeted and presented with āGet Startedā button that allows to strike the conversation. We will also add a persistent menu that will give user constant access two both features, even if heās in the middle of something else. Hereās how it will look like:
Making our botĀ nicer
This will require us to use facebook-messengerās Bot.onĀ :postback { |..| block }
functionality. First, go to your Facebook developer console, under āWebhooksā click āEdit eventsā and enable messaging_postbacks.
Add this to your bot.rb:
Now add some logic to handle postback events:
And thatās all for PartĀ 2!
We have a fully functional (though may be not particularly sexy in order to attract VC millions) bot that we wrote in under 200 lines of pure Ruby code. Follow the third and the last part of this tutorial to see how we add some final bells and whistles and deploy to Heroku, so our Smooth Coordinator can finally go live.
All the code for our bot at this stage of its development can be found on my Github. You can find the code for a complete project (still WIP on the time of this writing) in the same repo, but in the main branch. Feel free to fork it, clone it and play with it.
GO TO PARTĀ 3
About me: Iām a beginner web developer, ex senior international TV correspondent, and young father who is looking for a first proper programmer job. Aside from obsessing about coding style and trying to pump my head full of technical literature, I recently graduated top of the class from an excellent Le Wagon coding bootcamp in Paris and co-authored Halfway.ninja as a graduate project there. Itās an airfare comparator for couples. Go check it out! You can find me on Github, Twitter and Instagram. I also own this domain. I am currently available for hire, be it an inspiring internship, freelance or even full-time work. I speak English, Russian, French, Ruby, JavaScript, Python and Swift and I am currently based in Moscow, Russian Federation. Feel free to write me directly with any offers.