Build a chatbot with Ionic and Dialogflow

  • Christian Nwamba
July 2nd, 2018
You will need Node and npm installed on your machine. A basic knowledge of Ionic development will be helpful.

Introduction

Over the past few years, advances in machine learning, especially natural language processing (NLP), have made it easy for computers to analyze and derive meaning from human language in a smart way. With this, developers can now create smaller, simpler and more intuitive natural language processing software.

In this tutorial, we’ll demonstrate how to build a chatbot using Pusher with an Ionic application and also using the Dialogflow conversation platform formerly know as api.ai by Google. This bot will engage in simple conversations.

Here is the demo of the final product:

We will send our message to our server and Dialogflow, with the help of Pusher we will update our chat conversation in realtime. To make use of Pusher, you’ll have to create an account here. Let’s build!

Prerequisites

To follow this tutorial a basic understanding of Angular, Ionic and Node.js is required. Please ensure that you have Node and npm installed before you begin. If you have no prior knowledge of Ionic, kindly follow the tutorial here. Come back and finish the tutorial when you’re done.

We’ll be using these tools to build out our application:

  1. Express
  2. Node
  3. Pusher
  4. Ionic
  5. Dialogflow

Setup and folder structure

To get started, we will use the CLI (command line interface) provided by the Ionic team to initialize our project.

First, install the CLI by running npm install -g ionic cordova. NPM is a package manager used for installing packages. It will be available on your PC if you have Node installed.

To create a new Ionic App using the CLI, open a terminal and run:

    ionic start chatbotApp blank

The command is merely telling the CLI to create a new app called chatbotApp and it should make use of the blank starter template. While the setup is running you might get a prompt "Would you like to integrate your new app with Cordova to target native iOS and Android?". If you want to start running or testing the application on a mobile device or emulator as you build, then choose yes by typing Y and pressing Enter on your keyboard, else type N and continue. You might get another prompt "If you would like to integrate ionic pro ?" , we wouldn’t be needing that in this tutorial so just type N and if you would like to extend the project into production with your team choose Y.

Open the newly created chatbotApp. Your folder structure should look something like this:

    chatbotApp /
      node_modules /
      src / 
        app /
          assets /
          pages /
            home /
              home.html
              home.ts
              home.scss

Open a terminal inside the app folder and start the application by running:

    ionic serve 

Automatically your default browser should open, and you should see the screenshot below if everything went well.

Installing dependencies and creating providers

First, we need to add the pusher cdn to the index.html file in order to use Pusher in our application :

Open index.html file in the src folder of the project and paste the cdn into it:

    ...

      <script src="https://js.pusher.com/4.1/pusher.min.js"></script>
      <!-- add to homescreen for ios --> 
    ...

Next, we need to create two providers to manage our Pusher instance (we don’t always have to create a new instance of Pusher anytime we want to use it, we can use just one instance of it throughout the entire app) and our chat (interaction with the remote server and vice versa for chat messages):

      ionic g provider pusher-service
      ionic g provider chat-service

We also need to create an interface to model out how each chat message should be. Create a new folder in our src directory called models and create a file in it called chatModel.ts and update it like this:

    // src/models/chatModel.ts

    export interface IChat {
        id : string;
        message : string;
        isMe : boolean;
        createdAt : string;
        type : 'human' | 'bot'   
    }

Now open our Pusher provider and update it like the one below:

    // src/providers/pusher-service/pusher-service.ts

    import { HttpClient } from '@angular/common/http';
    import { Injectable } from '@angular/core';

    declare const Pusher: any;
    @Injectable()
    export class PusherServiceProvider {
      public _pusher : any;
      constructor(public http: HttpClient) {
        this._pusher = new Pusher("PUSHER_KEY", {
          cluster: "CLUSTER",
          encrypted: true
        });
      }

      getPusher(){
        return this._pusher;
      }
    }

First, we initialize Pusher in the constructor. The getPusher() method returns the Pusher property we created. Ensure you replace the PUSHER_KEY and CLUSTER string with your actual Pusher key and cluster from Pusher.

To make the provider available in the application, import it into the app.module.ts file:

    // ../src/app/app.module.ts

    import { PusherServiceProvider } from '../providers/pusher-service/pusher-service';
    ....
    providers: [
    StatusBar,
    SplashScreen,
    {provide: ErrorHandler, useClass: IonicErrorHandler},
    PusherServiceProvider
    ]
    ....

Let’s update our chat-service provider for sending messages to our bot and connect to Pusher’s channel.

    // src/providers/chat-service/chat-service.ts

    import { PusherServiceProvider } from './../pusher-service/pusher-service';
    import { HttpClient } from '@angular/common/http';
    import { Injectable } from '@angular/core';
    import { Observable } from 'rxjs';

    @Injectable()
    export class ChatServiceProvider {

      private _url = 'http://localhost:5000';
      private _channel : any;
      constructor(public http: HttpClient, private _pusher : PusherServiceProvider) {
        this._channel = this._pusher.getPusher().subscribe('chat-bot');  
      }


      sendMessage( message : string) : Observable<any>{
        const param = {
          type: 'human',
          message,
        };
        return this.http.post(`${this._url}/message`, param)
      }
      getChannel(){
        return this._channel;
      }
    }
  • sendMessage(): this method takes in our inputted message and sends it to the server.
  • getChannel(): is returning our Pusher channel so we can easily subscribe to any message coming from chat-bot channel.

Let’s add the chat-service provider and HttpclientModule for HTTP requests to the app.module.ts. FormsModule is going to be used later in building our chat input form.

    // src/app/app.module.ts

    import { ChatServiceProvider } from '../providers/chat-service/chat-service';
    import { HttpClientModule } from '@angular/common/http';
    import { FormsModule } from '@angular/forms';
    ....
    imports: [
    ...
    HttpClientModule,
    FormsModule,
    ...
    ]
    providers: [
    StatusBar,
    SplashScreen,
    {provide: ErrorHandler, useClass: IonicErrorHandler},
    PusherServiceProvider,
    ChatServiceProvider
    ]
    ....

Building the chat interface

Open the home.html and update it with the code below :

    // src/pages/home/home.html

    <ion-header>
      <ion-navbar>
        <ion-title>ChatBot</ion-title>
      </ion-navbar>
    </ion-header>

    <ion-content padding>
        <div class="chats">
          <div class="chatbox">
            <div *ngFor="let chat of chats">
              <div class="message sent" *ngIf="chat.type!=='bot' && chat.isMe">
                {{chat.message}}
                <span class="metadata">
                  <span class="time">{{chat.createdAt | date: 'HH:mm aaa'}}</span>
                </span>
              </div>
              <div class="message received"  *ngIf="chat.type =='bot' && !chat.isMe">
                {{chat.message}}
                <span class="metadata">
                  <span class="time">{{chat.createdAt | date: 'HH:mm aaa'}}</span>
                </span>
              </div>
            </div>
          </div>
          </div>

      <ion-footer>
          <p text-center *ngIf="sending">sending...</p>
          <div class="flex-items" padding>

              <ion-input [(ngModel)]="message" name="message" class="input_message" placeholder="send message ..."></ion-input>
              <ion-icon (click)="sendMessage()" class="send" name="send"></ion-icon>
            </div>
      </ion-footer>
    </ion-content>

For the above template, we are making use of the *ngFor directive to loop through our chats and display them based on the type of message.

  • bot: here we are using the *ngIf directive to check if the message sent is from the bot, then it uses the message received class which we would create soon.
  • isMe: this checks if it was the user that sent the message.

Now, let’s update our home.scss file with the styles defined below :

    // src/pages/home/home.scss

    .flex-items{
            display:  flex;
            background-color: #fff;
        }
        .icon{
            font-size: 50px;
            opacity: 0.5;
            padding-left: 40px;
            align-self: center;
        }
        .send{
            color: #603CD7;
            font-size: 30px;
        }
        .input_message{
            border: 1px solid #603CD7;
            border-radius: 15px; 
        }
        .messages{
            height: 100%;
            position: relative;
        }
        .footer{
            position: fixed;
            bottom: 5%;
        }
        .chats {
            height: 100%;
            position: relative;
            z-index: 0;
        }
        .chats .chatbox {
            height: calc(100% - 80px);
            overflow-x: hidden;
            padding: 0 16px;
            padding-bottom: 30px;
        }
        .joined {
          clear: both;
          line-height: 18px;
          font-size: 15px;
          margin: 8px 0;
          padding: 8px;
        }
        .joined span {
          padding: 5px
        }
        /* Messages*/
        .message {
          color: #000;
          clear: both;
          line-height: 18px;
          font-size: 15px;
          padding: 8px;
          position: relative;
          margin: 8px 0;
          max-width: 85%;
          word-wrap: break-word;
          z-index: -1;
        }
        .message:after {
          position: absolute;
          content: "";
          width: 0;
          height: 0;
          border-style: solid;
        }
        .message:first-child {
          margin: 16px 0 8px;
        }
        .message.received {
          background: #ccc;
          border-radius: 0px 5px 5px 5px;
          float: left;
        }

        .message.received:after {
          border-width: 0px 10px 10px 0;
          border-color: transparent #ccc transparent transparent;
          top: 0;
          left: -10px;
        }
        .message.sent {
          background: #e1ffc7;
          border-radius: 5px 0px 5px 5px;
          float: right;
        }
        .message.sent:after {
          border-width: 0px 0 10px 10px;
          border-color: transparent transparent transparent #e1ffc7;
          top: 0;
          right: -10px;
        }
        .scroll-content{
          padding: 0 !important;
        }

Let’s create the missing variables and methods that have been defined in our template:

    // src/pages/home/home.ts

    import { ChatServiceProvider } from './../../providers/chat-service/chat-service';
    import { IChat } from './../../models/chatModel';
    import { Component } from '@angular/core';
    import { IonicPage, NavController, NavParams } from 'ionic-angular';

    @Component({
      selector: 'page-home',
      templateUrl: 'home.html',
    })
    export class HomePage {
      chats : IChat[] = [];
      message : string;
      sending : boolean;

      constructor(public navCtrl: NavController, public navParams: NavParams, private _chat : ChatServiceProvider) {
      }
      ionViewDidLoad() {
         // subscribe to pusher's event
         this._chat.getChannel().bind('chat', data => {
          if(data.type !== 'bot'){
            data.isMe = true;
          };
          this.chats.push(data);
        });
      }
      sendMessage() {
        this.sending = true;
        this._chat.sendMessage(this.message)
          .subscribe(resp => {
            this.message = '';
            this.sending = false;
          }, err => {
            this.sending = false;
          } );
      }
    }

From the above code, we subscribed to our Pusher channel for chat-bot when the page is being called by Ionic (ionViewDidLoad). We also checked if the type of message coming in from Pusher isn’t from the bot. We used the sendMessage method to send messages to our server and toggling a variable called sending to notify our template to show what is going on to the users. Then we reset the message to an empty string to clear the message input field on our home.html.

Now if you should run the app you should observe something similar to the picture below:

This is because the chat-service provider url doesn’t exist yet. Let’s set up our server.

Setting up our Node server with Pusher

Our Node server is going to handle when a new message is sent from our Ionic application. Open your terminal and run the below command:

    npm install express body-parser cors pusher dotenv shortid

What we have done above is to install our node dependencies. Create a server.js in the root directory of our project. This will hold our APIs using Express and a .env file to help manage our environment variables using the dotenv module. Open server.js and update it to look like so:

      // server.js
        const express = require('express')
        const bodyParser = require('body-parser')
        const Pusher = require('pusher')
        const cors = require('cors')
        require('dotenv').config()
        const shortId = require('shortid') 
        const app = express()

        app.use(cors())
        app.use(bodyParser.urlencoded({ extended: false }))
        app.use(bodyParser.json())
        const pusher = new Pusher({
          appId: process.env.PUSHER_APP_ID,
          key: process.env.PUSHER_APP_KEY,
          secret: process.env.PUSHER_APP_SECRET,
          cluster: 'eu',
          encrypted: true
        })
        app.post('/message', async (req, res) => {
          // simulate actual db save with id and createdAt added
          console.log(req.body);
          const chat = {
            ...req.body,
            id: shortId.generate(),
            createdAt: new Date().toISOString()
          } 
          //update pusher listeners
          pusher.trigger('chat-bot', 'chat', chat)
          res.send(chat)
        })

        app.listen(process.env.PORT || 5000, () => console.log('Listening at 5000'))

We have created an API /message, which sends a message to our bot which we will configure soon by triggering an event with Pusher so our chat[] can receive the message.

Before running our server to test our group chat, we need to set our .env variables used in our server.js. Open .env and make it look like so:

      PUSHER_APP_ID= "APP_ID"
      PUSHER_APP_KEY= "APP_KEY"
      PUSHER_APP_SECRET= "APP_SECRET"
      PUSHER_CLUSTER = "CLUSTER"

You can get your own .env config within your Pusher’s channel application dashboard. Navigate to the Getting Started tab, under Add this to your server column, select .env to copy, then paste in your .env file.

Let’s configure our chatbot.

Integrating Dialogflow with Pusher

Let’s add our chatbot so the user doesn’t just messages himself. Head over to Dialogflow to set up our chatbot. Open your Dialogflow dashboard after creating a free account:

Creating small talk

For a quick start, we can easily program our chatbot for common chat style questions in the small talk panel. This will give our bot a basic level of interactivity with the users. By default, there are responses with predefined phrases within the small talk panel. Go ahead and customize the response as you deem fit. For now, we are going to keep it simple and respond to few questions:

Enable and save it. You can use the Try it now by your right to test our chatbot responses. Let’s get our access token for connecting to our chatbot from our Node server:

Click on the icon within the red circle to view our agent settings where we can locate our API keys.

Open our .env and add our client access token, not the developer token, as our use case we are going to be doing more of querying of our chatbot via APIs. Read more.

    PUSHER_APP_ID='APP_ID'
    PUSHER_APP_KEY='APP_KEY'
    PUSHER_APP_SECRET='APP_SECRET'
    PUSHER_CLUSTER = 'CLUSTER'
    DIALOGFLOW_ACCESS_TOKEN = 'CLIENT_ACCESS_TOKEN'

Let’s install Axios for sending HTTP requests from our Node server to Dialogflow endpoints:

    npm install axios

Create a dialogflow.js file in the root folder where your server.js file is located and update it to look like the following:

    // dialogflow.js
    const axios = require('axios')
    const accessToken = process.env.DIALOGFLOW_ACCESS_TOKEN
    const baseURL = 'https://api.dialogflow.com/v1/query?v=20150910'
    module.exports = {
      send (message) {
        const data = {
          query: message,
          lang: 'en',
          sessionId: '123456789!@#$%'
        }
        return axios.post(baseURL, data, {
          headers: { Authorization: `Bearer ${accessToken}` }
        })
      }
    }

We used Axios to send a post request to Dialogflow, passing our message to the bot as query. Let’s make use of our utility function above to communicate with our chatbot. Open server.js and update it to look like so:

      // server.js
        const express = require('express')
        const bodyParser = require('body-parser')
        const Pusher = require('pusher')
        const cors = require('cors')
        require('dotenv').config()
        const shortId = require('shortid') 
        const dialogFlow = require('./dialogFlow')
        const app = express()
        app.use(cors())
        app.use(bodyParser.urlencoded({ extended: false }))
        app.use(bodyParser.json())
        const pusher = new Pusher({
          appId: process.env.PUSHER_APP_ID,
          key: process.env.PUSHER_APP_KEY,
          secret: process.env.PUSHER_APP_SECRET,
          cluster: 'eu',
          encrypted: true
        })
        app.post('/message', async (req, res) => {
          // simulate actual db save with id and createdAt added
          console.log(req.body);
          const chat = {
            ...req.body,
            id: shortId.generate(),
            createdAt: new Date().toISOString()
          } 
          //update pusher listeners
          pusher.trigger('chat-bot', 'chat', chat)

          const message = chat.message;
          const response = await dialogFlow.send(message);
          // trigger this update to our pushers listeners
          pusher.trigger('chat-bot', 'chat', {
            message: `${response.data.result.fulfillment.speech}`,
            type : 'bot',
            createdAt : new Date().toISOString(),
            id: shortId.generate()
          })
          res.send(chat)
        })

        app.listen(process.env.PORT || 5000, () => console.log('Listening at 5000'))

What we have done is to update the POST /message endpoint to handle any message sent from our Ionic application, then the server then sends the message to Dialogflow through our utility function. Dialogflow returns a response containing what our bot processed from the message. Then we triggered Pusher’s event to send a message to our user using the response from Dialogflow.

Let’s re-run our server like so:

    node server.js

And also restart your ionic application by running :

    ionic serve

Open our Ionic application and try sending a message, Our application should be working like the demo below:

Conclusion

Here, we have been able to build a chat application that connects the user to a chatbot on Dialogflow. The intention was to give you a general building block that can be built on, explored, and improved to build an amazing chatbot.

I hope this tutorial was helpful and gave you enough information required to start building bots tailored for other use cases, as you deem fit in your organization.

The source code for this tutorial can be found on GitHub. Feel free to explore and add more features.

  • Channels

© 2018 Pusher Ltd. All rights reserved.

Pusher Limited is a company registered in England and Wales (No. 07489873) whose registered office is at 160 Old Street, London, EC1V 9BW.