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
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.
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.
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.
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:
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 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:
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.
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.