How To Build a Recipe Slackbot Using Watson Conversation and Spoonacular API
The code for this tutorial is here.
Note: If you’re interested in adding a database to this chatbot, here’s a good follow-up tutorial on integrating this chatbot with Cloudant.
Watson released its own Conversation Service last month. Naturally, I decided to take it out for a spin.
After looking at some existing bots and APIs, the last thing I wanted to build was another pizza-ordering bot. I do, however, love food way too much. In the end, I decided that asking a Slackbot for recipe suggestions would be a fun interaction.
PSA: This bot has absolutely nothing to do with Chef Watson. It’s just a simple Slackbot you can add to your Slack channel.
What finally sold me on this idea was Spoonacular’s find-recipe-by-ingredients feature. By the way, Spoonacular has a very easy-to-use API, along with thorough documentation, which the integration process much easier.
Sometimes, you just have no idea what to do with a piece of leftover chicken and some radish. Don’t fret, your very own sous chef is here to help!
Say ‘hi’ to souschef
Before diving into the tutorial, let me show you what this bot, souschef, can do. Souschef addresses two scenarios.
The first is suggesting recipes based on a list of ingredients.
The second is suggesting a recipe based on a type of cuisine.
At the end of this tutorial, you should be able to deploy this in your own slack channel.
Again, the code is here.
Before Starting, You Need…
- An account with Bluemix to retrieve Watson Conversation credentials. You can read the official documentation or my previous post on how to get started with a Watson service.
- A Spoonacular API key. There is a free tier.
- A Slack account and a channel to talk to your Slackbot.
- Python 2 or 3
- pip and virtualenv
Step 1: Set Up Slackbot in Python
Matt Makai has written a good how-to guide on how to get a Slackbot running using Python. He also takes you through setting up pip and virtuaenv. I’d recommend you following that to get started. Make sure you have your Slackbot token and Slackbot ID ready.
pip install -r requirements.txt
You then need to create an .env file. Here’s what mine looks like:
Get the relevant credentials and put them here. You can then use python-dotenv to read them into your program.
Now onto actual Slack integration. At the end of this step, you should be able to do 2 things (code stolen from Matt’s guide with slight modifications):
1. Continuously monitor and parse inputs from your Slack channel. The following code appears in souschef.py.
The run function constantly monitors Slack activities and registers events that are messages directed at souschef. The parse_slack_output function then parses the event and returns the text of the message.
2. Post messages to your Slack channel
Step 2: Design Your Conversation
At this point, you should have a vague idea on how your conversation should play out. Here’s what I drew up before I started coding. I know, very elegant.
My actual dialog flow turned out to be quite different from this initial diagram. This is natural as you refine the user experience and discover more technical constraints. But the two main branches, suggestion based on ingredients and suggestion based on cuisine type, had already been decided.
Step 3: Set Up Recipe Client in Python
It’s a good idea to abstract away the functionalities related to searching and retrieving recipes. I wrote a class called RecipeClient. Below are its main functions.
The four functions that are quite self-explanatory.
- find_by_ingredients retrieves 5 recipes based on the list of ingredients entered by the user.
- find_by_cuisine retrieves 5 recipes from a type of cuisine.
- get_info_by_id retrieves detailed information on a specific recipe after the user has entered a selection. This includes information such as the number of servings or the total cook time.
- get_steps_by_id retrieves the actual steps involved in making a specific recipe.
You’ll find Spoonacular’s full documentation here. Now moving on to Watson Conversation!
Step 4: Conversation Tool
After launching and login into the Conversation Tool, you should see something like this.
As you can see, I’ve already created a Recipe Bot workspace. If this is your first time, this page will be empty. Just go ahead and create a new workspace and remember your workspace ID. You can find the ID by clicking the button on the top right and hit View details.
Once inside the tooling, you’ll see three tabs:
Dialog is where you wire up your dialog flow. But before jumping into dialog, you need to understand Intents and Entities, the two key components in building a conversation using Watson. Let’s get to know them by building your Intents and Entities into your workspace.
Note: Step 5–8 explain how to set up everything in the Conversation Tool. I recommend following along to grasp the important concepts, especially if you plan to build your own version of the chat bot. But if you already grok the concepts, you can import the JSON file that encapsulates the entire workspace here.
Step 5: Intents
Here is the official documentation on intents. The purpose of intents is to determine the purpose of an arbitrary user input. You build a single intent by giving Watson examples of inputs that all have the same purpose. Under the hood, Watson builds a text classifier based on the the examples you’ve given it. For souschef, you only need three intents:
I’ve expanded the #start_cooking intent, which once identified, prompts souschef to ask you if you have specific ingredients you’d like to use. The #yes and #no intents are collapsed, but I’m using them to capture all the different ways a user could say “yes” or “no”.
If you’re familiar with our past services, Intents are basically an integrated version of the Watson Natural Language Classifier.
Step 6: Entities
Here is the official documentation on entities. Entities are for keywords identification. You give Watson a list of keywords that all categorize into the same entity, and Watson will inform you of such an entity next time it encounters any of those keywords. In the case of souschef, I only needed one entity, cuisine:
With this entity in place, anytime a user mentions one or more words in that list, Watson will inform my app through the returned JSON. Here is a screenshot of my terminal showing the returned JSON after the user has entered the sentence “i want to cook a french dinner, but using veal”:
Step 7: Dialog
Now we can wire up our dialog. I’ve taken some screenshots to show you what the whole thing looks like. Again, I recommend wiring this up yourself but you can also also import workspace.json into your own workspace.
You can do the import by going to the home page of the tool and click the “up” arrow shown in the picture below.
This is the dialog flow to retrieve a recipe from a list of ingredients.
This is the dialog flow to retrieve a recipe from a type of cuisine.
One thing you’ll notice is that some nodes are black boxes containing JSON. This is the Advanced view of a node. You can toggle any node between Simple and Advanced view by click the button on the top right.
This is a really important feature to understand because Advanced view let’s you directly edit the JSON returned to your app.
To the left, you’ll see that I’ve added a context object with the variable get_recipe to the JSON.
This is how you communicate contextual information from Watson to your app. The reverse is also true (see next step).
This is a convenient segue to…
Step 8: Using the Context
Here is the official documentation on context variable.
The context variable is contains all contextual information shared between Watson and your app. Let’s expand on the example above.
If I’m at the red-arrow node, it means the user had said “yes” to the question “Are you looking to use specific ingredients?” How do I tell my Python app that it’s ok to start looking for recipes? I set get_recipes = true in the context variable.
Note: get_recipes is actually used for controlling if my app should hit the spoonacular API. It’s not until the next node, when I set is_ingredients = true, that my app actually hits the spoonacular API. More explanations coming.
In my app, I get the returned response from Watson and store it in the variable self.context. I now have access to the get_recipe variable, as shown in line 67.
Note: The context variable isn’t simply an object to pass arbitrary variables back and forth. It actually contains important system information in the system object, which holds the current node of the conversation. See screen shot below.
For example, if you force the context variable to be empty, the conversation restarts because Watson doesn’t know which node it’s at anymore.
Step 9: Integrate With Spoonacular (or any 3d party) API
Let’s continue with the same section of dialog from the previous step. I’ve included the same screenshot for your convenience.
The red-arrow node asks the user for a list of ingredients. Once the user enters the list, we’ve arrived at the blue-arrow node. Notice that I “cheated” and set the condition to be simply true, which means this node executes no matter what. To simplify this tutorial, I’m blindly trusting the user to actually enter a list of ingredients. This is a simple, but fragile, way to capture user input.
Note: One way to make this more robust is creating an entity called ingredients that contains all possible ingredients. So instead of trusting whatever the user enters, you can check the context variable to see if the ingredients entity is actually detected. Another way is to do the check in your Python app with some sort of a database lookup. In a serious chat bot deployed in production, you should always assume that the user will not follow your instructions.
So back to to the blue-arrow node. Notice that I set is_ingredients = true. This informs my app that the user input is a list of ingredients, so it’s time to take that input and find some recipes based on those ingredients. See line 109.
In line 109, I check for the is_ingredients variable and construct a response based on the results returned by the handle_ingredients_message function.
At this point, I’ve presented a list of possible recipes to choose from, and waiting for the user to enter a number between 1 and 5. Once the user inputs something, we’ve arrived at the yellow-arrow node. Again, I’m using the simple but fragile hack to get the user input. But this time, I’ve also added some input validation in my app. This is to illustrate how you can do app-side input validation and redirect the conversation accordingly.
So what happened here? As you can see from line 113, this section of code executes when I see is_selection == True. This tells my app that the user has entered a number (supposedly) corresponding to the list of recipe titles.
But before I go and look for a specific recipe, I want to make sure the input a number between 1 and 5 inclusive. First, I assume that the input is bad (line 116–118) by adding a variable selection_valid: False to context. I then check if the selection is valid by making sure it’s a integer between 1 and 5 inclusive. If that’s the case, replace the invalid input response with the handle_selection_message response.
This is the opposite of what we did in the previous two nodes. I’m creating a variable in my app and passing it to Watson instead of the other way around. In the Dialog flow, I direct the conversation based on the selection_valid variable:
Here’s the collapsed view showing the boolean checks (right side).
And here is the expanded view showing how different selection_valid values lead to different points in the dialog flow. Read the official documentation to understand Continue from statements.
If selection_valid is True, the app returns the selected recipe steps and goes back to the beginning of the dialog. But if the selection_valid is False, the conversation goes to the node that presents the recipe options and prompts the user to make a recipe selection again.
You should now do the same for the other conversation path that suggests recipes based on a specific type of cuisine. Refer to my screenshots above or import workspace.json.
That’s A Wrap!
You now have a working chat bot. More importantly, you’re more equipped to build a chat bot using Watson Conversation.
Of course souschef can be improved in many ways. Here are some obvious next steps.
- Deployed to a server so that souschef is always available.
- State persistence using a database. Keeping context in memory is never a good idea, especially if it has many users in production. Again, here’s the tutorial on how to integrate this Slackbot with Cloudant.
- More robustness against arbitrary user inputs. There are currently many ways to crash souschef, but of course you’ll have to figure that out for yourself!
Having built my first chat bot, I realize that chat bots are easy to build, but very difficult to perfect. It’s a simple process to create a successful interaction assuming all the stars align. Unfortunately, though the free form of natural language means a much lower cognitive load on the user, it also means an infinite number of possible inputs. Being able to deal with that while guiding the users to a successful outcome is what makes a chat bot seem magical. I’ll have more thoughts on what makes a successful chat bot in my next post.
Good luck on your journey to create the first WhatApp of chat bots!
I hope you enjoyed this tutorial. If you have any questions feel free to reach out at firstname.lastname@example.org or connect with me on LinkedIn.