Creating a “Pokédex” Chatbot with React Native and Dialogflow

As a developer who grew up playing Pokémon, the projects I do for fun usually have to do with Pokémon. The dataset is just too fun to work with, and you feel like you’re a child again while you’re building the thing (whatever it might be).

In this tutorial, we’ll create a chatbot version of the Pokédex. We’ll use React Native to build the app and Dialogflow to build the brains of the chatbot.

Prerequisites

Basic knowledge of React Native and Node.js is required to follow this tutorial.

The following package versions will be used:

  • Node 11.2.0
  • Nodemon 1.19.1
  • Yarn 1.13.0
  • React Native CLI 2.0.1
  • React Native 0.59.9

Be sure to use the versions above in case the app doesn’t compile.

We’ll be using Dialogflow to implement the chatbot. This requires you to have a Google account. If you have one, you can go ahead and sign up for the service. Their free edition is very generous.

We’ll also be using ngrok to expose the server that we’ll be creating. Go ahead and create an account if you don’t have one already.

Lastly, you must know the basics of the original Pokémon game. More importantly, the type effectiveness.

App Overview

We’ll build a Pokédex chatbot and it will look like this:

Node.js will be used to build the server, Dialogflow for implementing the bot, and PokéAPI will be the data source.

Creating a bot that can answer any question about Pokémon that you might have is time-consuming, so we’re only going to focus on a few details:

  • Basic data — get the photo, description, moves, and abilities of the Pokémon.
  • Evolutions — get the evolution chain of a Pokémon.
  • Type effectiveness — get information on how weak or strong a specific Pokémon type is against another. Due to time constraints, we’ll only focus on single-type effectiveness.

Overall, the bot will be able to respond to the following:

  • image of Pikachu
  • tell me about Zapdos
  • what are the moves of Vulpix
  • what are the abilities of Bayleef
  • what came before Charizard?
  • what does Meowth evolve to?
  • what’s the evolution chain of Bulbasaur?
  • what is super effective against dragon type?
  • what is weak against dark type?
  • what has no effect against ground type?

You can find the source code of the app on this GitHub repo.

Setting up Dialogflow

Before we proceed, let’s first get to know what Dialogflow is. Dialogflow is a service that allows developers to build chatbots. It allows you to define your business entities and the questions that your users will usually ask.

It’s powered by natural language processing, so it’s able to extract the meaning from the text and return it in a format that you can easily parse in your app’s backend. Overall, Dialogflow provides you with all the tools needed to build powerful conversational interfaces for your app or website.

Create Agent

This section assumes that you already have a Dialogflow account and that you are already on the Dialogflow dashboard.

The first step is to create an Agent. The Agent is basically the chatbot itself. You can create one by going to the Agents page and clicking on the Create Agent button. Set PokeAgent as the agent name and leave the other fields as they are:

Once the agent is created, we need to get the configuration file that we’ll use to connect to this agent in the React Native app. Click on the gear icon next to the agent name. That will redirect you to the Agent’s settings page. By default, you’ll be on the General tab. Scroll down until you find the Google Project section then click on the Service Account link:

That will lead you to the Google Cloud Platform console. Log in with the same Google account you used for signing up for Dialogflow, and it will redirect you to the following page. From here, click on the three dots next to the middle service account and choose Create key. Choose the JSON option in the modal window that pops up. The contents of this file is the config you will need to connect to the Dialogflow agent later on in the React Native app:

Entities

Entities are the objects or concepts in the specific chatbot that you’re building. Since we’re building a Pokédex chatbot, all of our entities will have something to do with Pokémon. These entities allow the agent to extract the meaning behind a specific word in the user’s query. You can also define synonyms for a specific entity so that when your user uses that instead of the name of the entity, the agent knows that it’s basically the same thing.

Specs

First, we need to define the entities, which refer to the basic details that a user can ask about a Pokémon. To create an entity, click on the plus sign next to the Entities menu:

For your convenience, I’ve created a CSV file that you can download and import for each of the entities that we’re going to create. First, click on the three vertical buttons next to the save button and select Switch to raw mode:

Then on the CSV tab, paste the contents of the CSV file:

Pokémon

We also need to add the list of Pokémon as an entity. This is how the agent will know that the user is referring to a Pokémon when it’s extracting the relevant information from text. This is especially true if the user wants to get the basic information about a specific Pokémon. They will say something like: “Show me the photo of Pikachu”. Since we’ve added “Pikachu” as a Pokémon entity, the agent knows that Pikachu is a Pokémon:

You can get the contents of the CSV file here and import it like you did for the specs entity.

Pokemon types

This entity is used for defining all the Pokémon types. We’ll use it to determine the type effectiveness between different types of Pokémon:

You can download the CSV here.

Type effectiveness

Just like the Pokémon types entity, we’ll also use this to determine the type effectiveness. This entity describes the damage done to or by a Pokémon. This follows the same format as the API.

You can download the CSV here.

Evolutions

This entity is used to extract the evolution relation that the user wants to view. For example, if the user asks: “What comes before Golduck?” it means that they’re asking for the previous Pokémon from the evolution chain of Golduck.

You can download the CSV here.

Intents

Intents refer to actions that the user wants to perform. For a Pokédex chatbot, the user mainly wants to obtain information about a specific Pokémon. But depending on what’s being asked, we may also need to create separate intents. This allows us to group related ideas together so it’s easier for the agent to extract the meaning behind the user’s query.

For this tutorial, we’ll only create three intents. Notice that these directly map to the user queries that were mentioned earlier in the App Overview section:

  • basic_data
  • evolution
  • get_type_effectiveness

When you create a new agent in Dialogflow, two intents are also created for you by default:

  • Default Welcome Intent — the intent that contains the message for welcoming the users. You can actually delete this one because we’ll be hardcoding the welcome message in the app itself.
  • Default Fallback Intent — this intent gets triggered when the agent can’t determine what the user means. If you open it, you will actually see responses like: “I didn’t get that. Can you say it again?” or “Sorry, I didn’t get that. Can you rephrase?”.

Basic data Intent

Now that you know what intents are, it’s time to start adding them. Click on the plus icon next to the Intents menu to create a new intent. First, we’ll create the basic_data intent. As the name suggests, this is the intent that will be triggered if the user wants to get obtain basic Pokémon data. This includes things like the photo, description, moves, and abilities of a specific Pokémon.

The heart of every intent is its training phrases. This is where you can define the phrases that your users would most likely use if they want to get the data that the intent is serving. In this case, it’s the basic Pokémon data. Here are a few examples:

  • what are the abilities of psyduck?
  • tell me about volcarona.
  • what are the moves of hitmonchan?

Once you’ve added the training phrases, the next step is to highlight the words (“parameters”) that directly map to the entities we’ve defined earlier. In this case, we only need two: specs and pokémon. Go ahead and highlight the words for each phrase as you see fit. Feel free to add other phrases as well:

Here’s the list of training phrases I’ve added to the basic_data intent. You can also use them if you want.

Next, scroll down to the Actions and parameters tab. Verify that pokémon and specs are set. Mark all the parameters as required. This makes the agent automatically ask for these specific parameters if it has detected this specific intent but they weren’t supplied.

Dialogflow automatically keeps the original context, so the request parameters you get back on your fulfillment server (to be discussed shortly) will be the same:

Lastly, enable the webhook call for the intent. This tells the agent to make a request to the fulfillment server when this specific intent is triggered. The rest of the intents we’ll be creating needs to have this setting enabled as well:

Evolution Intent

The evolution intent gets triggered if the user wants to access evolution information about a specific Pokémon. It allows the user to ask questions like:

  • what’s next after vulpix?
  • what is the evolution chain of magnemite?
  • machamp evolves from?

You can get the sample training phrases here.

Once you’ve added the training phrases, highlight the keywords and select the corresponding entity. In this case, we only need two: evolutions and pokemon:

Don’t forget to mark all the parameters, as required:

Get type effectiveness Intent

The final intent we need to add is the get_type_effectiveness intent. This gets triggered when the user asks questions about how effective a specific Pokemon type is against another:

  • what is weak against ground type?
  • what is fire type very effective against?
  • what is ground type resistant to?
  • what is resistant to normal type?

This intent is a bit different from the other two because we need to add an output context to it. In this case, we’re adding type_effectiveness as the output context. This tells the agent to return the original word or phrase which got translated to an entity value.

For example, if the user asks: “what is resistant to normal type?”. In this case, “resistant to” is translated to no_damage_from because we’ve added “resistant to” as its synonym in the type_effectiveness entity earlier. Adding type_effectiveness as an output context allows us to get the original phrase “resistant to”. We’ll go through the specifics of why we need it later when we get to the fulfillment server code.

Next, we add the training phrases. You can get the sample training phrases here:

Lastly, make all the parameters required:

Setting up the app

Now that we’re done with the setup, it’s time to bootstrap the app. I’ve prepared a starter branch in the GitHub repo, which you can switch to to get the starter code:

git clone https://github.com/anchetaWern/RNPokedexBot.git
cd RNPokedexBot
git checkout starter
yarn
react-native eject
react-native link react-native-dialogflow
react-native link react-native-voice
react-native run-android
react-native run-ios

Once that’s done, replace the contents of the config.js file with the config file that you downloaded earlier from the Google Cloud Platform website:

export const dialogflowConfig = {
  "type": "service_account",
  "project_id": "XXX-XXXX-XXXX",
  "private_key_id": "XXXXX",
  "private_key": "-----BEGIN PRIVATE KEY----XXXXXn-----END PRIVATE KEY-----n",
  "client_email": "[email protected]",
  "client_id": "XXXXXXXXXX",
  "auth_uri": "https://accounts.google.com/o/oauth2/auth",
  "token_uri": "https://oauth2.googleapis.com/token",
  "auth_provider_xxxx_cert_url": "https://www.googleapis.com/oauth2/v1/certs",
  "client_x509_cert_url": "https://www.googleapis.com/robot/v1/metadata/XXXX/dialogflow-XXXXXXX.iam.gserviceaccount.com"
};

Building the app

After all the set up, we finally get to the meat of this tutorial: the coding! We’ll first work on the React Native app before we move on to the fulfillment server.

Start by opening the App.js file and importing the modules that we need. For this app, we only needDialogFlow_V2 for communicating with Dialogflow’s servers and GiftedChat for building the chat UI:

import React, { Component } from 'react';
import { View, Text, StyleSheet } from 'react-native';
import { GiftedChat } from 'react-native-gifted-chat';
import { Dialogflow_V2 } from 'react-native-dialogflow';

import { dialogflowConfig } from './config';

Next, initialize the bot user. This is just a simple user object and an initial message that will be displayed in the UI. This is what we’re using instead of the default welcome intent from Dialogflow:

const BOT_USER = {
  _id: 2,
  name: 'Poke Bot',
  avatar: 'https://ui-avatars.com/api/?background=0D8ABC&color=fff&name=Poke Bot'
};

class App extends Component {
  state = {
    messages: [
      {
        _id: 1,
        text: `Hi! I am the PokéBot 🤖.nnWhat would you like to know about Pokemon today?`,
        createdAt: new Date(),
        user: BOT_USER
      }
    ]
  };

  
}

On your componentDidMount(), set the Dialogflow config. This allows us to connect to the Dialogflow service using the config from the config.js file:

componentDidMount() {
  Dialogflow_V2.setConfiguration(
    dialogflowConfig.client_email,
    dialogflowConfig.private_key,
    Dialogflow_V2.LANG_ENGLISH_US,
    dialogflowConfig.project_id
  );
}

Next, render the UI. It’s very minimal because GiftedChat is already doing all the work for us. All we have to do is supply the messages stored in the state, the function for sending the message, and the data for the current user. This allows GiftedChat to distinguish between the user and the bot user:

render() {
  return (
    <View style={styles.container}>
      <GiftedChat
        messages={this.state.messages}
        onSend={messages => this.onSend(messages)}
        user={{
          _id: 1
        }}
      />
    </View>
  );
}

Next, add the onSend() function. This appends the message sent by the user to the messages array in the state. After that, we use the Dialogflow_V2 module to make a request to Dialogflow. This accepts three arguments: the user’s message, success callback, and error callback function:

onSend(messages = []) {
  this.setState(previousState => ({
    messages: GiftedChat.append(previousState.messages, messages)
  }));

  let message = messages[0].text;
  Dialogflow_V2.requestQuery(
    message,
    result => this.handleResponse(result),
    error => console.log(error)
  );
}

If Dialogflow was able to accept the request and returned a response, the handleResponse() function gets executed. This is where we extract the fulfillmentMessage, which we show to the user. If the user asks for the Pokémon image, we supply it from the server via the webhookPayload. This contains the image URL of the Pokémon being requested. Later on, you’ll see how we’re supplying it from the server:

handleResponse(result) {
  console.log(result);
  let text = result.queryResult.fulfillmentMessages[0].text.text[0];
  let payload = result.queryResult.webhookPayload;
  this.showResponse(text, payload);
}

Here’s a sample response that we get from Dialogflow:

{  
   "responseId":"xxxxx-xxx-xx-xxx-xxxxx-xxxxxxx",
   "queryResult":{  
      "queryText":"Image of pikachu",
      "parameters":{  
         "specs":"photo",
         "pokemon":"Pikachu"
      },
      "allRequiredParamsPresent":true,
      "fulfillmentText":"pikachu",
      "fulfillmentMessages":[  
         {  
            "text":{  
               "text":[  
                  "pikachu"
               ]
            }
         }
      ],
      "webhookPayload":{  
         "is_image":true,
         "url":"https://www.pkparaiso.com/imagenes/xy/sprites/global_link/025.png"
      },
      "outputContexts":[  
         {  
            "name":"projects/newagent-xxxx/agent/sessions/xxx-xxxx-xxx-xxx-xxxxxxxx/contexts/type_effectiveness",
            "lifespanCount":4,
            "parameters":{  
               "pokemon.original":"pikachu",
               "pokemon_types.original":"ice",
               "specs":"photo",
               "pokemon_types":"ice",
               "type_effectiveness.original":"super effective against",
               "pokemon":"Pikachu",
               "type_effectiveness":"double_damage_from",
               "specs.original":"Image"
            }
         }
      ],
      "intent":{  
         "name":"projects/newagent-xxxx/agent/intents/xxxxxx-739a-xxx-xxx-xxxxxxxx",
         "displayName":"basic_data"
      },
      "intentDetectionConfidence":1,
      "diagnosticInfo":{  
         "webhook_latency_ms":818
      },
      "languageCode":"en"
   },
   "webhookStatus":{  
      "message":"Webhook execution successful"
   }
}

The showResponse() function simply appends Dialogflow’s response to the messages array. If a payload is supplied as an argument, we just assume that it’s an image so we add the image property to the message and supply the payload.url to it:

showResponse(text, payload) {
  let msg = {
    _id: this.state.messages.length + 1,
    text,
    createdAt: new Date(),
    user: BOT_USER
  };

  if (payload && payload.is_image) {
    msg.text = text;
    msg.image = payload.url;
  }

  this.setState(previousState => ({
    messages: GiftedChat.append(previousState.messages, [msg])
  }));
}

Fulfillment Server

The fulfillment server is responsible for fetching the required data from the PokéAPI based on the user’s intent, formatting it, and returning the user-friendly response. Dialogflow makes a request to the fulfillment server when an intent that’s enabled the fulfillment server gets triggered.

First, create a server.js file and import the modules we need:

  • express — for quickly spinning up a server.
  • body-parser — for parsing the request body as a JSON object.
  • cors — to enable cross-origin request sharing.
  • axios— for making requests to the PokéAPI.

Below, we’re also creating the axios instance that we’ll use for making the request to the API. We’re setting a timeout of 4000 so that it automatically fails if the API fails to respond within that span of time. Dialogflow expects the fulfillment server to respond within five seconds, so we’re only giving one second as extra time for the latency:

const express = require("express");
const bodyParser = require("body-parser");
const cors = require("cors");
const axios = require("axios");

require("dotenv").config();
const app = express();

app.use(bodyParser.urlencoded({ extended: true }));
app.use(bodyParser.json());
app.use(cors());

const axios_instance = axios.create({
  baseURL: 'https://pokeapi.co/api/v2',
  timeout: 3000,
});

Next, add the route for handling the requests from Dialogflow. At the top are the arrays that contain the actions grouped together by the PokéAPI endpoint that they’re hitting. pokemon_endpoint hits the /pokemon endpoint in PokéAPI while pokemon_species_endpoint hits the /pokemon-species endpoint:

const pokemon_endpoint = ['abilities', 'moves', 'photo'];
const pokemon_species_endpoint = ['description', 'evolution'];

app.post("/pokedex", async (req, res) => {
  
  try {
    const { intent, parameters, outputContexts, queryText } = req.body.queryResult;
    
    const pokemon = (parameters.pokemon) ? parameters.pokemon.toLowerCase().replace('.', '-').replace(' ', '').replace("'", "") : ''; 
    const specs = parameters.specs;
    const get_type_effectiveness = (parameters.type_effectiveness) ? true : false;

    let response_obj = {};

    if (pokemon_endpoint.indexOf(specs) !== -1) {
      const { data } = await axios_instance.get(`/pokemon/${pokemon}`);

      let fulfillmentText;
      const id = String(data.id).padStart(3, '0');
      const value = (specs == 'abilities') ? data.abilities.map(item => item.ability.name).join(', ') : data.moves.map(item => item.move.name).join(', ');

      fulfillmentText = `The ${specs} of ${pokemon} are: ${value}`;

      Object.assign(response_obj, { fulfillmentText });

      if (specs == 'photo') {
        Object.assign(response_obj, {
          fulfillmentText: pokemon,
          payload: {
            is_image: true,
            url: `https://www.pkparaiso.com/imagenes/xy/sprites/global_link/${id}.png`
          }
        });
      }
    }

    if (pokemon_species_endpoint.indexOf(specs) !== -1 || intent.displayName == 'evolution') {

      const { data } = await axios_instance.get(`/pokemon-species/${pokemon}`);

      const evolution_chain_id = data.evolution_chain.url.split('/')[6];
      const text = data.flavor_text_entries.find(item => {
        return item.language.name == 'en';
      });

      let fulfillmentText;
      if (specs == 'description') {
        fulfillmentText = `${pokemon}:nn ${text.flavor_text}`;
        Object.assign(response_obj, {
          fulfillmentText
        });
      } 

      if (intent.displayName == 'evolution') {
        const evolution_response = await axios_instance.get(`/evolution-chain/${evolution_chain_id}`);
        const evolution_requirement = parameters.evolutions;
       
        let pokemon_evolutions = [evolution_response.data.chain.species.name];
        fulfillmentText = `${pokemon} has no evolution`;

        if (evolution_response.data.chain.evolves_to.length) {
          pokemon_evolutions.push(evolution_response.data.chain.evolves_to[0].species.name);
        }

        if (evolution_response.data.chain.evolves_to[0].evolves_to.length) {
          pokemon_evolutions.push(evolution_response.data.chain.evolves_to[0].evolves_to[0].species.name);
        }

        let evolution_chain = pokemon_evolutions.join(' -> ');

        const order_in_evolution_chain = pokemon_evolutions.indexOf(pokemon);
        const next_form = pokemon_evolutions[order_in_evolution_chain + 1];
        const previous_form = pokemon_evolutions[order_in_evolution_chain - 1];
        
        const evolution_text = {
          'evolution_chain': `${pokemon}'s evolution chain is: ${evolution_chain}`,
          'first_evolution': (pokemon == pokemon_evolutions[0]) ? `This is already the first form` : `${pokemon_evolutions[0]} is the first evolution`,
          'last_evolution': (pokemon == pokemon_evolutions[pokemon_evolutions.length - 1]) ? `This is already the final form` : pokemon_evolutions[pokemon_evolutions.length - 1],
          'next_form': `${pokemon} evolves to ${next_form}`,
          'previous_form': `${pokemon} evolves from ${previous_form}`
        };

        if (evolution_text[evolution_requirement]) {
          fulfillmentText = evolution_text[evolution_requirement];
        }

        Object.assign(response_obj, {
          fulfillmentText
        });
      }
    }

    if (get_type_effectiveness) {
      const pokemon_type = parameters.pokemon_types;
      let type_effectiveness = parameters.type_effectiveness;
      const type_effectiveness_formatted = type_effectiveness.replace(/_/g, ' ');
      const type_effectiveness_word = outputContexts[0].parameters['type_effectiveness.original'];

      let from_or_to = type_effectiveness.split('_')[2];
      const pokemon_type_comes_first = (queryText.indexOf(pokemon_type) < queryText.indexOf(type_effectiveness_word)) ? true : false;

      const exempt_words = ['resistant', 'no damage', 'zero damage', 'no effect'];
      const has_exempt_words = exempt_words.some(v => type_effectiveness_word.includes(v));

      if (
        (pokemon_type_comes_first && !has_exempt_words) ||
        (!pokemon_type_comes_first && has_exempt_words)
      ) {
        let new_from_or_to = (from_or_to == 'from') ? 'to' : 'from';
        type_effectiveness = type_effectiveness.replace(from_or_to, new_from_or_to);
        from_or_to = new_from_or_to;
      }
     
      const response = await axios_instance.get(`/type/${pokemon_type}`);
      const damage_relations = (response.data.damage_relations[type_effectiveness].length > 0) ? response.data.damage_relations[type_effectiveness].map(item => item.name).join(', ') : 'none';
     
      const nature_of_damage = (from_or_to == 'from') ? 'receives' : 'inflicts';

      fulfillmentText = (nature_of_damage == 'inflicts') ? 
        `${pokemon_type} type inflicts ${type_effectiveness_formatted} ${damage_relations} type` : 
        `${pokemon_type} ${nature_of_damage} ${type_effectiveness_formatted} the following: ${damage_relations}`;

      Object.assign(response_obj, {
        fulfillmentText
      });
    }

    return res.json(response_obj);

  } catch (err) {
    console.log('err: ', err);
    return res.json({
      fulfillmentText: "Sorry, the API is currently unavailable"
    });
  }
});

Let’s break down the above code into more manageable chunks so you know exactly what it does. First, we extract the request data that’s passed by Dialogflow:

  • intent — contains information about the triggered intent.
  • parameters — the specific entity values the parameters got translated to. For example, if the user asked: “What’s the evolution of Pikachu?”, in this case, “evolution” is considered by Dialogflow as a parameter that’s linked to the evolutions entity. So it looks it up on that entities dictionary and finds out that “evolution” is a synonym of next_form. This sets parameters.evolution equal to next_form. The same process applies to “Pikachu” so parameters.pokemon will have that as its value.
  • outputContexts — contains the output context that you’ve set in the intent. Earlier, in the get_type_effectiveness intent, we set type_effectiveness as an output context. You learned that this allows us to get the original value of a specific parameter before being translated. In the case of the evolution parameter from the previous item, the original value of next_form is “evolution”.
  • queryText — contains the actual message sent by the user.
const { intent, parameters, outputContexts, queryText } = req.body.queryResult;

const pokemon = (parameters.pokemon) ? parameters.pokemon.toLowerCase().replace('.', '-').replace(' ', '').replace("'", "") : ''; 
const specs = parameters.specs;
const get_type_effectiveness = (parameters.type_effectiveness) ? true : false;

let response_obj = {};

Note that we’re formatting the name of the Pokémon a bit on the above code because we’re directly using that name to make a request to the API. Most of the Pokémon names are one word, so simply converting to lowercase does the trick. But for Pokemon names like Mr. Mime, Mime Jr., and Farfetch’d, we need to format the string a bit for it to be considered as a valid identifier.

Here’s a sample request body we get from Dialogflow:

{  
   "responseId":"xxxx-xxx-xxx-xx-xxxxxx-xxxxx",
   "queryResult":{  
      "queryText":"What is super effective against ice type?",
      "parameters":{  
         "pokemon_types":"ice",
         "type_effectiveness":"double_damage_from"
      },
      "allRequiredParamsPresent":true,
      "fulfillmentMessages":[  
         {  
            "text":{  
               "text":[  
                  ""
               ]
            }
         }
      ],
      "outputContexts":[  
         {  
            "name":"projects/newagent-xxxxx/agent/sessions/xxxxx-xxxx-xxxx-xxxx-xxxxxxxxxx/contexts/type_effectiveness",
            "lifespanCount":5,
            "parameters":{  
               "pokemon_types":"ice",
               "pokemon_types.original":"ice",
               "type_effectiveness":"double_damage_from",
               "type_effectiveness.original":"super effective against"
            }
         }
      ],
      "intent":{  
         "name":"projects/newagent-xxxxx/agent/intents/xxxxxx-xxxx-xxxx-xxxx-xxxxxxxx",
         "displayName":"get_type_effectiveness"
      },
      "intentDetectionConfidence":1,
      "languageCode":"en"
   },
   "originalDetectIntentRequest":{  
      "payload":{  

      }
   },
   "session":"projects/newagent-xxxxx/agent/sessions/xxxxx-xxxx-xxxx-xxxx-xxxxxxxxxx"
}

Next, we add the code for handling the pokemon_endpoint. On the 2nd line, we make a request to the /pokemon endpoint of PokéAPI. We then extract the id (National Pokédex ID) of the Pokémon and the value being asked by the user.

In this case, there are only two possible details: abilities or moves. We then construct the fulfillmentText based on that value. However, if the user asked for the photo, this is where we include the additional payload property to the response_obj:

if (pokemon_endpoint.indexOf(specs) !== -1) {
  const { data } = await axios_instance.get(`/pokemon/${pokemon}`);

  let fulfillmentText;
  const id = String(data.id).padStart(3, '0');
  const value = (specs == 'abilities') ? data.abilities.map(item => item.ability.name).join(', ') : data.moves.map(item => item.move.name).join(', ');

  fulfillmentText = `The ${specs} of ${pokemon} are: ${value}`;

  Object.assign(response_obj, { fulfillmentText });

  if (specs == 'photo') {
    Object.assign(response_obj, {
      fulfillmentText: pokemon,
      payload: {
        is_image: true,
        url: `https://www.pkparaiso.com/imagenes/xy/sprites/global_link/${id}.png`
      }
    });
  }
}

Next, we add the code for handling both the pokemon_species_endpoint and the evolution intent. Both of these need to make a request to the /pokemon-species endpoint of the PokéAPI, which is why we’ve grouped them together.

Once we get a response from the API, we extract the evolution_chain_id. This is the ID we need to use for the next request, which hits the /evolution-chain API endpoint. From there, we just assemble the array that contains all the Pokémon in the evolution chain and construct the fullfillmentText based on the value of parameters.evolution:

if (pokemon_species_endpoint.indexOf(specs) !== -1 || intent.displayName == 'evolution') {

    const { data } = await axios_instance.get(`/pokemon-species/${pokemon}`);

    const evolution_chain_id = data.evolution_chain.url.split('/')[6];
    const text = data.flavor_text_entries.find(item => {
      return item.language.name == 'en';
    });

    let fulfillmentText;
    if (specs == 'description') {
      fulfillmentText = `${pokemon}:nn ${text.flavor_text}`;
      Object.assign(response_obj, {
        fulfillmentText
      });
    } 

    if (intent.displayName == 'evolution') {
      const evolution_response = await axios_instance.get(`/evolution-chain/${evolution_chain_id}`);
      const evolution_requirement = parameters.evolutions;
    
      let pokemon_evolutions = [evolution_response.data.chain.species.name];
      fulfillmentText = `${pokemon} has no evolution`;

      if (evolution_response.data.chain.evolves_to.length) {
        pokemon_evolutions.push(evolution_response.data.chain.evolves_to[0].species.name);
      }

      if (evolution_response.data.chain.evolves_to[0].evolves_to.length) {
        pokemon_evolutions.push(evolution_response.data.chain.evolves_to[0].evolves_to[0].species.name);
      }

      let evolution_chain = pokemon_evolutions.join(' -> ');
      
      const order_in_evolution_chain = pokemon_evolutions.indexOf(pokemon);
      const next_form = pokemon_evolutions[order_in_evolution_chain + 1];
      const previous_form = pokemon_evolutions[order_in_evolution_chain - 1];

      const evolution_text = {
        'evolution_chain': `${pokemon}'s evolution chain is: ${evolution_chain}`,
        'first_evolution': (pokemon == pokemon_evolutions[0]) ? `This is already the first form` : `${pokemon_evolutions[0]} is the first evolution`,
        'last_evolution': (pokemon == pokemon_evolutions[pokemon_evolutions.length - 1]) ? `This is already the final form` : pokemon_evolutions[pokemon_evolutions.length - 1],
        'next_form': `${pokemon} evolves to ${next_form}`,
        'previous_form': `${pokemon} evolves from ${previous_form}`
      };

      if (evolution_text[evolution_requirement]) {
        fulfillmentText = evolution_text[evolution_requirement];
      }

      Object.assign(response_obj, {
        fulfillmentText
      });
    }
  }

Next, we add the code for handling the get_type_effectiveness intent. This is where we extract the outputContexts to get the original word or phrase that got translated to an entity value. So if the user typed in “What is rock type super effective against?”, the agent determines “super effective” to be a parameter, and it gets translated to the entity value double_damage_from. Thus, parameters.type_effectiveness will have a value of double_damage_from.

The original phrase “super effective” is needed because we need to check for the exempt_words. We’re using these exempt_words to transpose the damage relation for the determined type_effectiveness. For example, if the user typed in “What has no effect to ground type?”, based on the dictionary of type_effectiveness entity, “no effect” gets translated to no_damage_to which means that it will return all the Pokémon types against which ground type moves have no effect (e.g. Flying type).

But if you examine carefully, the user actually means “get all Pokemon move types in which ground type Pokémon receives no damage from” (e.g. Electric type moves). This means that we need no_damage_from instead of no_damage_to. This is where the original phrase comes in, because it’s what we’re using to check for the exempt_words. Along with the condition to check if the Pokémon type came first in the user’s original query, we need it because the order in which the Pokémon type appears in the user’s query completely changes its meaning. Take, for example, the following:

  1. What is [super effective] against <dark> type? [effectiveness]<type>
  2. What is <dark> type [super effective] against? <type>[effectiveness]

To the agent, the queries above are basically the same. Even humans won’t notice the difference if they’re not paying attention. The first one is asking for the weakness of dark type Pokemon, while the second one is asking for the Pokémon types that are weak against dark type.

From there, we just check for the specific conditions for negating the damage relation (either from or to) and then negate the actual field that we’re extracting from the API’s response:

if (get_type_effectiveness) {
    const pokemon_type = parameters.pokemon_types;
    let type_effectiveness = parameters.type_effectiveness;
    const type_effectiveness_formatted = type_effectiveness.replace(/_/g, ' ');
    const type_effectiveness_word = outputContexts[0].parameters['type_effectiveness.original'];

    let from_or_to = type_effectiveness.split('_')[2];

    const pokemon_type_comes_first = (queryText.indexOf(pokemon_type) < queryText.indexOf(type_effectiveness_word)) ? true : false;

    const exempt_words = ['resistant', 'no damage', 'zero damage', 'no effect'];
    const has_exempt_words = exempt_words.some(v => type_effectiveness_word.includes(v));

    if (
      (pokemon_type_comes_first && !has_exempt_words) ||
      (!pokemon_type_comes_first && has_exempt_words)
    ) {
      let new_from_or_to = (from_or_to == 'from') ? 'to' : 'from';
      type_effectiveness = type_effectiveness.replace(from_or_to, new_from_or_to);
      from_or_to = new_from_or_to;
    }

    const response = await axios_instance.get(`/type/${pokemon_type}`);
    const damage_relations = (response.data.damage_relations[type_effectiveness].length > 0) ? response.data.damage_relations[type_effectiveness].map(item => item.name).join(', ') : 'none';

    const nature_of_damage = (from_or_to == 'from') ? 'receives' : 'inflicts';

    fulfillmentText = (nature_of_damage == 'inflicts') ? 
      `${pokemon_type} type inflicts ${type_effectiveness_formatted} ${damage_relations} type` : 
      `${pokemon_type} ${nature_of_damage} ${type_effectiveness_formatted} the following: ${damage_relations}`;

    Object.assign(response_obj, {
      fulfillmentText
    });
  }

Lastly, expose the server on port 5000:

const PORT = 5000;
app.listen(PORT, (err) => {
  if (err) {
    console.error(err);
  } else {
    console.log(`Running on ports ${PORT}`);
  }
});

Running the app

At this point, we’re now ready to run the app. First run the server and expose it to the internet using ngrok:

cd server
nodemon server.js
~/ngrok http 5000

Next, go to your Dialogflow agent’s fulfillment page and update the webhook URL with your ngrok HTTPS URL. Don’t forget to include the /pokedex endpoint. Save it once you’re done:

Finally, you can now run the app:

react-native run-android
react-native run-ios

Try asking the chatbot the questions from the App Overview section.

Conclusion and next steps

In this tutorial, you learned the basic concepts of how to build a chatbot with Dialogflow. Along the way, you also learned how to use the Dialogflow agent in a React Native app, as well as how to build a fulfillment server in Node.js.

The bot we’ve built in this tutorial is already great as far as basic Pokémon data goes. But just like anything else, there’s always room for improvement. The most important areas being availability and performance. As you might have noticed, PokéAPI is slow and sometimes the request times out because Dialogflow expects the fulfillment server to respond within five seconds. Even their documentation recommends that you cache the data if you’ll be using it regularly.

As for the app side of things, here are a few ideas:

  • Get type effectiveness about a Pokémon itself. This will let the bot respond to questions like: “What types are effective against Bulbasaur?”
  • Get type effectiveness on double-typed Pokémon.
  • Use speech-to-text so the user won’t have to type their queries.
  • Use text-to-speech for a screen-free experience.

You can find the source code of the app on this GitHub repo.

Fritz

Our team has been at the forefront of Artificial Intelligence and Machine Learning research for more than 15 years and we're using our collective intelligence to help others learn, understand and grow using these new technologies in ethical and sustainable ways.

Comments 0 Responses

Leave a Reply

Your email address will not be published. Required fields are marked *

wix banner square