Build a Pokemon bot with SAP Conversational AI and NodeJS | by Paul Pinard | Towards Data Science

Pokémon is one of the first game I remember playing. It had everything to shine: an engaging story, a deep but easy to learn gameplay and an amazing character design.
However, with the growing numbers of Pokemon present in each new iteration, it’s very hard to keep track with the growing number of Pokemons you can encounter. In the latest games, Sun and Moon, there are more than 807 Pokémon to catch! That’s why I wanted to build a bot to help me keep track: I wanted a companion to help me remember what type is Gyarados or how to make my Stufful evolve.
Besides this copious program, the goal of this tutorial is to show you how to use Gazettes to improve your bot entity detection and how to interact with a Node.JS API that retrieves informations from a knowledge base to create a rich and interactive experience for your users.
This tutorial assumes that you already have a basic knowledge of the SAP Conversational AI’s platform. If you’re a beginner, I advise you to start by reading this tutorial instead and create your own chatbot for free.
To follow this tutorial you’ll need:
An account on SAP Conversational AI. It’s free!
An up-to-date version (6.0 or higher) of Node.JS installed on your computer.
A Unix-like command line prompt. If you’re using Windows and can’t install Bash for Windows you’ll need to adapt some steps to your toolchain. We would be happy to help you if you need any.
Note: If you would like to skip the bot creation on SAP Conversational AI and go straight to the server-side, you can fork it from here.
Create a new bot on the platform, and choose Greetings and Small Talk as predefined skills:
We’ll focus on training the bot on 2 intents:
Getting general information about a Pokémon
Learning about its evolutions.
Create a new intent called pokemon-informations, and add the following sentences to begin with:
You can add many more sentences, the more examples you add the stronger your bot will be.
You need to tag the Pokémon name with the entity POKEMON:
It would get very, very tedious if we had to teach the algorithm every single Pokémon name this way.Instead we will use a tool called Gazettes.
Gazettes are meant to improve entity detection when you can provide a list of synonyms for an entity. A Gazette holds a list of terms that are called, well, Synonyms, and they are used by the algorithm to make your bot smarter.
You can create a Gazette for an entity at the bottom of the Train tab:
There are two settings for a Gazette; it can be either open or closed:
If it is open (the default setting), it will simply improve the entity detection. To do so, it works hand in hand with the entities examples you tagged in your intent expressions. Just think of it as bonus training.
If it is closed, the synonyms in the Gazette represent every single value the entity can hold. Forget about context and all those fancy features NLP engines use to extract entities, if a word is held in a closed Gazette, it will be tagged, if it is not, it won’t.The only exception is that closed Gazettes used fuzzy matching to allow for small mistakes and typo, but you can adjust its strictness (100 means a perfect match is required, 1 means everything will be tagged with the entity).
The good news is that you can upload csv files containing synonyms for your gazette. I created one containing every single Pokémon names that exist: You can download it here.
Click on UPLOAD A .CSV FILE in your gazette page. It might take a while before everything gets loaded.
Now that the bot is taught every Pokémon name, it’s a perfect fit for a closed Gazette. Go to its options and toggle the button to close it.
But why would I need to train my bot once the Gazette is created?
Well, entity and intents, even if linked, are distinct concepts. You still need to train your bot to correctly detect intents even when entities are well trained.
Finish your bot training by adding the last intent, pokemon-evolutions.Add the following sentences :
You should notice that the POKEMON entity is tagged at the creation of your expression, with no further action!
A big part of the logic of this bot will happen in the NodeJS API, so its skills are very straightforward.
Skills:
are triggered if one of the intents you created are present.
require an instance from the POKEMON entity to be present.
send a request to the NodeJS API with the information collected.
Next to the 2 skills you already have in your Build tab, create a new skill, called pokemon-informations.
It is triggered when the intent pokemon-informations is present:
It checks that the entity POKEMON is set :
Set a message if #pokemon is missing, something like “Which Pokémon do you want to know more about?”
Sends a request to the API once the Pokémon is known, and clean up the memory afterwards:
Create another skill called pokemon-evolutions structured the same way, but that is triggered if the intent Pokemon-evolutions is present, and has a webhook URL of /pokemon-evolutions.
The core of the API simply is an Express.js application. It will have two main routes: one for general information and another for questions about a Pokémon evolutions.It will also handle POST requests on /errors, that will be used by the Bot Builder to notice you when something goes wrong.
Start by creating a workspace and install the dependencies:
mkdir ~/pokebot && cd ~/pokebot npm init # you can accept all the default settings npm install --save express body-parser npm touch index.js
Instead of using a remote database, I created a JSON file containing all the Pokemon knowledge your bot needs.Since NodeJS can require JSON files as regular Javascript files it will be very easy to use, and it’s small enough to be loaded into RAM without any concerns (it’s “only” around 500kb).
You can either download it using this link and move it to the folder you just created or you can also use this curl command:
curl -O https://cdn.cai.tools.sap/blog/pokebot/pokedex.json
This file is an array of objects structured as such :
[   {     "id": 1,     "height": "7",     "weight": "69",     "base_experience": "64",     "description": "A strange seed was planted on its back at birth. The plant sprouts and grows with this POKéMON.",     "types": [       "grass",       "poison"     ],     "name": "Bulbasaur",     "image": "https://cdn.cai.tools.sap/blog/pokebot/1.png",     "evolutions": [       {         "id": 1,         "name": "Bulbasaur"       },       {         "id": 2,         "from": 1,         "trigger": "leveling",         "trigger_lvl": 16,         "name": "Ivysaur"       },       {         "id": 3,         "from": 2,         "trigger": "leveling",         "trigger_lvl": 32,         "name": "Venusaur"       }     ]   },   ... ]
Not all the data included will be used in this tutorial so feel free to add features to your bot, or even improve this JSON if needed. You pull requests will be much welcomed!
Now open the index.js file with you favorite editor.We’ll start by loading the dependencies and start the express application.
const express = require('express'); const bodyParser = require('body-parser'); const db = require('./pokedex.json');  const app = express(); app.use(bodyParser.json());  // Load routes app.post('/pokemon-informations', getPokemonInformations); app.post('/pokemon-evolutions', getPokemonEvolutions); app.post('/errors', function (req, res) {   console.error(req.body);   res.sendStatus(200); });  // Start server const PORT = process.env.PORT || 5000; app.listen(PORT, () => console.log(`App is listening on port ${PORT}`));
Both routes will need to retrieve information about a Pokémon from the JSON file by its name. Let’s write a function to do that:
function findPokemonByName(name) {   const data = db.find(p => p.name.toLowerCase() === name.toLowerCase());   if (!data) {     return null;   }   return data; };
The controller for the route /pokemon-informations extracts the Pokémon name for the memory, as configured in the corresponding skill, and fetches its information from JSON.It then returns a list of messages to display the data in a friendly format.
There is a fallback in case the Pokémon entity extracted by SAP Conversational AI can not be found in the database.
function getPokemonInformations(req, res) {   const pokemon = req.body.conversation.memory.pokemon;   const pokemonInfos = findPokemonByName(pokemon.value);    if (!pokemonInfos) {     res.json({       replies: [         { type: 'text', content: `I don't know a Pokémon called ${pokemon} :(` },       ],     });   } else {     res.json({       replies: [         { type: 'text', content: `${pokemonInfos.name} infos` },{ type: 'text', content: `Type(s): ${pokemonInfos.types.join(' and ')}` },         { type: 'text', content: pokemonInfos.description },         { type: 'picture', content: pokemonInfos.image },       ],     });   } }
The controller for the route /pokemon-evolutions is similar, it just returns different data.Since the formatting of the message displaying the evolution requirements is quite dense, it is better to move it to its own function, formatEvolutionString.
In addition to the fallback if the Pokémon can’t be found, this controller also needs a fallback if the Pokémon doesn’t have any evolution.
function getPokemonEvolutions(req, res) {   const pokemon = req.body.conversation.memory.pokemon;   const pokemonInfos = findPokemonByName(pokemon.value);    if (!pokemonInfos) {     res.json({       replies: [         { type: 'text', content: `I don't know a Pokémon called ${pokemon} :(` },       ],     });   } else if (pokemonInfos.evolutions.length === 1) {     res.json({       replies: [{ type: 'text', content: `${pokemonInfos.name} has no evolutions.` }],     });   } else {     res.json({       replies: [{ type: 'text', content: `${pokemonInfos.name} family` },{           type: 'text',           content: pokemonInfos.evolutions.map(formatEvolutionString).join('\n'),         },         {           type: 'card',           content: {             title: 'See more about them',             buttons: pokemonInfos.evolutions               .filter(p => p.id !== pokemonInfos.id) // Remove initial pokemon from list               .map(p => ({                 type: 'postback',                 title: p.name,                 value: `Tell me more about ${p.name}`,               })),           },         },       ],});   } }
You can now launch the API:
npm start # You can type `PORT=XXX npm start` if you don't want to use the default port (5000)
The program should output this message on startup:
App is listening on port 5000
If that is not the case for you, try to read along the code again to check for any errors, you’re also welcome to come by our Slack if you get stuck!
Since SAP Conversational AI needs to be able to publicly access your API, you either need to host it on a public server or to use Ngrok.Ngrok allows you to expose a port of your computer to the outside world.Download Ngrok and launch it using this command:
ngrok http 5000
You can now paste the https redirection URL into your Bot base url, in your settings on SAP Conversational AI platform:
Your bot is now live! You can connect to various messaging channels by following the steps in the Connect tab.
You’ve got a Pokemon bot up and running. That’s nice already, but with what you’ve learned in this tutorial you can do much more. Here are some queries your bot could process if you want to go further:
Questions about the moves and when each Pokémon learn them
Where to find each Pokémon in the games
Types-matching: what is the modifier of a given type versus another? Mega-bonus if you can handle dual types!
Adapt the replies depending on the Pokemon version (Good luck on this one!)
Building an exhaustive, all-in-one, all-knowing Pokemon bot seems like a life’s mission. Only a true Pokéfan could pull it off. Would you be the one? And remember you’re very welcome to contact us if you need help, trough the comment section below or via Slack.
Originally published on SAP Conversational AI blog.